Rename maglev-frontend → maglevd-frontend; v0.9.1; API RX/TX pulse

Rename the web dashboard binary to maglevd-frontend and move it to
/usr/sbin (it's a daemon and belongs with maglevd). The systemd unit
name stays vpp-maglev-frontend.service since that prefix is the
package name. Manpage, README, user-guide, and debian packaging all
updated in lockstep; bump to 0.9.1 for the first real release.

All frontend env vars are now prefixed MAGLEV_FRONTEND_ so a single
/etc/default/vpp-maglev can be shared with maglevd without collisions.
Every flag has an env equivalent for Docker use. MAGLEV_FRONTEND_USER
and MAGLEV_FRONTEND_PASSWORD still gate the /admin surface.

VPPInfoPanel now pulses "API: ↑↓" indicators in the zippy title
whenever a vpp-api-send / vpp-api-recv log event arrives on the SSE
stream for the scoped maglevd — 250ms blue flash, re-triggerable,
with the two arrows tightly kerned via negative letter-spacing.
This commit is contained in:
2026-04-13 00:13:47 +02:00
parent 1191b3d994
commit 35643fd774
20 changed files with 380 additions and 83 deletions

View File

@@ -1,4 +1,4 @@
BINARIES := maglevd maglevc maglev-frontend
BINARIES := maglevd maglevc maglevd-frontend
MODULE := git.ipng.ch/ipng/vpp-maglev
PROTO_DIR := proto
PROTO_FILE := $(PROTO_DIR)/maglev.proto
@@ -15,7 +15,7 @@ FRONTEND_WEB_SRC := $(shell find cmd/frontend/web/src -type f 2>/dev/null) \
FRONTEND_WEB_DIST := cmd/frontend/web/dist/index.html
NATIVE_ARCH := $(shell go env GOARCH)
VERSION := 0.1.1
VERSION := 0.9.1
COMMIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -X '$(MODULE)/cmd.version=$(VERSION)' \
@@ -26,7 +26,7 @@ TEST ?= tests/
VPP_API_DIR ?= $(HOME)/src/vpp/build-root/install-vpp_debug-native/vpp/share/vpp/api
.PHONY: all build build-amd64 build-arm64 test proto vpp-binapi lint fixstyle fixstyle-web pkg-deb robot-test clean maglev-frontend-web
.PHONY: all build build-amd64 build-arm64 test proto vpp-binapi lint fixstyle fixstyle-web pkg-deb robot-test clean maglevd-frontend-web
all: build
@@ -34,24 +34,24 @@ build: $(GEN_FILES) $(FRONTEND_WEB_DIST)
mkdir -p build/$(NATIVE_ARCH)
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevd ./cmd/maglevd/
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevc ./cmd/maglevc/
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglev-frontend ./cmd/frontend/
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevd-frontend ./cmd/frontend/
build-amd64: $(GEN_FILES) $(FRONTEND_WEB_DIST)
mkdir -p build/amd64
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevd ./cmd/maglevd/
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevc ./cmd/maglevc/
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglev-frontend ./cmd/frontend/
GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o build/amd64/maglevd-frontend ./cmd/frontend/
build-arm64: $(GEN_FILES) $(FRONTEND_WEB_DIST)
mkdir -p build/arm64
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevd ./cmd/maglevd/
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevc ./cmd/maglevc/
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglev-frontend ./cmd/frontend/
GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o build/arm64/maglevd-frontend ./cmd/frontend/
# maglev-frontend-web rebuilds the SolidJS bundle. The Go binary embeds the
# maglevd-frontend-web rebuilds the SolidJS bundle. The Go binary embeds the
# resulting cmd/frontend/web/dist/ via //go:embed, so a `go build` after
# this target picks up any asset changes automatically.
maglev-frontend-web: $(FRONTEND_WEB_DIST)
maglevd-frontend-web: $(FRONTEND_WEB_DIST)
$(FRONTEND_WEB_DIST): $(FRONTEND_WEB_SRC)
cd cmd/frontend/web && npm install && npm run build

View File

@@ -1,34 +1,57 @@
# maglevd
# vpp-maglev
Health checker and gRPC control plane for VPP Maglev load balancing.
Health checker, gRPC control plane, CLI, and web dashboard for the VPP
`lb` (load-balancer) plugin. Runs as a set of three binaries under one
Debian package:
## Build and Install
- **`maglevd`** — the long-running health-checker daemon. Probes backends
(HTTP, TCP, ICMP), tracks their aggregate state, programs the VPP
dataplane via the `lb` plugin binary API, and exposes everything
over a gRPC API + Prometheus `/metrics` endpoint.
- **`maglevc`** — the interactive CLI client. Tab-completing shell with
inline help; also runs one-shot commands for scripting.
- **`maglevd-frontend`** — optional web dashboard. One binary with the
SolidJS SPA embedded via `//go:embed`; connects to one or more
maglevds over gRPC and serves a live HTTP view (read-only `/view/`
and optional basic-auth `/admin/`).
## Build and install
```sh
make # builds build/<arch>/maglevd and build/<arch>/maglevc
make # builds build/<arch>/{maglevd,maglevc,maglevd-frontend}
make test # runs all tests
make pkg-deb # Creates a debian package for arm64 and amd64
make pkg-deb # creates a Debian package for amd64 and arm64
```
Requires Go 1.25+ and (for `make proto`) `protoc` with `protoc-gen-go` and
`protoc-gen-go-grpc`.
Requires Go 1.25+ and (for `make proto`) `protoc` with `protoc-gen-go`
and `protoc-gen-go-grpc`. The SolidJS bundle under
`cmd/frontend/web/` is built automatically via `make` through the
`maglevd-frontend-web` target, which needs `npm`.
Produces `vpp-maglev_<version>_amd64.deb` and `vpp-maglev_<version>_arm64.deb`
in the `build/` directory by cross-compiling with `GOOS=linux GOARCH=<arch>`.
Requires `dpkg-deb` (available on any Debian/Ubuntu host). The installed
binaries report the exact git commit via `maglevd --version` (and
similarly for `maglevc` / `maglev-frontend`).
Produces `vpp-maglev_<version>_amd64.deb` and
`vpp-maglev_<version>_arm64.deb` in the `build/` directory by
cross-compiling with `GOOS=linux GOARCH=<arch>`. Requires `dpkg-deb`
(available on any Debian/Ubuntu host). The installed binaries report
the exact git commit via `maglevd --version` (and similarly for
`maglevc` / `maglevd-frontend`).
## Running
After installing, the unit is enabled but not started automatically:
After installing, `maglevd` is enabled automatically but
`maglevd-frontend` is **not** — it's opt-in, so the web dashboard
doesn't surprise anyone who just wanted the daemon:
```sh
# edit /etc/vpp-maglev/maglev.yaml, then:
systemctl enable --now vpp-maglev
# optional: web dashboard. Edit /etc/default/vpp-maglev to set
# MAGLEV_FRONTEND_ARGS and (optionally) MAGLEV_FRONTEND_USER /
# MAGLEV_FRONTEND_PASSWORD for /admin/ access, then:
systemctl enable --now vpp-maglev-frontend
```
Or run the server and client by hand:
Or run the components by hand:
```sh
maglevd --config /etc/vpp-maglev/maglev.yaml --grpc-addr :9090
@@ -38,15 +61,29 @@ maglevc --server localhost:9090 # interactive shell
maglevc show frontends # one-shot
maglevc -color=false show backends # one-shot, no ANSI color
maglevc set backend nginx0-ams pause
maglevd-frontend -server localhost:9090 -listen :8080
```
Send `SIGHUP` to `maglevd` to reload config without restarting.
`maglevd` requires `CAP_NET_RAW` for ICMP health checks.
Check out a minimal configuration file in [[debian/maglev.yaml](debian/maglev.yaml)].
See [docs/user-guide.md](docs/user-guide.md) for flags, signals, and `maglevc` usage.
See [docs/config-guide.md](docs/config-guide.md) for the full configuration reference.
See [docs/healthchecks.md](docs/healthchecks.md) for health state machine details.
Every flag on every binary also has an environment-variable
equivalent (e.g. `MAGLEV_CONFIG`, `MAGLEV_GRPC_ADDR`,
`MAGLEV_SERVERS`, `MAGLEV_LISTEN`, `MAGLEV_LOG_LEVEL`) so all three
programs can be driven entirely via env in containerized
deployments.
## Documentation
- A minimal configuration file in
[debian/maglev.yaml](debian/maglev.yaml) shows every knob.
- [docs/user-guide.md](docs/user-guide.md) — flags, signals, and
`maglevc` command reference.
- [docs/config-guide.md](docs/config-guide.md) — full YAML reference.
- [docs/healthchecks.md](docs/healthchecks.md) — health state
machine, probe scheduling, rise/fall semantics.
- Manpages: `maglevd(8)`, `maglevc(1)`, `maglevd-frontend(8)`.
## Docker

View File

@@ -106,7 +106,7 @@ func registerHandlers(mux *http.ServeMux, clients []*maglevClient, broker *Broke
adminAPI := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handleAdminAPI(w, r, byName)
})
realm := "maglev-frontend admin"
realm := "maglevd-frontend admin"
// Register /admin/api/ before /admin/ so the more specific
// pattern wins in net/http's ServeMux.
mux.Handle("/admin/api/", basicAuth(realm, admin.User, admin.Password, adminAPI))

