Skip to content

feat(web): a dispatch timeline lens on the fleet rail - #335

Open
saucam wants to merge 2 commits into
mainfrom
feat/p5-4-dispatch-timeline
Open

saucam wants to merge 2 commits into
mainfrom
feat/p5-4-dispatch-timeline

Conversation

@saucam

@saucam saucam commented Sep 14, 2026

Copy link
Copy Markdown
Collaborator

P5.4 (conductor-frontends-design.md §4). The rail gains a second lens: what was dispatched when, and what came back.

Why a second lens rather than a richer list

The lanes answer "what needs me now" by re-sorting on urgency — which is exactly wrong for retrospection. A list that reorders itself cannot be read as history. §4 asks for both, so they now sit behind a switch instead of one pretending to do the other's job.

Built from the two streams the board already carries (tasks + events). No new wire data.

Four rules, each from a case that would otherwise read wrong

A dispatch and its outcome routinely share a millisecond on a fast task. Newest-first then has to render the dispatch second, or the row order implies the result preceded the request.

An unknown event type is kept, not dropped. A newer daemon naming something differently must not silently erase rows from the one view whose entire purpose is history.

Events outlive their tasks. The board is capped, so a long run ages tasks out while their events remain — and "the digest for a task I can no longer see" is the most useful thing left. Such a row keeps its task id so it still correlates by eye.

Days split on LOCAL midnight. A UTC split files an evening dispatch under tomorrow for anyone east of the meridian.

A fix that only showed up on real data

The target now resolves to a session name where one exists, degrading to a short id prefix otherwise.

That came from rendering the actual captured board rather than fixtures — every dispatch row showed a full UUID like 7bdbd557-656a-4b0c-bab1-6a7894ab3efe, which is noise in a 288px rail and tells the reader nothing. And a finished task has no session at all, since the dispatcher tears its worker down after the digest, so the fallback matters as much as the happy path.

Verified against a real board

5 tasks, 5 events:

Sun Sep 06 2026
  12:55 PM  [done      ] task_done     1c261b20  task 1c261b20 (spawn/scout) in /tmp/…
  12:55 PM  [dispatched] spawn scout   1c261b20  worker-scout-1c261b20
Sat Sep 05 2026
  11:52 PM  [done      ] task_done     17960316  …
  09:00 PM  [blocked   ] task_blocked  cff8e6c5  task cff8e6c5 (spawn/scout) auto-BLOCKED
  08:59 PM  [dispatched] spawn scout   cff8e6c5  01f9ec68

Day separators land correctly, each outcome sits directly above its dispatch, and the two tasks blocked by the old daemon-collision bug (#319) show their real task_blocked digests.

Verification: 13 timeline tests, 279 web tests total, typecheck / lint / build clean.

What's still blocked in P5.4

The map/tree lens remains undrawable — createdBy is always the conductor (never the dispatching agent), workers get no fleet tools so sub-workers can't exist, and finished workers are torn down before they could be leaves. It needs daemon changes first; details in my earlier analysis.

Routing rationale and cross-vendor review edges likewise have no wire representation yet.

🤖 Generated with Claude Code

P5.4 (docs/conductor-frontends-design.md §4). The rail gains a second
lens: what was dispatched when, and what came back.

The lanes answer "what needs me now" by re-sorting on urgency, which is
exactly wrong for retrospection — a list that reorders itself cannot be
read as history. §4 asks for both, so the two now sit behind a switch
rather than one pretending to do the other's job.

Built from the two streams the board already carries, tasks and events,
with no new wire data.

Four rules, each from a case that would otherwise read wrong:

**A dispatch and its outcome routinely share a millisecond** on a fast
task. Newest-first then has to render the dispatch SECOND, or the row
order implies the result preceded the request.

**An unknown event type is kept, not dropped.** A newer daemon naming
something differently must not silently erase rows from the one view
whose purpose is history.

**Events outlive their tasks.** The board is capped, so a long run ages
tasks out while their events remain — and "the digest for a task I can no
longer see" is the most useful thing left. Such a row keeps its task id
so it still correlates by eye.

**Days split on LOCAL midnight.** A UTC split files an evening dispatch
under tomorrow for anyone east of the meridian.

The target resolves to a session NAME where one exists, degrading to a
short id prefix otherwise. That came from rendering the real board rather
than fixtures: every dispatch row showed a full UUID, which is noise in a
288px rail and tells the reader nothing — and a finished task has no
session at all, since the dispatcher tears its worker down after the
digest.

Verified against a captured board of 5 tasks and 5 events: day separators
land correctly, each outcome sits directly above its dispatch, and the
two tasks blocked by the old daemon-collision bug show their real
task_blocked digests.

13 timeline tests; 279 web tests; typecheck, lint and build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@highflame-oracle highflame-oracle Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔮 Oracle Review

🎯 Start Here

web/src/components/FleetRail.tsx (~21 min) — Security changes in FleetRail.tsx


📋 PR Summary

What this PR does: Adds a dispatch timeline lens to the fleet rail that displays historical dispatch data with outcomes, separate from the existing urgency-based lanes view.

Key changes:

  • Implemented timeline view using existing tasks and events streams (no new wire data)
  • Added logic to handle dispatch/outcome ordering on same millisecond timestamps
  • Implemented local midnight day splitting instead of UTC
  • Added session name resolution with UUID prefix fallback for readability

Areas affected: Fleet rail component, Timeline display logic

Testing notes: Verified against real board data with 5 tasks and 5 events; includes 13 timeline tests as part of 279 total web tests


🔍 Code Review

This is a thoughtful implementation that solves a real UX problem—separating urgency-based workflows from historical retrospection. The four design rules show deep consideration of edge cases, and leveraging existing data streams avoids unnecessary backend changes. Strong verification against real data and comprehensive test coverage gives confidence in the quality.

What's good:

  • ✨ Excellent architectural decision to use two separate lenses rather than overcomplicating a single view
  • ✨ Smart approach of building from existing wire data (tasks + events) without new API endpoints
  • ✨ Thorough handling of edge cases: same-millisecond ordering, unknown event types, outliving tasks, and local timezone splitting

Review Stats: suggestion:1


Generated by Oracle - Highflame's AI Code Reviewer

>
<For each={lanes()}>{(lane) => <Lane group={lane} />}</For>
</Show>
</Show>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion

Verify date parsing for day headers

Using new Date(day.day) relies on the format returned by groupByDay. If day.day is a date string (e.g., '2026-09-06'), browsers typically parse it as UTC, which may shift the header by one day depending on the user's local timezone. Ensure groupByDay returns an ISO timestamp or that parsing logic explicitly handles the local time context intended by the 'LOCAL midnight' requirement.

Suggested fix:

Suggested change
</Show>
If `day.day` is a string, consider using a local-parsing library or constructing the date explicitly from parts to avoid UTC shifts. If it is a timestamp, the current implementation is correct.

Related: lib/fleet-timeline.ts

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.

2 participants