feat(ui): fleet board — nodes & models as boxes, drag to deploy (UI rebuild, phase 1) - #8
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
/api/deploy/{recipe}/{node}./models, drop targets on/— no page had both. The board unifies them.New backend:
GET /api/nodes(the fleet as JSON, same committed/margin math asplanOnNode);NodeView.LastSnapshotfor freshness;deployNode/undeployFromNodegained awantsHTMLbranch (form → redirect+banner, fetch → JSON); static-asset plumbing (static/*embed, one/static/route, auth exemption so/loginshares the stylesheet).Progressive enhancement: server renders the whole working board;
board.jslayers drag-drop, live fit-preview, and a 4s/api/nodespoll 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/nodesand board tests).🤖 Generated with Claude Code