View File

@@ -26,14 +26,18 @@ func main() {
}
func run() error {
// All env vars are prefixed with MAGLEV_FRONTEND_ so a single
// /etc/default/vpp-maglev (or a Docker env file) can be shared
// with maglevd without its MAGLEV_LOG_LEVEL / MAGLEV_GRPC_ADDR /
// etc. leaking into this process's config.
printVersion := flag.Bool("version", false, "print version and exit")
servers := stringFlag("server", "", "MAGLEV_SERVERS", "comma-separated maglevd gRPC addresses (required)")
listen := stringFlag("listen", ":8080", "MAGLEV_LISTEN", "HTTP listen address")
logLevel := stringFlag("log-level", "info", "MAGLEV_LOG_LEVEL", "log verbosity (debug|info|warn|error)")
servers := stringFlag("server", "", "MAGLEV_FRONTEND_SERVERS", "comma-separated maglevd gRPC addresses (required)")
listen := stringFlag("listen", ":8080", "MAGLEV_FRONTEND_LISTEN", "HTTP listen address")
logLevel := stringFlag("log-level", "info", "MAGLEV_FRONTEND_LOG_LEVEL", "log verbosity (debug|info|warn|error)")
flag.Parse()
if *printVersion {
fmt.Printf("maglev-frontend %s (commit %s, built %s)\n",
fmt.Printf("maglevd-frontend %s (commit %s, built %s)\n",
buildinfo.Version(), buildinfo.Commit(), buildinfo.Date())
return nil
}

View File

@@ -25,7 +25,7 @@ type MaglevdInfo struct {
LastError string `json:"last_error,omitempty"`
}
// VersionInfo is the build metadata of this maglev-frontend binary
// VersionInfo is the build metadata of this maglevd-frontend binary
// plus runtime capability flags the SPA needs to know at mount time.
type VersionInfo struct {
Version string `json:"version"`

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>maglev</title>
<script type="module" crossorigin src="/view/assets/index-C-XMkBf5.js"></script>
<link rel="stylesheet" crossorigin href="/view/assets/index-CxDuAfMR.css">
<script type="module" crossorigin src="/view/assets/index-DjixLt11.js"></script>
<link rel="stylesheet" crossorigin href="/view/assets/index-CExoCDXh.css">
</head>
<body>
<div id="root"></div>

View File

@@ -676,6 +676,45 @@
background: var(--state-down);
}
.vpp-io {
display: inline-flex;
align-items: center;
gap: 0;
margin-left: 4px;
font-size: 13px;
font-weight: 600;
line-height: 1;
letter-spacing: -0.15em;
padding-right: 0.15em;
}
.vpp-io-label {
color: var(--fg-muted);
font-weight: 500;
letter-spacing: normal;
margin-right: 4px;
}
.vpp-tx,
.vpp-rx {
display: inline-block;
color: var(--fg-muted);
opacity: 0.25;
transition:
color 120ms ease-out,
opacity 120ms ease-out,
transform 120ms ease-out;
transform: translateY(0);
}
.vpp-tx.lit {
color: var(--accent);
opacity: 1;
transform: translateY(-1px);
}
.vpp-rx.lit {
color: var(--accent);
opacity: 1;
transform: translateY(1px);
}
.kv {
display: grid;
grid-template-columns: max-content 1fr;

View File

@@ -24,7 +24,7 @@ const Overview: Component = () => {
<div class="frontend-list">
<For each={s().frontends}>{(fe) => <FrontendCard snap={s()} frontend={fe} />}</For>
</div>
<VPPInfoPanel info={s().vpp_info} state={s().vpp_state} />
<VPPInfoPanel name={s().maglevd.name} info={s().vpp_info} state={s().vpp_state} />
</>
)}
</Show>

View File

@@ -1,9 +1,11 @@
import { Show, type Component } from "solid-js";
import { Show, createEffect, createSignal, onCleanup, type Component } from "solid-js";
import Zippy from "../components/Zippy";
import Flash from "../components/Flash";
import type { VPPInfoSnapshot } from "../types";
import { events } from "../stores/events";
import type { LogEventPayload, VPPInfoSnapshot } from "../types";
type Props = {
name: string;
info?: VPPInfoSnapshot;
state?: string; // "connected" | "disconnected" | ""
};
@@ -15,6 +17,45 @@ const VPPInfoPanel: Component<Props> = (props) => {
props.info?.connecttime_ns ? new Date(props.info.connecttime_ns / 1e6).toISOString() : "";
const label = () => (props.state === "connected" ? "connected" : "disconnected");
// RX/TX indicators: pulse for 100ms whenever a vpp-api-send / vpp-api-recv
// debug log arrives for this maglevd. The upstream events() signal is
// updated one push at a time, so looking at the newest entry in an effect
// sees every event.
const [tx, setTx] = createSignal(false);
const [rx, setRx] = createSignal(false);
let txTimer: number | undefined;
let rxTimer: number | undefined;
const flashTx = () => {
setTx(true);
if (txTimer) clearTimeout(txTimer);
txTimer = window.setTimeout(() => {
setTx(false);
txTimer = undefined;
}, 250);
};
const flashRx = () => {
setRx(true);
if (rxTimer) clearTimeout(rxTimer);
rxTimer = window.setTimeout(() => {
setRx(false);
rxTimer = undefined;
}, 250);
};
createEffect(() => {
const evs = events();
if (!evs.length) return;
const ev = evs[evs.length - 1];
if (ev.maglevd !== props.name || ev.type !== "log") return;
const msg = (ev.payload as LogEventPayload | undefined)?.msg;
if (!msg) return;
if (msg.startsWith("vpp-api-send")) flashTx();
else if (msg.startsWith("vpp-api-recv")) flashRx();
});
onCleanup(() => {
if (txTimer) clearTimeout(txTimer);
if (rxTimer) clearTimeout(rxTimer);
});
const title = (
<span class="zippy-title">
VPP
@@ -23,6 +64,15 @@ const VPPInfoPanel: Component<Props> = (props) => {
{label()}
</span>
</Flash>
<span class="vpp-io" aria-hidden="true">
<span class="vpp-io-label">API:</span>
<span class="vpp-tx" classList={{ lit: tx() }}>
</span>
<span class="vpp-rx" classList={{ lit: rx() }}>
</span>
</span>
</span>
);

10
debian/build-deb.sh vendored
View File

@@ -4,7 +4,7 @@
#
# The commit hash is baked into the binaries at link time via -ldflags
# in the Makefile, so `maglevd --version` / `maglevc --version` /
# `maglev-frontend --version` are the source of truth for "which
# `maglevd-frontend --version` are the source of truth for "which
# build". The .deb itself carries only the release version.
set -euo pipefail
@@ -28,15 +28,17 @@ install -d "$STAGING/etc/default"
install -d "$STAGING/etc/vpp-maglev"
install -d "$STAGING/DEBIAN"
# Binaries
# Binaries. maglevd and maglevd-frontend are daemons and live under
# /usr/sbin; maglevc is the interactive CLI client and lives under
# /usr/bin so it's on every login shell's PATH.
install -m 755 "$REPO_ROOT/build/${ARCH}/maglevd" "$STAGING/usr/sbin/maglevd"
install -m 755 "$REPO_ROOT/build/${ARCH}/maglevc" "$STAGING/usr/bin/maglevc"
install -m 755 "$REPO_ROOT/build/${ARCH}/maglev-frontend" "$STAGING/usr/bin/maglev-frontend"
install -m 755 "$REPO_ROOT/build/${ARCH}/maglevd-frontend" "$STAGING/usr/sbin/maglevd-frontend"
# Man pages
gzip -9 -c "$REPO_ROOT/docs/maglevd.8" > "$STAGING/usr/share/man/man8/maglevd.8.gz"
gzip -9 -c "$REPO_ROOT/docs/maglevc.1" > "$STAGING/usr/share/man/man1/maglevc.1.gz"
gzip -9 -c "$REPO_ROOT/docs/maglev-frontend.8" > "$STAGING/usr/share/man/man8/maglev-frontend.8.gz"
gzip -9 -c "$REPO_ROOT/docs/maglevd-frontend.8" > "$STAGING/usr/share/man/man8/maglevd-frontend.8.gz"
# Systemd units
install -m 644 "$REPO_ROOT/debian/vpp-maglev.service" "$STAGING/lib/systemd/system/vpp-maglev.service"

2
debian/control.in vendored
View File

@@ -13,7 +13,7 @@ Description: Maglev health-checker daemon, CLI client, and web frontend
maglevc is an interactive CLI client for maglevd with tab completion,
inline help, and one-shot mode for scripting.
.
maglev-frontend is an optional web dashboard that fans one or more
maglevd-frontend is an optional web dashboard that fans one or more
maglevd gRPC streams out to browsers over Server-Sent Events. It is
installed but not enabled by default; enable with:
systemctl enable --now vpp-maglev-frontend

View File

@@ -17,12 +17,21 @@ MAGLEV_CONFIG=/etc/vpp-maglev/maglev.yaml
# Log level: debug, info, warn, error (default: info)
#MAGLEV_LOG_LEVEL=info
# ---- maglev-frontend -------------------------------------------------------
# ---- maglevd-frontend ------------------------------------------------------
# The web dashboard is installed but not enabled by default. Enable with
# systemctl enable --now vpp-maglev-frontend
# after reviewing the arguments below.
# Command-line arguments passed to /usr/bin/maglev-frontend. At minimum
# Command-line arguments passed to /usr/sbin/maglevd-frontend. At minimum
# -server is required (comma-separated list of maglevd gRPC addresses).
# -listen controls the HTTP bind address. See maglev-frontend(8).
# -listen controls the HTTP bind address. See maglevd-frontend(8).
MAGLEV_FRONTEND_ARGS="-server localhost:9090 -listen=:8080"
# Basic-auth credentials for the /admin/ surface. When both are set to
# non-empty values, /admin/ is reachable and the SPA exposes backend
# lifecycle mutations (pause/resume/enable/disable/set-weight). When
# either is missing or empty, /admin/ returns 404 and the SPA hides
# the admin toggle entirely. Leave commented out for a read-only
# deployment.
#MAGLEV_FRONTEND_USER=admin
#MAGLEV_FRONTEND_PASSWORD=changeme

View File

@@ -1,6 +1,6 @@
[Unit]
Description=Maglev web frontend dashboard
Documentation=man:maglev-frontend(8)
Documentation=man:maglevd-frontend(8)
After=network-online.target
Wants=network-online.target
@@ -8,12 +8,13 @@ Wants=network-online.target
User=maglevd
Group=maglevd
EnvironmentFile=/etc/default/vpp-maglev
ExecStart=/usr/bin/maglev-frontend $MAGLEV_FRONTEND_ARGS
ExecStart=/usr/sbin/maglevd-frontend $MAGLEV_FRONTEND_ARGS
Restart=on-failure
RestartSec=5s
Type=simple
# Read-only presentation layer — needs no capabilities.
# Presentation layer — needs no capabilities. /admin/ mutations go
# through the gRPC client to maglevd, which does the privileged work.
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes

View File

@@ -91,6 +91,7 @@ command, with examples and operational notes — see the user guide at:
https://git.ipng.ch/ipng/vpp-maglev/docs/user-guide.md
.RE
.SH SEE ALSO
.BR maglevd (8)
.BR maglevd (8),
.BR maglevd\-frontend (8)
.SH AUTHOR
Pim van Pelt <pim@ipng.ch>

View File

@@ -1,14 +1,14 @@
.TH MAGLEV\-FRONTEND 8 "April 2026" "vpp\-maglev" "System Administration"
.TH MAGLEVD\-FRONTEND 8 "April 2026" "vpp\-maglev" "System Administration"
.SH NAME
maglev\-frontend \- web dashboard for one or more running maglevd instances
maglevd\-frontend \- web dashboard for one or more running maglevd instances
.SH SYNOPSIS
.B maglev\-frontend
.B maglevd\-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
.B maglevd\-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,
@@ -21,7 +21,7 @@ one or more maglevds with
is enough to serve the dashboard.
.PP
For each configured maglevd,
.B maglev\-frontend
.B maglevd\-frontend
maintains:
.IP \(bu 2
A long\-lived
@@ -45,16 +45,36 @@ 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
.B maglevd\-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.
.PP
The frontend exposes two base paths:
.B /view/
is the read\-only dashboard and serves without authentication;
.B /admin/
is a basic\-auth\-protected variant of the same SPA that exposes
lifecycle mutations (pause / resume / enable / disable a backend,
set configured weight within a pool). The admin surface is only
mounted when both
.B MAGLEV_FRONTEND_USER
and
.B MAGLEV_FRONTEND_PASSWORD
are set to non\-empty values at startup; otherwise
.B /admin/
returns 404 and the SPA hides the admin\-toggle button entirely.
.SH OPTIONS
Each flag may also be supplied via an environment variable (shown in
parentheses); the flag takes precedence.
parentheses); the flag takes precedence when both are set. All env
vars are prefixed with
.B MAGLEV_FRONTEND_
so a single env file can be shared with
.BR maglevd (8)
without variables leaking across processes.
.TP
.BI \-server " addr[,addr...]"
Comma\-separated list of maglevd gRPC addresses. Required. Each
@@ -62,11 +82,11 @@ 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 )
.RI "(env: " MAGLEV_FRONTEND_SERVERS )
.TP
.BI \-listen " addr"
HTTP bind address for the dashboard.
.RI "(default: " :8080 "; env: " MAGLEV_LISTEN )
.RI "(default: " :8080 "; env: " MAGLEV_FRONTEND_LISTEN )
.TP
.BI \-log\-level " level"
Structured\-log verbosity:
@@ -76,19 +96,19 @@ Structured\-log verbosity:
or
.BR error .
Affects
.B maglev\-frontend 's
.B maglevd\-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 )
.RI "(default: " info "; env: " MAGLEV_FRONTEND_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).
Static SPA (HTML, JS, CSS, assets). Read\-only.
.TP
.I /view/api/maglevds
JSON array describing the configured maglevds and their current
@@ -101,22 +121,39 @@ Full JSON state snapshot for every maglevd.
Full JSON state snapshot for a single maglevd.
.TP
.I /view/api/version
Build version, commit hash, and build date.
Build version, commit hash, and build date, plus an
.B admin_enabled
flag the SPA uses to decide whether to show the admin toggle.
.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.
replay from a 30\-second / 2000\-event ring buffer, plus a
.B resync
control event emitted after every maglevd config reload so the SPA
re\-hydrates from the now\-fresh server cache.
.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 .
SPA shell served behind basic auth when
.B MAGLEV_FRONTEND_USER
and
.B MAGLEV_FRONTEND_PASSWORD
are configured. Returns 404 when they're not.
.TP
.I "/admin/api/{maglevd}/backend/{name}/{action}"
Backend lifecycle POST. Action is
.BR pause ", " resume ", " enable ", or " disable .
Returns the fresh backend snapshot as JSON.
.TP
.I "/admin/api/{maglevd}/frontend/{fe}/pool/{pool}/backend/{name}/weight"
Weight change POST. Body is
.B {"weight": 0\-100, "flush": bool} .
Returns the fresh frontend snapshot as JSON.
.SH REVERSE PROXY NOTES
The SSE stream has a handful of operational requirements that every
reverse proxy must satisfy:
@@ -124,7 +161,7 @@ reverse proxy must satisfy:
Disable buffering on the events endpoint. Nginx honours
.B X\-Accel\-Buffering: no
(sent by
.BR maglev\-frontend )
.BR maglevd\-frontend )
but a global
.B proxy_buffering off;
in the server block is the more robust answer.
@@ -136,36 +173,72 @@ to at least
so the stream isn't torn down between the 15\-second
.B :\ ping
heartbeats that
.B maglev\-frontend
.B maglevd\-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
All environment variables are prefixed with
.B MAGLEV_FRONTEND_
so this daemon can share
.I /etc/default/vpp-maglev
(or a container env file) with
.BR maglevd (8)
— whose env vars use only the shorter
.B MAGLEV_
prefix — without cross\-contamination.
.TP
.B MAGLEV_SERVERS
.B MAGLEV_FRONTEND_SERVERS
Default value of
.BR \-server .
.TP
.B MAGLEV_LISTEN
.B MAGLEV_FRONTEND_LISTEN
Default value of
.BR \-listen .
.TP
.B MAGLEV_LOG_LEVEL
.B MAGLEV_FRONTEND_LOG_LEVEL
Default value of
.BR \-log\-level .
.TP
.B MAGLEV_FRONTEND_USER
HTTP basic\-auth username for
.BR /admin/ .
When set together with
.B MAGLEV_FRONTEND_PASSWORD
the admin surface is enabled; when either is missing or empty the
admin surface is hidden entirely (the SPA doesn't render the admin
toggle button and
.B /admin/
itself returns 404).
.TP
.B MAGLEV_FRONTEND_PASSWORD
HTTP basic\-auth password for
.BR /admin/ .
See
.B MAGLEV_FRONTEND_USER
above.
.TP
.B MAGLEV_FRONTEND_ARGS
Extra command\-line arguments picked up by the systemd unit's
.B ExecStart
line. Not read directly by the process — the unit expands it before
exec\-ing the binary.
.SH FILES
.TP
.I /etc/default/vpp-maglev
Environment file sourced by the systemd unit before starting
.BR maglev\-frontend .
.BR maglevd\-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 .
.BR maglevd\-frontend ,
and
.B MAGLEV_FRONTEND_USER / MAGLEV_FRONTEND_PASSWORD
are read from the process environment.
.SH SEE ALSO
.BR maglevd (8),
.BR maglevc (1)

