← the Atlas

odu-web — the service face of odu

feature · seedling ·proposed ·

The browser face of odu is not a tab watching one live run — it is the service layer above the runner: a run ledger that outlives coordinators, the page a commit status Details link points at, forge triggers, and a multi-repo fleet dashboard. That territory belongs to juspay/vira today, so this is also the plan for whether odu-web replaces it. Proposed in five phases, with UI prototypes.

The question that produced this note: now that odu has a TUI and an MCP face, what would the web app look like — and should it replace juspay/vira? The answer reframes the question: the web app worth building is not a third rendering of the attach surface. It is a new program above the runner — ledger, triggers, fleet — and the browser is merely how you look at it. proposed

Status: proposed · maturity seedling · the runner this builds on is odu (Phase 1 + MCP face shipped) · the app shell is surface-app (kolu and drishti are its two consumers; odu-web would be the third)

forge — github.comodu-web — the new program (the service)odu runner — unchanged, one per repo × SHA runpush / PR eventscommit status · target_urltriggers — webhook / pollrun ledger — outlives runnersbrowser UI — surface-app PWAread-observer vs mutator gatenodes · nodeLog · rerun — typed surface events in (Phase 3)spawns + attaches as a clientverdicts + logs → ledgerposts status — target_url → the run page (Phase 1)
odu-web is a layer, not a face. The runner (bottom) is unchanged — odu-web is one more client of its typed surface, plus three things no runner client can be: a trigger ingester, a run ledger, and a browser server. The forge loop closes left to right: events in, statuses out, target_url back to the run page.

Two products hide in “the web app”

The first is the attach face: a browser tab rendering one live run — the TUI with rounder corners. The odu note already judged this one correctly, and the judgement stands: nobody opens a tab to watch a run they kicked off from their shell, the TUI and MCP faces serve the single-operator case better, and a hosted page is the one client that drags in the authz boundary the other faces dodge. Built alone, the attach face is cost with no constituency.

The second is the service face, and it has three constituencies the runner cannot reach today:

Every one of those is a property of a layer above the runner — persistence, triggers, identity across runs — not of a fourth rendering of nodes · nodeLog · rerun. That is the product. The attach face then falls out of it for free (Phase 2 below), instead of the other way around.

The wedge — be the target_url

The smallest shippable slice of the service face is also the highest-value one: the run page, the URL odu’s commit statuses start carrying as target_url. It is read-only, history-backed, and needs neither triggers nor live attach — only a run ledger: today’s per-SHA on-disk log layout (which already survives runner death, by design) formalized into a queryable record of runs, verdicts, durations, and logs that odu-web ingests as runs complete.

odu.srid.ca/juspay/kolu/runs/26d2c2d
juspay/kolu26d2c2dfix(surface): guard stdio write streams…✗ failed · 1 of 8
↳ you arrived from GitHub — commit status ci::e2e@x86_64-linux · Details
x86_64-linuxaarch64-darwinnix✓ 3m40✓ 4m02e2e✗ 2m12✓ 2m44unit✓ 0m31✓ 0m44lint✓ 0m12✓ 0m14
x86_64-linux · e2e  ✗ failed · exit 1⟳ rerun — operators only
cucumber · 14 scenarios ✓ open a terminal … 2.1s ✗ reconnect after server restart expected pane to repaint within 5s at features/reconnect.feature:31

Three properties make this the wedge and not just the first feature:

The service — a coordinator of coordinators

Everything past the wedge is one program growing around the ledger. The load-bearing decision is that none of it lands in the runner. odu keeps its shape — spawn, own one DAG, serve three primitives, exit relevance when the run is done. odu-web is the thing with opinions about many runs: it ingests forge events (webhook first, vira-style polling as the fallback for forges or networks without one), resolves repo × SHA into an odu invocation exactly the way the MCP face’s run tool does today, attaches to the live coordinator as a read client, mirrors nodes and nodeLog into the browser, and writes the verdict into the ledger when the run settles.

Because the browser speaks the same useCell / useStream Solid hooks every other face rides, the live view is the part that costs the least — surface-app already solves the app-shell problems (freshness, build identity, PWA install), with kolu and drishti as its two production consumers. A live run page looks like the wedge page with the verdict still warm:

odu.srid.ca/juspay/kolu — live
odujuspay/kolu26d2c2d

3 lanes · connected

x86_64-linuxaarch64-darwinaarch64-linuxnix✓ 3m40✓ 4m02● 1m12e2e✓ 2m03● 1m55⏸ —unit✓ 0m31✓ 0m44⏸ —lint✓ 0m12✓ 0m14⏸ —
aarch64-darwin · e2e  ● running 1m55s⟳ rerun
cucumber · 14 scenarios ✓ open a terminal … 2.1s ✓ split a pane … 1.4s ▸ reconnect after server restart …▍

And the fleet view is the same data one level up — repo is just another fan-in axis, the generalization of the lane matrix that the odu note catalogued as open:

odu.srid.ca
odu — fleet4 repos · 2 running · 11 runs today
repo · refcommitverdictdurationjuspay/kolu · master26d2c2d● running4m12juspay/kolu · PR #129153c0889✗ e2e9m03juspay/odu · master7fa40f3✓ green2m31srid/drishti · mastera41c9e2● nix1m18

The boundary the browser alone forces arrives on schedule, not up front: read-observer vs mutator. rerun is remote code execution, and a hosted page is the first client whose holder is not automatically the operator. The wedge and the live observer ship with mutations simply absent from the wire — the same trick odu mcp used to dodge run.configure — and Phase 4 introduces the mutator role as an explicit grant rather than retrofitting auth onto a surface that already leaked it.

Does it replace vira?

It should — on the timescale where the service face actually exists, and not before. vira is “no-frills CI for small teams using Nix”: a Haskell web app with acid-state persistence, managing registered repos and building them in Nix environments. It works today; nothing here argues for switching anything off this week. The argument is structural, and it is the same one the odu note ran against justci:

Axisvira todayodu-web as planned
Trigger modelwatches registered repos, builds on its own initiativePhase 3 — webhook first, polling fallback
Persistenceacid-state databasethe run ledger (Phase 1), grown from odu’s per-SHA layout
Multi-repoyes — its core shapePhase 3 — repo as a fan-in axis
Run granularitythe build, coarseodu’s per-node DAG: statuses, logs, surgical rerun per node
Live observationpage-gradesnapshot-then-delta surface; late attach replays everything
Agent facenoneshipped — odu mcp ( #3 )
Forge gateshipped — byte-compatible commit statuses + protect
StackHaskell · htmx · acid-stateTypeScript · @kolu/surface · surface-app (Solid)

Two stacks under one author is a losing position — justci already taught that lesson here. vira’s differentiators (triggers, persistence, multi-repo web UI) are things odu-web must build anyway; odu’s differentiators (the typed live surface, per-node DAG, the MCP face) are things vira’s batch-of-builds shape cannot retrofit, for the same reason justci couldn’t. So the honest framing is sunset-when-superseded: vira keeps serving its teams until odu-web’s Phase 3 covers a Nix team’s daily loop, and Phase 4’s exit criterion below names the test rather than the wish.

Phases

Phase 0 lives in the runner and is already on odu’s Phase 2 backlog; everything after it lives in odu-web, each phase shippable on its own.