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.
38 lines
963 B
Go
38 lines
963 B
Go
//go:build embedspa
|
|
|
|
// spa_embed.go — serves the Vite-built SPA embedded into the binary. Built only
|
|
// with `-tags embedspa` (after `npm --prefix ui ci && npm --prefix ui run build`),
|
|
// so the default `go build`/`go test` don't require ui/dist (a build artifact).
|
|
// The Dockerfile builds the SPA then compiles with this tag.
|
|
package main
|
|
|
|
import (
|
|
"embed"
|
|
"io/fs"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
//go:embed all:ui/dist
|
|
var spaFiles embed.FS
|
|
|
|
func (s *server) spaHandler() http.Handler {
|
|
sub, err := fs.Sub(spaFiles, "ui/dist")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
files := http.FileServer(http.FS(sub))
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
p := strings.TrimPrefix(r.URL.Path, "/")
|
|
if p == "" {
|
|
p = "index.html"
|
|
}
|
|
// Real asset -> serve it; anything else -> SPA entry (hash routing).
|
|
if _, err := fs.Stat(sub, p); err != nil {
|
|
http.ServeFileFS(w, r, sub, "index.html")
|
|
return
|
|
}
|
|
files.ServeHTTP(w, r)
|
|
})
|
|
}
|