Epic DAG live view¶
kew serve renders the epic driver's own record as a live, replayable
dependency graph — the flight-recorder model applied to kew epic
run/advance instead of a single dispatch. Two pages:
/epics— a list of every epic the driver has journaled at least one tick for: ref, tri-state signal, done/failed counts, drain spend, and how long ago the last tick landed. This is navigation, not a dashboard — one epic per page, no fleet-wide composite view./epics/<ref>— the graph for one epic, in live mode while a driver is running it, or replay mode over a completed drain's full journal history.
Both pages are read-only, like the Runs view:
no dispatch, merge, retry, or stop affordance lives here. A HELD node's pane
deep-links to /inbox for approval context; a FAILED node's pane shows the
kew epic retry <node> command rather than a button that would run it.
What feeds the page: the tick journal¶
Everything rendered here — signal, node statuses, counts, frontier, drain
spend — is read from the epic_ticks/epic_driver_state tables the driver
itself journals every tick (see kew epic: Tick journal).
The page never re-derives status from GitHub on its own; it shows the
driver's own record, so there is no way for "what the driver did" and "what
the page shows" to disagree. An epic the driver has never ticked shows an
honest empty state rather than a fabricated graph.
The honesty model: two layers, updated differently¶
The page draws two visually distinct layers on top of the same graph, and they are not on the same clock:
- Status colors (a node's fill, the frontier ring, HELD/FAILED treatment) change only on journal frames — i.e. only when the driver itself observed and recorded a transition. Every status-colored pixel traces to a journaled, audited record.
- The live-telemetry layer (an activity dot on IN_PROGRESS nodes, the in-flight cost readout) updates on the SSE poll cadence, independent of whether the driver has journaled anything new. It is visually distinct from status color and labeled as live telemetry, not as a driver observation.
Tick cadence moves the graph; run cadence moves the money. Between transitions you see proof of life — the telemetry layer, the "tick N · change M · Xs ago" header — never fake motion invented to make the page look busier than the driver actually is.
Live vs. replay¶
- Live subscribes to
GET /api/epics/<ref>/stream(SSE): the server fans outsnapshot/economics/heartbeatframes from a shared poller readingdispatch.db, at most one poll per second per driven epic regardless of how many browser tabs are open. A reconnect (including the server's periodic stream cap) always re-sends the latest frames rather than replaying history — the live view never backfills missed intermediate actions; only replay does that. - Replay fetches the full journal via
GET /api/epics/<ref>/ticksand steps through it locally. Pacing is frame-indexed, not wall-clock: changed-only journal frames are wildly irregular (a multi-hour HELD plateau, then a merge cascade), so linear time-compression would render most of a drain as dead air. The default preset dwells roughly30s / frame_countper frame (clamped to a sane per-frame range). The wall clock is mandatory UI in replay — each frame shows itstick_tsand elapsed time since the drain started, and the scrubber's tick marks are spaced by real elapsed time, not by frame index, so a clock jump between two adjacent frames is visible rather than hidden behind evenly spaced marks.
Drain-spend semantics¶
The header's spend figure and each node's cost badge are drain-scoped,
not lifetime: they sum runs rows with finished_at on or after the epic's
first journal row, so a node's cost badge doesn't inflate with unrelated
manual dispatches from before the drain started. Replay frames apply the
same window, upper-bounded at each frame's own tick_ts — so the first
replay frame's spend reads near zero, as it honestly should, and the last
frame matches the epic's true total drain spend.
Lifetime issue spend (all runs ever recorded against that issue number, drain or not) is shown only in the node detail pane, explicitly labeled as lifetime — never conflated with the drain-window headline number.
Staleness, complete, and stalled banners¶
The header banner is signal-aware and mode-aware so it can't lie in either direction:
| Situation | Banner |
|---|---|
| Driver alive, ticking normally | Advancing "tick N · change M · Xs ago" counter, no judgment |
| Driver process gone mid-drain | "driver last seen …" staleness banner once the tick-age threshold is exceeded |
Epic complete |
"COMPLETE · finished <ts>" — never a stale banner, even if the page hasn't been reloaded in hours |
Epic stalled |
A stall banner plus the stuck nodes' reasons — distinct from "driver gone," since a stalled driver exited by design |
One-shot/cron driver (interval_s is null) |
"last driven Xs ago (one-shot driver)" — no alive/gone claim is made, because none is possible |
| Replay mode | The live staleness header is replaced entirely by the frame's own wall clock |
Auth under --expose¶
The DAG page has no login UI of its own. It reuses the same session cookie
the Inbox mints from the /inbox#token=…
flow:
- On loopback without
--expose, reads work ungated, matching the rest of the read surface. - Under
--expose, every/api/epics*route requires a valid session (the router-levelread_guardpattern — no endpoint on this router can be added without it). - Any 401 redirects the browser to
/inbox, which handles both a fresh#token=fragment and an existing session, then returns. - No epics route ever accepts a token as a query or fragment parameter.
The token only ever travels once, in the
/inboxfragment, exactly like every other authenticated surface in kew.
See Security: Approval inbox and
kew serve: Authentication for the full
model this page inherits.
Recording a demo¶
The page is its own capture pipeline — no export code, just a screen
recorder over real infrastructure. Two scripts (run from the kew/ package
dir) reproduce the canonical scripted drain end-to-end:
# 1. Generate a completed demo drain (9 journaled ticks, HELD + FAILED beats)
uv run python scripts/make_demo_epic.py --out /tmp/demo-src
# 2. Point a scratch project's [logging].dir at an empty logs dir, start
# `kew serve` there, and open /epics in a browser.
# 3a. Replay capture: copy /tmp/demo-src/dispatch.db into the logs dir,
# open /epics/demo-epic, switch to replay, press play, record.
# 3b. Live capture: leave the logs dir empty and perform the drain in
# real time instead — statuses advance on screen as rows land:
uv run python scripts/drip_demo_epic.py /tmp/demo-src/dispatch.db \
<scratch-project>/logs/dispatch.db --interval 6
The drip script shifts every timestamp so the drain reads as happening now (fresh heartbeat, live-accumulating drain spend) and recomputes each snapshot hash after the rewrite, so the journal it produces stays internally truthful. Record with the OS screen recorder (macOS: Cmd-Shift-5); the 30-second pitch is the replay at default pacing.
Known limitation: epic refs containing /¶
epic_ref is routed as a plain path segment (/epics/<ref>), validated
against a conservative charset before use. GitHub label names can contain
/, and such an epic ref is unroutable in v1 — the driver still drives
it fine; only the web page 404s on it. There is no slug-encoding scheme yet;
this is a known, accepted gap rather than an oversight.
Related¶
kew epic— the driver commands and the tick journal this page reads.- Web UI overview — the cockpit shell this page sits alongside.
- Security: Approval inbox — the auth model
this page inherits under
--expose. - Design spec:
docs/superpowers/specs/2026-07-01-epic-dag-live-view-design.md