fleet-manager: checkpoint 6 — the admin SPA (fleet command deck)

The themed single-page admin UI (serving/fleet-manager/ui/), embedded into the
binary and served same-origin over the admin plane.

- Svelte 5 + Tailwind 4 + Vite; a sibling of serving/kiosk-app. Theme = LYSANDRA
  'restricted-violet' (high-security evolution of the kiosk's flight-deck DNA):
  amber deck, VIOLET reserved for privileged/destructive + auth surfaces, teal
  telemetry, red alert, clearance chrome (corner-reticle panels, scanlines, mono
  clearance labels, secure-link status bar).
- Passkey gate: Login (usernameless discoverable WebAuthn) + Enrol (from the
  register-admin one-time link) via navigator.credentials, with base64url<->buffer
  helpers and the go-webauthn response shapes.
- Three pillars over the JSON API: Monitor (fleet roster + version-drift + health
  + per-host detail with bespoke SVG telemetry charts), Authorise (pending queue:
  fingerprint cross-check, approve+bind in violet, deny), Lifecycle (desired-state
  form; drain/reboot confirm-gated). Session-aware fetch client (cookie + CSRF).
- go:embed behind the  build tag (spa_embed.go / spa_stub.go) so the
  default go build/test need no dist/; mounted at / (api/auth/healthz win).

Verified: npm build clean (71KB JS / 33KB CSS + self-hosted fonts); default +
-tags embedspa go builds; smoke test serves index + assets + API through the SPA
origin. Against an insecure+seeded backend the deck renders passkey-free for
theme work. dist/ + node_modules gitignored.
This commit is contained in:
Audiobuzz
2026-07-04 05:54:41 +00:00
parent c40dd5ac93
commit fae3624c58
30 changed files with 3203 additions and 1 deletions
@@ -0,0 +1,111 @@
<script>
import { api } from '../lib/api.js'
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'
let hosts = $state([])
let error = $state('')
let selected = $state(null)
let metrics = $state({ tmpfs_used_pct: [], cpu_pct: [], temp_c: [] })
let metricsErr = $state('')
async function load() {
try {
const d = await api.get('/api/hosts')
hosts = d.hosts || []
error = ''
if (selected) selected = hosts.find((h) => h.identity.cert_serial === selected.identity.cert_serial) || selected
} catch (e) {
error = e?.message || 'failed to load fleet'
}
}
async function select(h) {
selected = h
metricsErr = ''
for (const f of Object.keys(metrics)) {
try {
const d = await api.get(`/api/hosts/${h.identity.cert_serial}/metrics?field=${f}&range=6h`)
metrics[f] = d.points || []
} catch (e) {
metrics[f] = []
metricsErr = e?.message || ''
}
}
}
function drift(h) {
const run = h.version?.image_version
const want = h.desired?.target_version
return want && run && want !== run
}
$effect(() => {
load()
const id = setInterval(load, 10000)
return () => clearInterval(id)
})
</script>
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
<div class="lg:col-span-2">
<div class="mb-3 flex items-baseline justify-between">
<h1 class="font-display text-amber">FLEET ROSTER</h1>
<span class="tag">{hosts.length} hosts · {hosts.filter((h) => h.online).length} online</span>
</div>
{#if error}<div class="pill pill-alert mb-3">{error}</div>{/if}
<div class="grid grid-cols-1 gap-3 sm:grid-cols-2">
{#each hosts as h (h.identity.cert_serial)}
<button class="panel p-3 text-left {selected?.identity.cert_serial === h.identity.cert_serial ? 'panel-restricted' : ''}" onclick={() => select(h)}>
<div class="flex items-center justify-between">
<span class="font-display text-ink">{h.identity.starship_name || shortSerial(h.identity.cert_serial)}</span>
<StatePill state={h.online ? healthState(h.health) : 'off'} />
</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><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">seen</span> {h.online ? 'live' : relTime(h.last_seen)}</div>
</div>
<div class="mt-2 flex flex-wrap gap-1.5">
{#if h.desired?.drain}<span class="pill pill-warn">drain</span>{/if}
{#if drift(h)}<span class="pill pill-warn">version-drift</span>{/if}
{#if h.health?.selinux === 'permissive'}<span class="pill pill-alert">selinux permissive</span>{/if}
</div>
</button>
{/each}
{#if !hosts.length && !error}<p class="text-muted">no hosts enrolled yet — approve one in AUTHORISE.</p>{/if}
</div>
</div>
<div>
{#if selected}
<Panel title={selected.identity.starship_name || shortSerial(selected.identity.cert_serial)} tag="host detail">
<dl class="grid grid-cols-2 gap-y-1 font-mono text-xs">
<dt class="text-faint">hostnqn</dt><dd class="truncate text-muted">{selected.identity.hostnqn || '—'}</dd>
<dt class="text-faint">running</dt><dd class="text-ink">{selected.version?.image_version || '—'}</dd>
<dt class="text-faint">desired</dt><dd class={drift(selected) ? 'text-amber-hot' : 'text-ink'}>{selected.desired?.target_version || '—'}</dd>
<dt class="text-faint">kernel</dt><dd class="truncate text-muted">{selected.version?.kernel || '—'}</dd>
<dt class="text-faint">auth</dt><dd class="text-muted">{selected.health?.auth || '—'}</dd>
<dt class="text-faint">nvme root</dt><dd class="text-muted">{selected.health?.nvme_root || '—'}</dd>
<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">last seen</dt><dd class="text-muted">{selected.online ? 'live' : relTime(selected.last_seen)}</dd>
</dl>
<div class="mt-4 space-y-3">
<Chart label="tmpfs used %" color="var(--color-amber)" points={metrics.tmpfs_used_pct} />
<Chart label="cpu %" color="var(--color-teal)" points={metrics.cpu_pct} />
<Chart label="temp °C" color="var(--color-amber-hot)" points={metrics.temp_c} />
{#if metricsErr}<p class="tag text-faint">metrics: {metricsErr}</p>{/if}
</div>
</Panel>
{:else}
<Panel title="host detail" tag="select a ship">
<p class="text-sm text-muted">Select a host from the roster to see live health + telemetry charts.</p>
</Panel>
{/if}
</div>
</div>