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.
43 lines
1.6 KiB
Svelte
43 lines
1.6 KiB
Svelte
<script>
|
|
import { router } from '../lib/router.svelte.js'
|
|
import { passkeyRegister, passkeySupported } from '../lib/webauthn.js'
|
|
let busy = $state(false)
|
|
let done = $state(false)
|
|
let who = $state('')
|
|
let error = $state('')
|
|
async function enrol() {
|
|
busy = true
|
|
error = ''
|
|
try {
|
|
const r = await passkeyRegister(router.token)
|
|
who = r.admin
|
|
done = true
|
|
} catch (e) {
|
|
error = e?.message || 'enrolment failed'
|
|
} finally {
|
|
busy = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="relative z-10 flex min-h-full items-center justify-center p-6">
|
|
<div class="panel panel-restricted w-full max-w-md p-8 text-center">
|
|
<div class="tag mb-2">passkey enrolment</div>
|
|
<h1 class="mb-4 font-display text-xl text-amber">Register a hardware passkey</h1>
|
|
{#if done}
|
|
<p class="mb-4"><span class="pill pill-ok">enrolled · {who}</span></p>
|
|
<a class="btn" href="#/monitor">continue → sign in</a>
|
|
{:else if !router.token}
|
|
<p class="pill pill-alert">missing enrolment token — use the link from <code>register-admin</code></p>
|
|
{:else if !passkeySupported()}
|
|
<p class="pill pill-alert">this browser has no WebAuthn support</p>
|
|
{:else}
|
|
<p class="mb-6 text-sm text-muted">Insert your YubiKey and tap to bind it to this fleet console.</p>
|
|
<button class="btn btn-restricted w-full py-3" disabled={busy} onclick={enrol}>
|
|
{busy ? 'waiting for passkey…' : '⚿ register passkey'}
|
|
</button>
|
|
{/if}
|
|
{#if error}<p class="mt-4"><span class="pill pill-alert">{error}</span></p>{/if}
|
|
</div>
|
|
</div>
|