Web UI for Eugene Plexus: a dashboard for the engine processes the supervisor runs, a schema-driven config editor for every component, and a chat playground over the gateway's OpenAI-compatible endpoint.
Current through the M9 contracts (2026-09-11). Runtime dashboard, config editor,
model library and profiles, discovery/downloads with hardware guidance, retained
request metrics, and the playground are wired to the services. The UI generates all
five API documents, including control, and proxies to the control root. /nodes
is the first control-root screen — it mints a join token and renders the
eugene-plexus-agent join command for the machine being added; the workflows past
it are still blocked on whether the agent should own the UI at all.
A browser drives this now. npm run test:e2e runs Playwright against the system
Chrome and a live install, covering first run, login, restart-on-login and the
topology-resolved proxy — the arc jsdom cannot reach, because it has no layout, no
navigation, no real fetch and no cookie jar. It is opt-in like every other
acceptance run; CI still runs vitest. The first-run wizard is split one module per
screen (src/app/setup/), and each screen can be mounted on its own; its screen
list is still an open question. See the
project status and the
M9 acceptance record.
-
Playground (
/) — pick a model fromGET /v1/modelsand talk to it throughPOST /v1/chat/completions. Both are the gateway's OpenAI-compatible surface, unmodified: the playground deliberately uses no private path, because an endpoint only a first-party client can drive is not compatible with anything. A bar under the transcript reports which driver, runtime and backend served the turn, how long it took, and how many backends were tried —attempts > 1is the visible evidence the failover cascade fired. -
Runtimes (
/runtimes) — the engine processes the agent supervises (GET /v1/runtimes) plus which engine adapters found a usable binary on this host (GET /v1/engines). Start / stop / restart per runtime, the resolved context length and slot count read back from the running engine, and the exact argv it was spawned with.loadingis shown distinctly fromstarting, because a large quant off a slow disk sits there for minutes and that is not a fault. -
Library (
/library) - scanned GGUF and safetensors models, model details, named launch profiles, and launch through the agent. -
Discover (
/discover) - with nothing typed, the starter set: one card naming the model this machine should take, with the sentence that says why and a Download button, above the rest of the set. It renders with the hub down, because the endpoint behind it makes no upstream call. Typing searches; pasting a hub link resolves it to one repo and selects it. A repo opens with one suggested version and its own button above All versions, and every fit verdict names the context it was scored at (fits at 32k), with the control that changes it beside them. Model cards, download candidates, quant guidance against the selected node's hardware, and resumable download progress as before. -
Config (
/config) — reads/v1/config/schemafrom the selected component and renders a typed form. Component addresses come from agent topology. Driven by schema metadata, which is the point — a component that adds a knob gets a form field for free. PATCHes the diff back, surfacesapplied/rejected/requiresRestart, and offers a Restart Now modal that polls/healthzuntil the component is back. -
Metrics (
/metrics) — what the gateway kept: per-request and per-attempt rows, latency and throughput by backend, and the tier each request was served from. Operator-only, and deliberately two rows per request rather than one, because a request total includes the attempts that failed. -
Nodes (
/nodes) — every machine in the install, its address, and whether the control root can reach it. Mints a single-use join token and renders the exacteugene-plexus-agent joincommand to paste on the machine being added. The other machine answers on its own terminal, because you cannot reach a worker's web UI until it advertises a non-loopback address, and setting that is part of what joining does. -
First-run wizard (
/setup) — five screens: welcome, passphrase + security mode, model directories, one external backend (optional), summary. It was eight until 2026-09-11; the three that went asked questions nothing answered — deployment topology and gateway address were collected, echoed back on the summary as if they were configuration, and never written by Start, and theme/font only ever touchedlocalStorageand lives on/config. Split one module per screen undersrc/app/setup/, so a screen can be mounted and tested on its own. Auto-saves to sessionStorage; commits to the install on Start as a single transaction — which now includes enrolling this host's own agent with the control root it just spawned. -
Same-origin proxy at
/api/proxy/<target>/<...path>— the browser only talks to the origin it was served from; that origin forwards to the component URL.agentis this host's agent;gateway,libraryandcontrolresolve from the agent's topology by kind, because an install has exactly one of each; anything else is an inference-driver by name. No CORS configuration on the components, no private URLs in the browser.The proxy is not in this repo any more. It moved into the agent (
eugene_plexus_agent/routes/proxy.py) at install-paths §9 step 1, where resolving a target is an in-process lookup rather than an authenticated HTTP call back to the thing asking. Deleting it — the only dynamic route this application had — is what lets the UI ship as a static export inside a Python wheel, which takes Node out of the runtime on every platform.
- Control-root workflows: enrollment, rotation, promotion and topology management need dedicated screens; generated types and a proxy target are not those screens.
- Structured model slots:
model_slotscurrently renders as a JSON editor. - Runtime-name selection: the driver's
runtime_namefield remains free text. - Creating topology entries. The config editor and wizard configure components that already
exist; adding one still means
POST /v1/componentsor editingagent.yaml.
In an install, this repo is not running at all. The agent serves the built assets at its own
root (http://127.0.0.1:8079/ by default) out of the eugene-plexus-ui wheel. That is the path an
operator takes and the path acceptance runs drive.
For development, with hot reload:
npm install
npm run codegen # produces src/generated/*.ts from pinned specs
npm run dev # http://localhost:3000next dev is not the agent, so /api/proxy/... does not exist at that origin; a dev-only
rewrites entry forwards it to the agent. Point it somewhere else with AGENT_URL:
AGENT_URL=http://agent.tailnet:8079 npm run devComponent URLs are not configured here at all — not even the gateway's. The agent resolves every target from its own topology, which is also where the gateway reads it from, so one place records a component's URL and the UI and the router cannot disagree about where it is.
The browser half of an install is a Python distribution: a Next.js static export staged into
python/eugene_plexus_ui/ and wrapped by hatchling, so pip install eugene-plexus-ui into the
agent's interpreter is the whole of "add a browser". The agent finds it through
importlib.resources and mounts it; without it the agent serves the API and says so at /.
npm run build:python # next build, then stage out/ into the package
python -m build --wheel # dist/eugene_plexus_ui-*.whlTwo things that are easy to get wrong and are checked in CI: the export is only produced when
NODE_ENV is production (output: "export" is conditional, because it cannot coexist with the dev
rewrites), and the staged assets are gitignored build output, so hatchling has to be told to
include them — a wheel without index.html installs and imports perfectly and is useless in a way
only a browser notices.
Precedent for the shape: Open WebUI ships its built frontend inside its Python wheel.
TypeScript types are generated from the pinned commit of eugene-plexus/specs recorded in SPECS_REF:
npm run codegenThe script downloads specs at the pinned SHA and runs openapi-typescript for
agent, control, gateway, inference-driver, and library. Generated files
(src/generated/*.ts) are committed; CI re-runs codegen and fails on diff.
When introducing a spec document, update the codegen input list with the pin:
a newer SPECS_REF alone does not cause a new document to be generated.
npm install
npm run lint # ESLint
npm run typecheck # tsc --noEmit
npm run format:check # Prettier
npm run build # Next.js production build
npm run codegen # Regenerate from pinned specsApache 2.0 — see LICENSE.