View File

@@ -130,6 +130,7 @@ https://git.ipng.ch/ipng/vpp-maglev/docs/user-guide.md
.RE
.SH SEE ALSO
.BR maglevc (1),
.BR maglevd\-frontend (8),
.BR vpp (8)
.SH AUTHOR
Pim van Pelt <pim@ipng.ch>

View File

@@ -241,3 +241,83 @@ keyword. The `?` character is not added to the input line.
Commands and keywords support **prefix matching**: typing `sh ba` is equivalent
to `show backends`, and `sh ba nginx0` is equivalent to `show backends nginx0`.
---
## maglevd-frontend
`maglevd-frontend` is an optional web dashboard that connects to one or
more running `maglevd` instances over gRPC and renders a live view of
frontends, backends, health checks, and VPP load-balancer state. It is
a single Go binary with the SolidJS SPA embedded via `//go:embed`; no
runtime file dependencies.
Installed by the Debian package to `/usr/sbin/maglevd-frontend` but
**not** enabled by default — the operator opts in via:
```sh
systemctl enable --now vpp-maglev-frontend
```
The systemd unit (`vpp-maglev-frontend.service`) reads its arguments
from `/etc/default/vpp-maglev` via `MAGLEV_FRONTEND_ARGS`. The same
env file is shared with `maglevd`; all `maglevd-frontend`-specific
variables are prefixed with `MAGLEV_FRONTEND_` so there's no overlap.
### Flags
| Flag | Environment variable | Default | Description |
|---|---|---|---|
| `--server` | `MAGLEV_FRONTEND_SERVERS` | *(required)* | Comma-separated list of `host:port` maglevd addresses. |
| `--listen` | `MAGLEV_FRONTEND_LISTEN` | `:8080` | HTTP bind address. |
| `--log-level` | `MAGLEV_FRONTEND_LOG_LEVEL` | `info` | Structured-log verbosity for `maglevd-frontend`'s own logs. |
| `--version` | — | — | Print version, commit hash, and build date, then exit. |
In addition to flags, two env-only variables control the admin surface:
| Environment variable | Purpose |
|---|---|
| `MAGLEV_FRONTEND_USER` | HTTP basic-auth username for `/admin/`. |
| `MAGLEV_FRONTEND_PASSWORD` | HTTP basic-auth password for `/admin/`. |
When **both** are set and non-empty the admin surface is mounted and
the SPA's "admin…" toggle becomes visible. When either is missing or
empty the `/admin/` route returns 404 and the SPA hides the toggle —
`/view/` is always reachable read-only.
### HTTP surface
- **`/view/`** — static SPA (dashboard). No authentication.
- **`/view/api/state`**, **`/view/api/state/{name}`** — full JSON
snapshot for every maglevd, or one maglevd.
- **`/view/api/maglevds`** — configured maglevds and connection status.
- **`/view/api/version`** — build info + `admin_enabled` flag.
- **`/view/api/events`** — Server-Sent Events stream; log, backend,
frontend, maglevd-status, vpp-status events with
`Last-Event-ID` replay from a 30-second / 2000-event ring buffer.
- **`/healthz`** — liveness; returns 200 if the HTTP server is up.
- **`/admin/`** — SPA shell behind basic auth (when configured).
- **`POST /admin/api/{maglevd}/backend/{name}/{action}`** — backend
lifecycle action. `action` is `pause`, `resume`, `enable`, or
`disable`. Returns the fresh backend snapshot as JSON.
- **`POST /admin/api/{maglevd}/frontend/{fe}/pool/{pool}/backend/{name}/weight`**
— weight change. Body: `{"weight": 0-100, "flush": bool}`. When
`flush=true`, VPP's flow table for the backend is cleared;
otherwise only the new-buckets map is updated and existing
sessions keep reaching the backend until they finish.
### Reverse-proxy requirements (SSE)
Nginx, HAProxy, or any proxy in front of `maglevd-frontend` must:
- Disable buffering on the events endpoint. `X-Accel-Buffering: no`
is sent by the server; a global `proxy_buffering off;` in the
nginx server block is the more robust answer.
- Raise `proxy_read_timeout` to at least 300s so the stream isn't
torn down between the 15-second `: ping` heartbeats the server
sends.
- Not wrap the events endpoint in any gzip/brotli middleware —
response compression buffers until its window fills and destroys
the live-stream property.
See `maglevd-frontend(8)` for the full reference.