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

@@ -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>
);