Frontend: live clocks, admin mode, backend actions; packaging polish

Builds on the maglev-frontend component introduced in 284b4cc with
quality-of-life improvements, an authenticated /admin surface, a
live-action control plane, and Debian packaging cleanup.

 - Backend state now renders live: maglevd's FrontendEvent synthetic
   from==to replay hydrates FrontendSnapshot.State on WatchEvents
   subscribe, and live transitions update both the in-process cache
   and every connected browser via a new applyFrontendTransition
   reducer. Shown as a StatusBadge next to the frontend name.
 - VPP connection state surfaces in the VPP zippy title as a
   green/red badge. Driven by vpp-connect / vpp-disconnect and by
   the steady stream of vpp-api-send/recv debug heartbeats so a
   silent VPP drop is caught within one debug-log tick.
 - Probe heartbeat dot becomes ❤️ while a probe is in flight and
   reverts to · on probe-done. Fixed-size wrapper so the emoji swap
   doesn't jiggle the row; both states share the same font-size.
 - Flash component replaced its subtle background-only fade with a
   scale-pop + yellow halo box-shadow + longer duration so
   weight/effective/state changes are unmissable on tiny numeric
   cells. Initial mount still skipped via defer so no flash on load.
 - Last-transition age is now a live countdown driven by a global
   1-second ticker signal (one timer, many subscribers). Two most
   significant units: 10m30s / 1h12m / 1d16h. Sub-second ages
   render as "now" to absorb clock skew between maglevd and the
   browser.
 - Event stream is now chronological (oldest at top) with tail-
   style auto-scroll, pause/resume, and the toolbar moved below the
   list. Row separators removed. Also shown only in /admin (see
   below) so /view stays a focused read-only surface.
 - Table nowrap so backend names like nginx0-frggh0 and the
   "last transition" header don't wrap. Frontends render in the
   order returned by ListFrontends instead of Go map iteration
   order so reload doesn't shuffle VIP order.
 - IPng logo in the header, clickable, links to the git repo.
   Header padding reduced so the logo can fill the bar up to the
   separator. Version + commit + build date shown in the brand area
   (fetched once from /view/api/version).
 - "view" / "admin" mode tag moved to sit just left of the admin
   toggle button so it reads as a pair.
 - Prettier wired in as the web-side fixstyle via a new
   fixstyle-web Make target that also runs from `make fixstyle`.
   Added .prettierrc.json and .prettierignore; 8 existing files
   were normalized in place.

 - Fixed a "20555d ago" rendering bug: maglevd's synthetic
   backend-replay events (from==to, at_unix_ns=0) were corrupting
   the local cache's LastTransition via applyBackendTransition.
   Backend synthetic events are now skipped entirely (refreshAll
   covers initial hydration for backends), while frontend synthetic
   events are still applied because FrontendInfo doesn't carry
   state — the event is the only source.

 - New MAGLEV_FRONTEND_USER / MAGLEV_FRONTEND_PASSWORD env vars.
   When both are set and non-empty, /admin/ becomes a basic-auth-
   protected SPA shell backed by the same embedded index.html as
   /view/. The SPA detects its base path via a new stores/mode.ts
   isAdmin constant and conditionally renders admin-only sections
   (currently: the Event Stream / DebugPanel). When disabled,
   /admin/ returns 404 (not 501) so operators who didn't configure
   it see no teasing affordance, and the SPA's admin-toggle button
   is hidden entirely via the admin_enabled flag on
   /view/api/version.
 - basicAuth uses crypto/subtle.ConstantTimeCompare for both user
   and password so timing can't distinguish a wrong username from
   a wrong password.

 - New POST /admin/api/{maglevd}/backend/{name}/{pause|resume|
   enable|disable} endpoint, gated by the same basic-auth
   middleware as the SPA shell. maglevClient.BackendAction wraps
   the four matching gRPC RPCs and returns a fresh BackendSnapshot;
   the same transition lands via WatchEvents so every connected
   browser converges through the normal reducer path.
 - BackendActionsMenu Solid component: kebab (⋮) button in a new
   trailing column rendered only in /admin. Click-outside and
   Escape close the popover (document listeners installed only
   while open). Actions are state-aware: up/down/unknown → pause,
   disable; paused → resume, disable; disabled → enable;
   removed → menu suppressed entirely. Busy indicator per action;
   errors render inline under the item list.
 - Structured audit log: every mutation logs an
   admin-backend-action record with maglevd / backend / action /
   resulting state.

 - Renamed debian/vpp-maglevd.service → debian/vpp-maglev.service
   to align naming with the new vpp-maglev-frontend.service
   sibling. postinst handles upgrades by stopping + disabling any
   lingering vpp-maglevd.service before enabling the renamed unit;
   prerm stops both (the frontend unit is installed but not
   enabled by default — operators opt in with systemctl enable).
 - New debian/vpp-maglev-frontend.service (hardened:
   NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp,
   no capabilities). Reads the same /etc/default/vpp-maglev
   conffile and expands MAGLEV_FRONTEND_ARGS via
   `ExecStart=/usr/bin/maglev-frontend $MAGLEV_FRONTEND_ARGS` so
   word-splitting works.
 - docs/maglev-frontend.8 manpage documenting flags, endpoints,
   and SSE reverse-proxy requirements.
 - build-deb.sh: drops the commit hash from the .deb filename
   (now vpp-maglev_<version>_<arch>.deb) and no longer takes the
   commit as a CLI arg. Binaries continue to carry the commit via
   -ldflags so `maglevd --version` et al are the authoritative
   "which build is running" answer.
