Skip to content

feat(ui): fleet board — nodes & models as boxes, drag to deploy (UI rebuild, phase 1) - #8

Merged
codemug merged 3 commits into
mainfrom
ui-rebuild
Sep 4, 2026
Merged

feat(ui): fleet board — nodes & models as boxes, drag to deploy (UI rebuild, phase 1)#8
codemug merged 3 commits into
mainfrom
ui-rebuild

Conversation

@codemug

@codemug codemug commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Phase 1 of the from-scratch UI rebuild: the fleet board replaces the landing page.

One idea — memory is a length. A shared GiB ruler; each node drawn to its pool width, each model to its footprint. Dragging a model onto a node deploys it; the same action works with no mouse/JS via each model's "Deploy to…" menu of real per-node forms.

Fixes the three complaints at the root:

  • Deploy reaches a node. Old Deploy posted to the nodeID-less legacy route → the control plane's own GPU-less Docker (dead path). Every affordance here posts /api/deploy/{recipe}/{node}.
  • Drag-and-drop works at all. It never could: drag sources were on /models, drop targets on / — no page had both. The board unifies them.
  • Honest state. Serving models show "on · ", not "not deployed". No fake "ready" on the node path (no health probe exists there yet); disconnected nodes grey out and say so; snapshot age is shown so a frozen catalog is visible.

New backend: GET /api/nodes (the fleet as JSON, same committed/margin math as planOnNode); NodeView.LastSnapshot for freshness; deployNode/undeployFromNode gained a wantsHTML branch (form → redirect+banner, fetch → JSON); static-asset plumbing (static/* embed, one /static/ route, auth exemption so /login shares the stylesheet).

Progressive enhancement: server renders the whole working board; board.js layers drag-drop, live fit-preview, and a 4s /api/nodes poll that tolerates the tailnet's dropped requests.

Deliberately still to come (phase 2, next pass): the other screens (models, model detail, keys with the plain-HTTP copy fix, recipes, admin) restyled to match — they keep working on the old templates meanwhile — and the deeper backend (real readiness probe, non-blocking deploy). The single-node build (cmd/sous) keeps the old node page, where the pool-ruler/stepper/local-deploy rendering genuinely still lives; those tests moved to that config, fleet-view tests were rewritten to the board.

Tests green across 28 packages (+ new /api/nodes and board tests).

🤖 Generated with Claude Code

codemug and others added 3 commits September 4, 2026 17:29
The new drag-and-drop board needs one machine-readable view of the
fleet; today the only node state reaches the browser as html/template
output (node.html), which a JS board would have to scrape. This adds a
single JSON endpoint built from the in-memory nodecatalog with the EXACT
committed/margin arithmetic the server-rendered cards use (status.go's
nodeCards) and planOnNode/capacity.Planner enforce - so the board, any
HTML fallback, and the deploy-time capacity check can never disagree
about whether a model fits.

Per node: node_id, connected, snapshot_age_s (freshness - a node can be
connected yet stale if snapshots stop, a distinct state the board shows),
pool/reserve/committed/margin GiB, deployments[recipe_id, host_port,
docker_status (Docker's RAW word, never dressed as a readiness verdict -
souslet has no health probe yet), weights/kv GiB], cached_weight_repos.
Sorted by node_id, deployments by recipe_id, empty fleet is [] not null.

Also adds NodeView.LastSnapshot (stamped in ReplaceSnapshot, preserved by
MarkDisconnected) to source snapshot_age_s. Registered under the existing
gsrv&&nodes gate so cmd/sous (nil catalog) never exposes it and cannot
nil-panic. +5 tests; 549 pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Widens the go:embed from the single dragdrop.js to static/* and replaces
the per-file route with one GET /static/ prefix route, so the board's
stylesheet and script (and any later asset) ship without a new route
each. Exempts /static/ in auth.Middleware alongside /login so the login
page can link the shared stylesheet before anyone signs in - the assets
are non-secret and the tailnet is the boundary; /static/ is a literal
prefix, not a redirect, so nothing else leaks through it.

Prep for the board rebuild; existing dragdrop.js still serves 200 via the
prefix route (tests green).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replaces the landing page ("/") on a multi-node control plane with a
from-scratch board built on one idea: memory is a length. One shared GiB
ruler runs across the fleet — every node is drawn to the width of its
memory pool and every model to its footprint — so "does it fit" is a
length the eye reads before any request is sent. Dragging a model onto a
node deploys it; the same action is reachable without a mouse (or
JavaScript) via each model's "Deploy to…" menu of real per-node forms.

Fixes the three things that made the old page unusable, at the root:
  - Deploy actually reaches a node. The old Deploy control posted to the
    nodeID-less legacy route, which deploys onto the control plane's own
    Docker (a box with no GPU) — a dead path. Every deploy affordance
    here (drag AND menu) posts /api/deploy/{recipe}/{node}.
  - Drag-and-drop works at all. It never could before: drag sources were
    on /models and drop targets on /, so no page ever had both. The board
    puts models and nodes on one surface.
  - Honest state. Models serving on a node show "on <node> · <status>",
    not "not deployed". The node path has no health probe, so the board
    shows Docker's raw status and never a green "ready" it cannot verify;
    disconnected nodes grey out and say so rather than vanishing; each
    node shows its snapshot age so a frozen catalog is visible.

Progressive enhancement throughout: the server renders the whole board
(working Deploy-to/Stop forms) so it functions with no JS; board.js layers
drag-and-drop, live fit-preview, and a 4s poll of GET /api/nodes that
tolerates the tailnet's dropped requests by keeping the last good state.
deployNode/undeployFromNode gained a wantsHTML branch so a form post
redirects back to the board with a banner while fetch() still gets JSON.

New: internal/ui/static/board.{css,js}, templates/board.html, board.go
(pageBoard + the shared fleetView the board and /api/nodes both read).
The single-node build (cmd/sous, nil gRPC) keeps the old node page as its
landing view — the pool-ruler/stepper/local-deploy rendering genuinely
only applies there now, so those tests moved to that configuration; the
fleet-view tests were rewritten against the board's real markup. Board
still to come (handed to the next pass): the remaining screens (models,
model detail, keys with the plain-HTTP copy fix, recipes, admin) restyled
to match, and the deeper backend (readiness probe, non-blocking deploy).

549 -> tests green across 28 packages.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@codemug
codemug merged commit 4b98467 into main Sep 4, 2026
1 check passed
@codemug
codemug deleted the ui-rebuild branch September 4, 2026 13:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant