fleet-manager: host uptime metric + reboot-pending status

The manager lets an admin schedule a reboot (desired.reboot_at) but never showed
whether it had happened. Add host uptime to the health snapshot and derive a
reboot status from it.

- proto: HealthSnapshot.uptime_seconds (15); regenerated pb.go (manager + agent copy).
- agent: probe /proc/uptime -> uptime_seconds (with parser + test).
- manager: Health.UptimeSeconds cached in inventory; snapshotToHealth maps it;
  influx writes + whitelists uptime_seconds as a queryable field.
- UI: fmt.js dur() + rebootStatus() — boot_time = last_seen - uptime (manager clock
  frame, no host/manager sync needed), compared to reboot_at. Monitor host detail
  shows an "uptime" row and a "reboot" row: pending in X / overdue by X / rebooted.

Observability-only — works before the reboot ACTUATOR lands (deferred). Once a host
boots after the scheduled time (BMC/manual now, agent later), the badge flips to
"rebooted" automatically.
This commit is contained in:
Audiobuzz
2026-07-10 06:41:09 +00:00
parent e6ae8c4b23
commit df7dad81b7
11 changed files with 128 additions and 29 deletions
+32
View File
@@ -35,3 +35,35 @@ export function healthState(h) {
export function shortSerial(s) {
return s ? s.slice(0, 8) : '—'
}
// dur formats a span of SECONDS as a compact "3d 4h" / "5h 12m" / "45s".
export function dur(s) {
s = Math.max(0, Math.floor(s || 0))
const d = Math.floor(s / 86400)
const h = Math.floor((s % 86400) / 3600)
const m = Math.floor((s % 3600) / 60)
const sec = s % 60
if (d) return `${d}d ${h}h`
if (h) return `${h}h ${m}m`
if (m) return `${m}m ${sec}s`
return `${sec}s`
}
// rebootStatus reports whether a scheduled reboot (desired.reboot_at) has been
// actioned, by comparing the host's boot time to the requested time. Boot time is
// derived in the manager's clock frame: last_seen (a manager-stamped snapshot
// time) minus the reported host uptime — so it needs no host/manager clock sync.
// Returns null when no reboot is scheduled. state ∈ pending | overdue | done.
export function rebootStatus(host) {
const raIso = host?.desired?.reboot_at
if (!raIso) return null
const ra = new Date(raIso).getTime()
if (Number.isNaN(ra) || ra <= 0) return null // unset / Go zero time
const now = Date.now()
const up = host?.health?.uptime_seconds || 0
const ls = host?.last_seen ? new Date(host.last_seen).getTime() : NaN
const boot = up > 0 && ls > 0 ? ls - up * 1000 : NaN // manager-frame boot time
if (!Number.isNaN(boot) && boot >= ra) return { state: 'done', label: 'rebooted' }
if (ra > now) return { state: 'pending', label: 'reboot pending in ' + dur((ra - now) / 1000) }
return { state: 'overdue', label: 'reboot overdue by ' + dur((now - ra) / 1000) }
}
@@ -3,7 +3,7 @@
import Panel from '../components/Panel.svelte'
import StatePill from '../components/StatePill.svelte'
import Chart from '../components/Chart.svelte'
import { relTime, pct, num, healthState, shortSerial } from '../lib/fmt.js'
import { relTime, pct, num, healthState, shortSerial, dur, rebootStatus } from '../lib/fmt.js'
let hosts = $state([])
let error = $state('')
@@ -105,6 +105,11 @@
<dt class="text-faint">selinux</dt><dd class="text-muted">{selected.health?.selinux || '—'}</dd>
<dt class="text-faint">chrony</dt><dd class="text-muted">{selected.health?.chrony || '—'}</dd>
<dt class="text-faint">kiosk</dt><dd class="text-muted">{selected.health?.kiosk_active ? 'active' : 'idle'} · {selected.usage?.session_uptime || '—'}</dd>
<dt class="text-faint">uptime</dt><dd class="text-muted">{selected.health?.uptime_seconds ? dur(selected.health.uptime_seconds) : '—'}</dd>
{#if rebootStatus(selected)}
{@const rb = rebootStatus(selected)}
<dt class="text-faint">reboot</dt><dd class={rb.state === 'overdue' ? 'text-amber-hot' : rb.state === 'pending' ? 'text-amber' : 'text-teal'}>{rb.label}</dd>
{/if}
<dt class="text-faint">last seen</dt><dd class="text-muted">{selected.online ? 'live' : relTime(selected.last_seen)}</dd>
</dl>
<div class="mt-4 space-y-3">