This commit is contained in:
2026-04-12 20:04:45 +02:00
parent 284b4cc9a4
commit 25e9d79aba
37 changed files with 1030 additions and 107 deletions

178
docs/maglev-frontend.8 Normal file
View File

@@ -0,0 +1,178 @@
.TH MAGLEV\-FRONTEND 8 "April 2026" "vpp\-maglev" "System Administration"
.SH NAME
maglev\-frontend \- web dashboard for one or more running maglevd instances
.SH SYNOPSIS
.B maglev\-frontend
\fB\-server\fR \fIaddr\fR[,\fIaddr\fR...]
[\fB\-listen\fR \fIaddr\fR]
[\fB\-log\-level\fR \fIlevel\fR]
[\fB\-version\fR]
.SH DESCRIPTION
.B maglev\-frontend
is a single\-binary web dashboard that connects to one or more running
.BR maglevd (8)
instances over gRPC and renders a live view of frontends, backends,
health checks, and VPP load\-balancer state. The SolidJS SPA is
embedded into the Go binary via
.BR embed.FS ,
so no runtime file dependencies are required; pointing the binary at
one or more maglevds with
.B \-server
is enough to serve the dashboard.
.PP
For each configured maglevd,
.B maglev\-frontend
maintains:
.IP \(bu 2
A long\-lived
.B WatchEvents
gRPC stream subscribed at
.BR log_level=debug ,
which delivers backend transitions, frontend transitions, per\-probe
log records (used to drive the live probe heartbeat), and per\-mutation
VPP LB sync records so the UI reflects every dataplane change in real
time.
.IP \(bu 2
A 30\-second refresh loop that re\-fetches
.BR ListFrontends / GetFrontend ,
.BR ListBackends / GetBackend ,
.BR ListHealthChecks / GetHealthCheck ,
and
.B GetVPPInfo
as a safety net against missed events.
.IP \(bu 2
A 5\-second health probe that surfaces maglevd connection drops
quickly and flips the scope\-selector indicator dot red.
.PP
Browsers connect to
.B maglev\-frontend
over HTTP. State is hydrated once via REST and then kept live via a
Server\-Sent Events stream. Short SSE disconnects (nginx idle timeout,
wifi flap, laptop wake) are handled silently via a 30\-second replay
ring buffer; longer outages fall through to a full refetch. The SPA
is stateless on reload so refreshing the page at any time returns a
consistent view.
.SH OPTIONS
Each flag may also be supplied via an environment variable (shown in
parentheses); the flag takes precedence.
.TP
.BI \-server " addr[,addr...]"
Comma\-separated list of maglevd gRPC addresses. Required. Each
entry is in
.I host:port
form; a short display name is derived from the hostname label (for
IP literals the full address is used).
.RI "(env: " MAGLEV_SERVERS )
.TP
.BI \-listen " addr"
HTTP bind address for the dashboard.
.RI "(default: " :8080 "; env: " MAGLEV_LISTEN )
.TP
.BI \-log\-level " level"
Structured\-log verbosity:
.BR debug ,
.BR info ,
.BR warn ,
or
.BR error .
Affects
.B maglev\-frontend 's
own logs, not the log level it subscribes to on the upstream maglevd
(which is always
.BR debug
so the probe heartbeat can animate).
.RI "(default: " info "; env: " MAGLEV_LOG_LEVEL )
.TP
.B \-version
Print version, commit hash, and build date, then exit.
.SH HTTP ENDPOINTS
.TP
.I /view/
Static SPA (HTML, JS, CSS, assets).
.TP
.I /view/api/maglevds
JSON array describing the configured maglevds and their current
connection status.
.TP
.I /view/api/state
Full JSON state snapshot for every maglevd.
.TP
.I /view/api/state/{name}
Full JSON state snapshot for a single maglevd.
.TP
.I /view/api/version
Build version, commit hash, and build date.
.TP
.I /view/api/events
Server\-Sent Events stream. Long\-lived HTTP/1.1 chunked response
fanning out log, backend, frontend, maglevd\-status, and vpp\-status
events to every connected browser. Supports
.B Last\-Event\-ID
replay from a 30\-second / 2000\-event ring buffer.
.TP
.I /healthz
Liveness endpoint; returns 200 if the HTTP server is up.
.TP
.I /admin/
Placeholder for a future basic\-auth mutation surface. Currently
returns
.B 501 Not Implemented .
.SH REVERSE PROXY NOTES
The SSE stream has a handful of operational requirements that every
reverse proxy must satisfy:
.IP \(bu 2
Disable buffering on the events endpoint. Nginx honours
.B X\-Accel\-Buffering: no
(sent by
.BR maglev\-frontend )
but a global
.B proxy_buffering off;
in the server block is the more robust answer.
.IP \(bu 2
Raise
.B proxy_read_timeout
to at least
.BR 300s
so the stream isn't torn down between the 15\-second
.B :\ ping
heartbeats that
.B maglev\-frontend
sends.
.IP \(bu 2
Do not wrap the events endpoint in a gzip/brotli middleware — response
compression buffers until its window fills and destroys the live\-stream
property.
.SH ENVIRONMENT
.TP
.B MAGLEV_SERVERS
Default value of
.BR \-server .
.TP
.B MAGLEV_LISTEN
Default value of
.BR \-listen .
.TP
.B MAGLEV_LOG_LEVEL
Default value of
.BR \-log\-level .
.SH FILES
.TP
.I /etc/default/vpp-maglev
Environment file sourced by the systemd unit before starting
.BR maglev\-frontend .
The same file is shared with
.BR maglevd (8);
the
.B MAGLEV_FRONTEND_ARGS
variable there is passed on the command line to
.B maglev\-frontend .
.SH SEE ALSO
.BR maglevd (8),
.BR maglevc (1)
.SH "FULL DOCUMENTATION"
.PP
.RS
https://git.ipng.ch/ipng/vpp-maglev/docs/user-guide.md
.RE
.SH AUTHOR
Pim van Pelt <pim@ipng.ch>