Files
vpp-maglev/cmd/frontend/types.go
Pim van Pelt 25e9d79aba 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.
2026-04-12 20:04:53 +02:00

136 lines
4.6 KiB
Go

// Copyright (c) 2026, Pim van Pelt <pim@ipng.ch>
package main
import "encoding/json"
// StateSnapshot is the full JSON snapshot served for a single maglevd.
type StateSnapshot struct {
Maglevd MaglevdInfo `json:"maglevd"`
Frontends []*FrontendSnapshot `json:"frontends"`
Backends []*BackendSnapshot `json:"backends"`
HealthChecks []*HealthCheckSnapshot `json:"healthchecks"`
VPPInfo *VPPInfoSnapshot `json:"vpp_info,omitempty"`
// VPPState is "connected", "disconnected", or "" (unknown). Updated
// from vpp-connect / vpp-disconnect / vpp-api-{send,recv} log
// events and re-seeded on every refreshAll tick.
VPPState string `json:"vpp_state,omitempty"`
}
// MaglevdInfo is the per-maglevd connection status record.
type MaglevdInfo struct {
Name string `json:"name"`
Address string `json:"address"`
Connected bool `json:"connected"`
LastError string `json:"last_error,omitempty"`
}
// VersionInfo is the build metadata of this maglev-frontend binary
// plus runtime capability flags the SPA needs to know at mount time.
type VersionInfo struct {
Version string `json:"version"`
Commit string `json:"commit"`
Date string `json:"date"`
AdminEnabled bool `json:"admin_enabled"`
}
type FrontendSnapshot struct {
Name string `json:"name"`
Address string `json:"address"`
Protocol string `json:"protocol"`
Port uint32 `json:"port"`
Description string `json:"description,omitempty"`
SrcIPSticky bool `json:"src_ip_sticky"`
Pools []*PoolSnapshot `json:"pools"`
// State is the aggregated frontend state ("up" | "down" | "unknown")
// populated from FrontendEvent messages, including the synthetic
// from==to replay that maglevd sends on WatchEvents subscribe.
State string `json:"state,omitempty"`
}
type PoolSnapshot struct {
Name string `json:"name"`
Backends []*PoolBackendSnapshot `json:"backends"`
}
type PoolBackendSnapshot struct {
Name string `json:"name"`
Weight int32 `json:"weight"`
EffectiveWeight int32 `json:"effective_weight"`
}
type BackendSnapshot struct {
Name string `json:"name"`
Address string `json:"address"`
State string `json:"state"`
Enabled bool `json:"enabled"`
HealthCheck string `json:"healthcheck"`
LastTransition *TransitionRecord `json:"last_transition,omitempty"`
Transitions []*TransitionRecord `json:"transitions,omitempty"`
}
type TransitionRecord struct {
From string `json:"from"`
To string `json:"to"`
AtUnixNs int64 `json:"at_unix_ns"`
}
type HealthCheckSnapshot struct {
Name string `json:"name"`
Type string `json:"type"`
Port uint32 `json:"port"`
IntervalNs int64 `json:"interval_ns"`
FastIntervalNs int64 `json:"fast_interval_ns"`
DownIntervalNs int64 `json:"down_interval_ns"`
TimeoutNs int64 `json:"timeout_ns"`
Rise int32 `json:"rise"`
Fall int32 `json:"fall"`
}
type VPPInfoSnapshot struct {
Version string `json:"version"`
BuildDate string `json:"build_date"`
PID uint32 `json:"pid"`
BoottimeNs int64 `json:"boottime_ns"`
ConnecttimeNs int64 `json:"connecttime_ns"`
}
// BrowserEvent is the wire shape sent over SSE to the browser.
type BrowserEvent struct {
Maglevd string `json:"maglevd"`
Type string `json:"type"` // log|backend|frontend|maglevd-status|resync
AtUnixNs int64 `json:"at_unix_ns"`
Payload json.RawMessage `json:"payload"`
}
// BackendEventPayload is what we ship inside BrowserEvent.Payload for
// type == "backend".
type BackendEventPayload struct {
Backend string `json:"backend"`
Transition TransitionRecord `json:"transition"`
}
type FrontendEventPayload struct {
Frontend string `json:"frontend"`
Transition TransitionRecord `json:"transition"`
}
type LogEventPayload struct {
Level string `json:"level"`
Msg string `json:"msg"`
Attrs map[string]string `json:"attrs,omitempty"`
}
type MaglevdStatusPayload struct {
Connected bool `json:"connected"`
LastError string `json:"last_error,omitempty"`
}
// VPPStatusPayload rides on a "vpp-status" BrowserEvent and tells the
// SPA when the maglevd↔VPP connection flips. Emitted by the frontend's
// log-event handler on vpp-connect / vpp-disconnect, and on the first
// sighting of vpp-api-send/recv (which implies VPP is up).
type VPPStatusPayload struct {
State string `json:"state"` // "connected" | "disconnected"
}