fleet-manager ui: readability + reboot-now

- Contrast: lighten the muted/faint text tokens (AA on the hull panels) and make
  Monitor host-card data values bright ink against dim faint labels, so version/
  tmpfs/seen details read clearly on the blue-grey panels (was grey-on-grey).
- Lifecycle: reboot selector now offers none | now | at... — 'now' sets
  reboot_at to the current time (agent reboots on reconcile); still confirm-gated
  as a high-consequence action.
This commit is contained in:
Audiobuzz
2026-07-04 06:05:29 +00:00
parent fae3624c58
commit 20b29d9990
3 changed files with 28 additions and 17 deletions
+4 -4
View File
@@ -27,10 +27,10 @@
--color-red: #FF3B2F; /* alert / out-of-range */ --color-red: #FF3B2F; /* alert / out-of-range */
--color-amber-hot: #FF8A3D; /* caution band */ --color-amber-hot: #FF8A3D; /* caution band */
/* text */ /* text — tuned for legibility on the dark hull panels (AA on hull2) */
--color-ink: #E9EEF6; /* primary text (warm off-white) */ --color-ink: #EAF1F9; /* primary text (warm off-white) */
--color-muted: #7E8DA3; /* secondary text, labels */ --color-muted: #AEC0D6; /* secondary text / data values — clearly readable */
--color-faint: #46566E; /* tertiary, timestamps, meta */ --color-faint: #7688A2; /* tertiary labels, timestamps, meta */
/* hairlines */ /* hairlines */
--color-line: #223146; /* structural */ --color-line: #223146; /* structural */
@@ -10,6 +10,7 @@
let selected = $state(null) let selected = $state(null)
let busy = $state(false) let busy = $state(false)
let confirmOpen = $state(false) let confirmOpen = $state(false)
let rebootMode = $state('none') // none | now | at
let form = $state({ target_version: '', kiosk_url: '', reboot_at: '', drain: false }) let form = $state({ target_version: '', kiosk_url: '', reboot_at: '', drain: false })
async function load() { async function load() {
@@ -24,6 +25,7 @@
function pick(h) { function pick(h) {
selected = h selected = h
okMsg = '' okMsg = ''
rebootMode = 'none'
form = { form = {
target_version: h.desired?.target_version || '', target_version: h.desired?.target_version || '',
kiosk_url: h.desired?.kiosk_url || '', kiosk_url: h.desired?.kiosk_url || '',
@@ -31,9 +33,8 @@
drain: !!h.desired?.drain, drain: !!h.desired?.drain,
} }
} }
let consequence = $derived( let rebootConseq = $derived(rebootMode === 'now' ? 'REBOOT it NOW' : rebootMode === 'at' && form.reboot_at ? 'schedule a REBOOT' : null)
[form.drain ? 'DRAIN this host' : null, form.reboot_at ? 'schedule a REBOOT' : null].filter(Boolean).join(' and ') let consequence = $derived([form.drain ? 'DRAIN this host' : null, rebootConseq].filter(Boolean).join(' and '))
)
async function apply() { async function apply() {
confirmOpen = false confirmOpen = false
busy = true busy = true
@@ -44,7 +45,8 @@
kiosk_url: form.kiosk_url || undefined, kiosk_url: form.kiosk_url || undefined,
drain: form.drain, drain: form.drain,
} }
if (form.reboot_at) body.reboot_at = new Date(form.reboot_at).toISOString() if (rebootMode === 'now') body.reboot_at = new Date().toISOString()
else if (rebootMode === 'at' && form.reboot_at) body.reboot_at = new Date(form.reboot_at).toISOString()
try { try {
const res = await api.put(`/api/hosts/${selected.identity.cert_serial}/desired`, body) const res = await api.put(`/api/hosts/${selected.identity.cert_serial}/desired`, body)
okMsg = res.pushed_live ? 'applied — pushed to the live host' : 'applied — host reconciles on next connect' okMsg = res.pushed_live ? 'applied — pushed to the live host' : 'applied — host reconciles on next connect'
@@ -58,7 +60,7 @@
} }
function submit() { function submit() {
// Gate destructive changes behind a confirm; a plain version/URL change applies directly. // Gate destructive changes behind a confirm; a plain version/URL change applies directly.
if (form.drain || form.reboot_at) confirmOpen = true if (form.drain || rebootMode === 'now' || (rebootMode === 'at' && form.reboot_at)) confirmOpen = true
else apply() else apply()
} }
$effect(() => { $effect(() => {
@@ -94,10 +96,19 @@
<span class="tag">kiosk url</span> <span class="tag">kiosk url</span>
<input class="mt-1 w-full rounded border border-line bg-void2 px-2 py-1.5 text-sm text-ink" bind:value={form.kiosk_url} placeholder="http://boot.fleet.lab/kiosk/" /> <input class="mt-1 w-full rounded border border-line bg-void2 px-2 py-1.5 text-sm text-ink" bind:value={form.kiosk_url} placeholder="http://boot.fleet.lab/kiosk/" />
</label> </label>
<label class="block"> <div class="block">
<span class="tag">reboot at (optional)</span> <span class="tag">reboot</span>
<input type="datetime-local" class="mt-1 w-full rounded border border-line bg-void2 px-2 py-1.5 text-sm text-ink" bind:value={form.reboot_at} /> <div class="mt-1 flex items-center gap-2">
</label> <select class="rounded border border-line bg-void2 px-2 py-1.5 text-sm {rebootMode !== 'none' ? 'text-violet-soft' : 'text-ink'}" bind:value={rebootMode}>
<option value="none">none</option>
<option value="now">now</option>
<option value="at">at…</option>
</select>
{#if rebootMode === 'at'}
<input type="datetime-local" class="flex-1 rounded border border-line bg-void2 px-2 py-1.5 text-sm text-ink" bind:value={form.reboot_at} />
{/if}
</div>
</div>
<label class="mt-4 flex items-center gap-2"> <label class="mt-4 flex items-center gap-2">
<input type="checkbox" bind:checked={form.drain} /> <input type="checkbox" bind:checked={form.drain} />
<span class="font-display text-sm text-violet-soft">drain (stop taking work)</span> <span class="font-display text-sm text-violet-soft">drain (stop taking work)</span>
@@ -64,10 +64,10 @@
<StatePill state={h.online ? healthState(h.health) : 'off'} /> <StatePill state={h.online ? healthState(h.health) : 'off'} />
</div> </div>
<div class="tag mt-0.5">{shortSerial(h.identity.cert_serial)} · {h.binding?.location || 'unbound'}</div> <div class="tag mt-0.5">{shortSerial(h.identity.cert_serial)} · {h.binding?.location || 'unbound'}</div>
<div class="mt-2 grid grid-cols-3 gap-2 font-mono text-xs text-muted"> <div class="mt-2 grid grid-cols-3 gap-2 font-mono text-xs text-ink">
<div><span class="text-faint">ver</span> {h.version?.image_version || '—'}{#if drift(h)}<span class="text-amber-hot"> ⇢ {h.desired.target_version}</span>{/if}</div> <div><span class="text-faint">ver </span>{h.version?.image_version || '—'}{#if drift(h)}<span class="text-amber-hot"> ⇢ {h.desired.target_version}</span>{/if}</div>
<div><span class="text-faint">tmpfs</span> {pct(h.health?.tmpfs_used_pct)}</div> <div><span class="text-faint">tmpfs </span>{pct(h.health?.tmpfs_used_pct)}</div>
<div><span class="text-faint">seen</span> {h.online ? 'live' : relTime(h.last_seen)}</div> <div><span class="text-faint">seen </span>{h.online ? 'live' : relTime(h.last_seen)}</div>
</div> </div>
<div class="mt-2 flex flex-wrap gap-1.5"> <div class="mt-2 flex flex-wrap gap-1.5">
{#if h.desired?.drain}<span class="pill pill-warn">drain</span>{/if} {#if h.desired?.drain}<span class="pill pill-warn">drain</span>{/if}