Skip to content

UI architecture

One page, three tabs over one shared graph

Section titled “One page, three tabs over one shared graph”

The UI is a single Svelte app with no client-side router — a tab selector just switches which body renders under a fixed header. All three tabs read from the same resolved graph — they’re different views over one shared model, not three separately fetched datasets:

  • Repos — service nodes and their depends-on edges only: “who requires whom,” matching the repo-centric graph described in Flat workspace model and Fog-of-war visibility.
  • Actual — every node kind, services and backing dependencies together, overlaid with live container status from whichever run is currently selected. This is also where run controls live — starting the default environment, running a specific flow, or starting a second, named review run.
  • Config — reserved for environment variables, the split-DNS table, and issued certs; not built out yet.

A flow selection is a highlight, never a filter, on every tab — see Fog-of-war visibility and Branch ownership model: every known repo always renders, dimmed when it’s outside the selected flow.

The current workspace id is read once from the page’s ?workspace= query param at load (falling back to whatever was last stored in localStorage if that’s absent), then applied to every request automatically, matching the control API’s own workspace-scoping — see Control API. Switching workspaces persists the new id to localStorage (the URL itself isn’t kept in sync afterward) and resets every piece of workspace-scoped state before re-fetching, so stale data from the previous workspace never briefly renders under the new one’s identity.

There’s no websocket for the graph or run list — both are plain interval polling. The graph poll (every few seconds) runs unconditionally regardless of which tab is active — deliberately, so branch/dirty state stays fresh in the background even while looking at another tab (see Branch ownership model) — while the run list poll is gated to the Actual tab, on a faster interval matched to the backend reconciler’s own tick rate (see Run lifecycle & registry) so the UI is essentially never stale relative to what the backend has already reconciled.

Logs are the one exception: the log drawer opens a real live stream whenever it’s showing a running container — genuinely pushed, not polled, since log lines arriving late would be a much more noticeable regression than a graph node being briefly stale.

Both pull jobs and run start/stop follow the same “kick off, then poll a status endpoint” shape client-side — matching the background-job design described in Docker & downloads: the request returns immediately, and a short-interval poll against a status endpoint drives the spinner/checkmark until the job finishes.

The graph view runs a longest-path layered layout, computed fresh from the current node/edge set on every render:

  1. Cycle removal. A real cross-flow dependency cycle is legitimate under this model (see Branch ownership model’s note on flow-scoped vs. hard dependencies pointing in opposite directions) — but a longest-path depth assignment over a graph with a cycle doesn’t terminate. A depth-first pass classifies back-edges (an edge into a node still in progress on the current traversal) and drops only those before the depth pass runs, so the visual layout is always a DAG even when the underlying dependency graph legitimately isn’t one.
  2. Depth assignment. A relaxation pass over the now-acyclic edge set pushes each node’s depth to one more than its deepest parent.
  3. Deterministic ordering. Nodes are sorted by id, not by insertion order, before being bucketed into depth levels — so the layout doesn’t depend on which flow is highlighted or on the raw order the graph happened to be served in. Switching which flow is highlighted never moves a single node, only recolors it — this mirrors the resolver’s own choice to sort nodes by id before serializing the graph, so it doesn’t visibly jitter on every poll for reasons unrelated to any real change.
  • Node detail — general info and logs, opened by clicking any node. Shows the node’s default-run domain and, when live, its container status and an “open” link — see Node identity & domains for exactly which run’s address that link reflects.
  • Operations — lists every download job started this daemon lifetime, most-recent-first, with its live log — the one place to see a “pull all” or an individual clone’s raw Git output.