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 MODULE := git.ipng.ch/ipng/vpp-maglev
PROTO_DIR := proto PROTO_DIR := proto
PROTO_FILE := $(PROTO_DIR)/maglev.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 FRONTEND_WEB_DIST := cmd/frontend/web/dist/index.html
NATIVE_ARCH := $(shell go env GOARCH) 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) COMMIT_HASH := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
LDFLAGS := -X '$(MODULE)/cmd.version=$(VERSION)' \ 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 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 all: build
@@ -34,24 +34,24 @@ build: $(GEN_FILES) $(FRONTEND_WEB_DIST)
mkdir -p build/$(NATIVE_ARCH) mkdir -p build/$(NATIVE_ARCH)
go build -ldflags "$(LDFLAGS)" -o build/$(NATIVE_ARCH)/maglevd ./cmd/maglevd/ 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)/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) build-amd64: $(GEN_FILES) $(FRONTEND_WEB_DIST)
mkdir -p build/amd64 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/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/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) build-arm64: $(GEN_FILES) $(FRONTEND_WEB_DIST)
mkdir -p build/arm64 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/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/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 # resulting cmd/frontend/web/dist/ via //go:embed, so a `go build` after
# this target picks up any asset changes automatically. # 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) $(FRONTEND_WEB_DIST): $(FRONTEND_WEB_SRC)
cd cmd/frontend/web && npm install && npm run build cd cmd/frontend/web && npm install && npm run build

View File

@@ -1,52 +1,89 @@
# 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 ```sh
make # builds build/<arch>/maglevd and build/<arch>/maglevc make # builds build/<arch>/{maglevd,maglevc,maglevd-frontend}
make test # runs all tests 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 Requires Go 1.25+ and (for `make proto`) `protoc` with `protoc-gen-go`
`protoc-gen-go-grpc`. 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` Produces `vpp-maglev_<version>_amd64.deb` and
in the `build/` directory by cross-compiling with `GOOS=linux GOARCH=<arch>`. `vpp-maglev_<version>_arm64.deb` in the `build/` directory by
Requires `dpkg-deb` (available on any Debian/Ubuntu host). The installed cross-compiling with `GOOS=linux GOARCH=<arch>`. Requires `dpkg-deb`
binaries report the exact git commit via `maglevd --version` (and (available on any Debian/Ubuntu host). The installed binaries report
similarly for `maglevc` / `maglev-frontend`). the exact git commit via `maglevd --version` (and similarly for
`maglevc` / `maglevd-frontend`).
## Running ## 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 ```sh
# edit /etc/vpp-maglev/maglev.yaml, then: # edit /etc/vpp-maglev/maglev.yaml, then:
systemctl enable --now vpp-maglev 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 ```sh
maglevd --config /etc/vpp-maglev/maglev.yaml --grpc-addr :9090 maglevd --config /etc/vpp-maglev/maglev.yaml --grpc-addr :9090
maglevd --version # print version and exit maglevd --version # print version and exit
maglevc --server localhost:9090 # interactive shell maglevc --server localhost:9090 # interactive shell
maglevc show frontends # one-shot maglevc show frontends # one-shot
maglevc -color=false show backends # one-shot, no ANSI color maglevc -color=false show backends # one-shot, no ANSI color
maglevc set backend nginx0-ams pause maglevc set backend nginx0-ams pause
maglevd-frontend -server localhost:9090 -listen :8080
``` ```
Send `SIGHUP` to `maglevd` to reload config without restarting. Send `SIGHUP` to `maglevd` to reload config without restarting.
`maglevd` requires `CAP_NET_RAW` for ICMP health checks. `maglevd` requires `CAP_NET_RAW` for ICMP health checks.
Check out a minimal configuration file in [[debian/maglev.yaml](debian/maglev.yaml)]. Every flag on every binary also has an environment-variable
See [docs/user-guide.md](docs/user-guide.md) for flags, signals, and `maglevc` usage. equivalent (e.g. `MAGLEV_CONFIG`, `MAGLEV_GRPC_ADDR`,
See [docs/config-guide.md](docs/config-guide.md) for the full configuration reference. `MAGLEV_SERVERS`, `MAGLEV_LISTEN`, `MAGLEV_LOG_LEVEL`) so all three
See [docs/healthchecks.md](docs/healthchecks.md) for health state machine details. 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 ## 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) { adminAPI := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handleAdminAPI(w, r, byName) handleAdminAPI(w, r, byName)
}) })
realm := "maglev-frontend admin" realm := "maglevd-frontend admin"
// Register /admin/api/ before /admin/ so the more specific // Register /admin/api/ before /admin/ so the more specific
// pattern wins in net/http's ServeMux. // pattern wins in net/http's ServeMux.
mux.Handle("/admin/api/", basicAuth(realm, admin.User, admin.Password, adminAPI)) mux.Handle("/admin/api/", basicAuth(realm, admin.User, admin.Password, adminAPI))

View File

@@ -26,14 +26,18 @@ func main() {
} }
func run() error { 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") printVersion := flag.Bool("version", false, "print version and exit")
servers := stringFlag("server", "", "MAGLEV_SERVERS", "comma-separated maglevd gRPC addresses (required)") servers := stringFlag("server", "", "MAGLEV_FRONTEND_SERVERS", "comma-separated maglevd gRPC addresses (required)")
listen := stringFlag("listen", ":8080", "MAGLEV_LISTEN", "HTTP listen address") listen := stringFlag("listen", ":8080", "MAGLEV_FRONTEND_LISTEN", "HTTP listen address")
logLevel := stringFlag("log-level", "info", "MAGLEV_LOG_LEVEL", "log verbosity (debug|info|warn|error)") logLevel := stringFlag("log-level", "info", "MAGLEV_FRONTEND_LOG_LEVEL", "log verbosity (debug|info|warn|error)")
flag.Parse() flag.Parse()
if *printVersion { 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()) buildinfo.Version(), buildinfo.Commit(), buildinfo.Date())
return nil return nil
} }

View File

@@ -25,7 +25,7 @@ type MaglevdInfo struct {
LastError string `json:"last_error,omitempty"` 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. // plus runtime capability flags the SPA needs to know at mount time.
type VersionInfo struct { type VersionInfo struct {
Version string `json:"version"` 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 charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>maglev</title> <title>maglev</title>
<script type="module" crossorigin src="/view/assets/index-C-XMkBf5.js"></script> <script type="module" crossorigin src="/view/assets/index-DjixLt11.js"></script>
<link rel="stylesheet" crossorigin href="/view/assets/index-CxDuAfMR.css"> <link rel="stylesheet" crossorigin href="/view/assets/index-CExoCDXh.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -676,6 +676,45 @@
background: var(--state-down); 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 { .kv {
display: grid; display: grid;
grid-template-columns: max-content 1fr; grid-template-columns: max-content 1fr;

View File

@@ -24,7 +24,7 @@ const Overview: Component = () => {
<div class="frontend-list"> <div class="frontend-list">
<For each={s().frontends}>{(fe) => <FrontendCard snap={s()} frontend={fe} />}</For> <For each={s().frontends}>{(fe) => <FrontendCard snap={s()} frontend={fe} />}</For>
</div> </div>
<VPPInfoPanel info={s().vpp_info} state={s().vpp_state} /> <VPPInfoPanel name={s().maglevd.name} info={s().vpp_info} state={s().vpp_state} />
</> </>
)} )}
</Show> </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 Zippy from "../components/Zippy";
import Flash from "../components/Flash"; import Flash from "../components/Flash";
import type { VPPInfoSnapshot } from "../types"; import { events } from "../stores/events";
import type { LogEventPayload, VPPInfoSnapshot } from "../types";
type Props = { type Props = {
name: string;
info?: VPPInfoSnapshot; info?: VPPInfoSnapshot;
state?: string; // "connected" | "disconnected" | "" 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() : ""; props.info?.connecttime_ns ? new Date(props.info.connecttime_ns / 1e6).toISOString() : "";
const label = () => (props.state === "connected" ? "connected" : "disconnected"); 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 = ( const title = (
<span class="zippy-title"> <span class="zippy-title">
VPP VPP
@@ -23,6 +64,15 @@ const VPPInfoPanel: Component<Props> = (props) => {
{label()} {label()}
</span> </span>
</Flash> </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> </span>
); );

18
debian/build-deb.sh vendored
View File

@@ -4,7 +4,7 @@
# #
# The commit hash is baked into the binaries at link time via -ldflags # The commit hash is baked into the binaries at link time via -ldflags
# in the Makefile, so `maglevd --version` / `maglevc --version` / # 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. # build". The .deb itself carries only the release version.
set -euo pipefail set -euo pipefail
@@ -28,15 +28,17 @@ install -d "$STAGING/etc/default"
install -d "$STAGING/etc/vpp-maglev" install -d "$STAGING/etc/vpp-maglev"
install -d "$STAGING/DEBIAN" install -d "$STAGING/DEBIAN"
# Binaries # Binaries. maglevd and maglevd-frontend are daemons and live under
install -m 755 "$REPO_ROOT/build/${ARCH}/maglevd" "$STAGING/usr/sbin/maglevd" # /usr/sbin; maglevc is the interactive CLI client and lives under
install -m 755 "$REPO_ROOT/build/${ARCH}/maglevc" "$STAGING/usr/bin/maglevc" # /usr/bin so it's on every login shell's PATH.
install -m 755 "$REPO_ROOT/build/${ARCH}/maglev-frontend" "$STAGING/usr/bin/maglev-frontend" 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}/maglevd-frontend" "$STAGING/usr/sbin/maglevd-frontend"
# Man pages # Man pages
gzip -9 -c "$REPO_ROOT/docs/maglevd.8" > "$STAGING/usr/share/man/man8/maglevd.8.gz" 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/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 # Systemd units
install -m 644 "$REPO_ROOT/debian/vpp-maglev.service" "$STAGING/lib/systemd/system/vpp-maglev.service" 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, maglevc is an interactive CLI client for maglevd with tab completion,
inline help, and one-shot mode for scripting. 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 maglevd gRPC streams out to browsers over Server-Sent Events. It is
installed but not enabled by default; enable with: installed but not enabled by default; enable with:
systemctl enable --now vpp-maglev-frontend 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) # Log level: debug, info, warn, error (default: info)
#MAGLEV_LOG_LEVEL=info #MAGLEV_LOG_LEVEL=info
# ---- maglev-frontend ------------------------------------------------------- # ---- maglevd-frontend ------------------------------------------------------
# The web dashboard is installed but not enabled by default. Enable with # The web dashboard is installed but not enabled by default. Enable with
# systemctl enable --now vpp-maglev-frontend # systemctl enable --now vpp-maglev-frontend
# after reviewing the arguments below. # 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). # -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" 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] [Unit]
Description=Maglev web frontend dashboard Description=Maglev web frontend dashboard
Documentation=man:maglev-frontend(8) Documentation=man:maglevd-frontend(8)
After=network-online.target After=network-online.target
Wants=network-online.target Wants=network-online.target
@@ -8,12 +8,13 @@ Wants=network-online.target
User=maglevd User=maglevd
Group=maglevd Group=maglevd
EnvironmentFile=/etc/default/vpp-maglev EnvironmentFile=/etc/default/vpp-maglev
ExecStart=/usr/bin/maglev-frontend $MAGLEV_FRONTEND_ARGS ExecStart=/usr/sbin/maglevd-frontend $MAGLEV_FRONTEND_ARGS
Restart=on-failure Restart=on-failure
RestartSec=5s RestartSec=5s
Type=simple 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 NoNewPrivileges=yes
ProtectSystem=strict ProtectSystem=strict
ProtectHome=yes 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 https://git.ipng.ch/ipng/vpp-maglev/docs/user-guide.md
.RE .RE
.SH SEE ALSO .SH SEE ALSO
.BR maglevd (8) .BR maglevd (8),
.BR maglevd\-frontend (8)
.SH AUTHOR .SH AUTHOR
Pim van Pelt <pim@ipng.ch> 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 .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 .SH SYNOPSIS
.B maglev\-frontend .B maglevd\-frontend
\fB\-server\fR \fIaddr\fR[,\fIaddr\fR...] \fB\-server\fR \fIaddr\fR[,\fIaddr\fR...]
[\fB\-listen\fR \fIaddr\fR] [\fB\-listen\fR \fIaddr\fR]
[\fB\-log\-level\fR \fIlevel\fR] [\fB\-log\-level\fR \fIlevel\fR]
[\fB\-version\fR] [\fB\-version\fR]
.SH DESCRIPTION .SH DESCRIPTION
.B maglev\-frontend .B maglevd\-frontend
is a single\-binary web dashboard that connects to one or more running is a single\-binary web dashboard that connects to one or more running
.BR maglevd (8) .BR maglevd (8)
instances over gRPC and renders a live view of frontends, backends, 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. is enough to serve the dashboard.
.PP .PP
For each configured maglevd, For each configured maglevd,
.B maglev\-frontend .B maglevd\-frontend
maintains: maintains:
.IP \(bu 2 .IP \(bu 2
A long\-lived 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. quickly and flips the scope\-selector indicator dot red.
.PP .PP
Browsers connect to Browsers connect to
.B maglev\-frontend .B maglevd\-frontend
over HTTP. State is hydrated once via REST and then kept live via a over HTTP. State is hydrated once via REST and then kept live via a
Server\-Sent Events stream. Short SSE disconnects (nginx idle timeout, Server\-Sent Events stream. Short SSE disconnects (nginx idle timeout,
wifi flap, laptop wake) are handled silently via a 30\-second replay wifi flap, laptop wake) are handled silently via a 30\-second replay
ring buffer; longer outages fall through to a full refetch. The SPA 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 is stateless on reload so refreshing the page at any time returns a
consistent view. 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 .SH OPTIONS
Each flag may also be supplied via an environment variable (shown in 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 .TP
.BI \-server " addr[,addr...]" .BI \-server " addr[,addr...]"
Comma\-separated list of maglevd gRPC addresses. Required. Each Comma\-separated list of maglevd gRPC addresses. Required. Each
@@ -62,11 +82,11 @@ entry is in
.I host:port .I host:port
form; a short display name is derived from the hostname label (for form; a short display name is derived from the hostname label (for
IP literals the full address is used). IP literals the full address is used).
.RI "(env: " MAGLEV_SERVERS ) .RI "(env: " MAGLEV_FRONTEND_SERVERS )
.TP .TP
.BI \-listen " addr" .BI \-listen " addr"
HTTP bind address for the dashboard. HTTP bind address for the dashboard.
.RI "(default: " :8080 "; env: " MAGLEV_LISTEN ) .RI "(default: " :8080 "; env: " MAGLEV_FRONTEND_LISTEN )
.TP .TP
.BI \-log\-level " level" .BI \-log\-level " level"
Structured\-log verbosity: Structured\-log verbosity:
@@ -76,19 +96,19 @@ Structured\-log verbosity:
or or
.BR error . .BR error .
Affects Affects
.B maglev\-frontend 's .B maglevd\-frontend 's
own logs, not the log level it subscribes to on the upstream maglevd own logs, not the log level it subscribes to on the upstream maglevd
(which is always (which is always
.BR debug .BR debug
so the probe heartbeat can animate). so the probe heartbeat can animate).
.RI "(default: " info "; env: " MAGLEV_LOG_LEVEL ) .RI "(default: " info "; env: " MAGLEV_FRONTEND_LOG_LEVEL )
.TP .TP
.B \-version .B \-version
Print version, commit hash, and build date, then exit. Print version, commit hash, and build date, then exit.
.SH HTTP ENDPOINTS .SH HTTP ENDPOINTS
.TP .TP
.I /view/ .I /view/
Static SPA (HTML, JS, CSS, assets). Static SPA (HTML, JS, CSS, assets). Read\-only.
.TP .TP
.I /view/api/maglevds .I /view/api/maglevds
JSON array describing the configured maglevds and their current 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. Full JSON state snapshot for a single maglevd.
.TP .TP
.I /view/api/version .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 .TP
.I /view/api/events .I /view/api/events
Server\-Sent Events stream. Long\-lived HTTP/1.1 chunked response Server\-Sent Events stream. Long\-lived HTTP/1.1 chunked response
fanning out log, backend, frontend, maglevd\-status, and vpp\-status fanning out log, backend, frontend, maglevd\-status, and vpp\-status
events to every connected browser. Supports events to every connected browser. Supports
.B Last\-Event\-ID .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 .TP
.I /healthz .I /healthz
Liveness endpoint; returns 200 if the HTTP server is up. Liveness endpoint; returns 200 if the HTTP server is up.
.TP .TP
.I /admin/ .I /admin/
Placeholder for a future basic\-auth mutation surface. Currently SPA shell served behind basic auth when
returns .B MAGLEV_FRONTEND_USER
.B 501 Not Implemented . 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 .SH REVERSE PROXY NOTES
The SSE stream has a handful of operational requirements that every The SSE stream has a handful of operational requirements that every
reverse proxy must satisfy: reverse proxy must satisfy:
@@ -124,7 +161,7 @@ reverse proxy must satisfy:
Disable buffering on the events endpoint. Nginx honours Disable buffering on the events endpoint. Nginx honours
.B X\-Accel\-Buffering: no .B X\-Accel\-Buffering: no
(sent by (sent by
.BR maglev\-frontend ) .BR maglevd\-frontend )
but a global but a global
.B proxy_buffering off; .B proxy_buffering off;
in the server block is the more robust answer. 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 so the stream isn't torn down between the 15\-second
.B :\ ping .B :\ ping
heartbeats that heartbeats that
.B maglev\-frontend .B maglevd\-frontend
sends. sends.
.IP \(bu 2 .IP \(bu 2
Do not wrap the events endpoint in a gzip/brotli middleware — response Do not wrap the events endpoint in a gzip/brotli middleware — response
compression buffers until its window fills and destroys the live\-stream compression buffers until its window fills and destroys the live\-stream
property. property.
.SH ENVIRONMENT .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 .TP
.B MAGLEV_SERVERS .B MAGLEV_FRONTEND_SERVERS
Default value of Default value of
.BR \-server . .BR \-server .
.TP .TP
.B MAGLEV_LISTEN .B MAGLEV_FRONTEND_LISTEN
Default value of Default value of
.BR \-listen . .BR \-listen .
.TP .TP
.B MAGLEV_LOG_LEVEL .B MAGLEV_FRONTEND_LOG_LEVEL
Default value of Default value of
.BR \-log\-level . .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 .SH FILES
.TP .TP
.I /etc/default/vpp-maglev .I /etc/default/vpp-maglev
Environment file sourced by the systemd unit before starting Environment file sourced by the systemd unit before starting
.BR maglev\-frontend . .BR maglevd\-frontend .
The same file is shared with The same file is shared with
.BR maglevd (8); .BR maglevd (8);
the the
.B MAGLEV_FRONTEND_ARGS .B MAGLEV_FRONTEND_ARGS
variable there is passed on the command line to 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 .SH SEE ALSO
.BR maglevd (8), .BR maglevd (8),
.BR maglevc (1) .BR maglevc (1)

View File

@@ -130,6 +130,7 @@ https://git.ipng.ch/ipng/vpp-maglev/docs/user-guide.md
.RE .RE
.SH SEE ALSO .SH SEE ALSO
.BR maglevc (1), .BR maglevc (1),
.BR maglevd\-frontend (8),
.BR vpp (8) .BR vpp (8)
.SH AUTHOR .SH AUTHOR
Pim van Pelt <pim@ipng.ch> 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 Commands and keywords support **prefix matching**: typing `sh ba` is equivalent
to `show backends`, and `sh ba nginx0` is equivalent to `show backends nginx0`. 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.