feat(web): Pipeline list view at /app/monitor (M19-2) #191

Merged
code-lead merged 1 commit from boss/175 into main 2026-04-20 21:47:21 +00:00
Collaborator

Summary

Turn /app/monitor into an issue-centric Pipeline list driven by GET /issues/pipeline (from #M19-1, shipped in #183). Each row shows one open issue with a horizontal mini-pipeline of stage pills so operators can answer "where is issue N?" at a glance. The legacy M18-3 task-event view is preserved one click away under the Tasks tab.

Closes #175.

Routing

URL Purpose
/app/monitor Pipeline list (new default, M19-2)
/app/monitor/tasks Legacy task-event three-pane view (M18-3 parity)
/app/monitor/task/$taskId Standalone, bookmarkable single-task view (shared by both surfaces)

The AppShell nav grows Pipeline and Tasks tabs (Pipeline is exact-match so /monitor/tasks doesn't also light it up).

Row-click pattern choice (per acceptance criteria)

Clicking an issue row's title sets up the href for /app/monitor/issue/<repo>/<n> (the expanded per-issue view landing in #M19-3) but preventDefaults until that route exists — so the URL shape is stable/bookmarkable today without 404-ing. Stage pills are the primary interaction and deep-link to /app/monitor/task/<task_id> verbatim from M18-3.

Stage palette

Colours resolve through design-token CSS variables (--stage-* in tokens.css) — no raw hex. Round counter (↺N, purple) and force-merge (★, gold) glyphs survive compact mode so they stay visible at a glance.

State Glyph Token
success --stage-success
running --stage-running (pulses via @keyframes stage-pulse)
pending --stage-pending
failure --stage-failure
stalled --stage-stalled
round > 1 ↺N --stage-round (purple, reviewer loop)
force-merge --stage-force-merge (gold)

The running animation is a shared @keyframes stage-pulse keyframe — the Grid (#M19-4) and Gantt (#M19-5) views can reuse it without duplicating CSS.

Live updates

  • pipeline_stage SSE events (already emitted from main.ts and pipeline.ts since M19-1) patch the cached response in-place via patchPipelineStage. Unmatched issues leave the cache unchanged — a freshly dispatched issue picks up on the next poll.
  • 60 s poll against /issues/pipeline is the backstop. TanStack Query's default refetchIntervalInBackground: false (spelled out in the query for audit clarity) satisfies the Page-Visibility acceptance criterion.
  • result events also trigger a targeted invalidateQueries(["pipeline"]) so link / duration_ms fields refresh without waiting 60 s.

Filters

Filters (repo / milestone / assignee / label / open|closed|all) sync to URL query params via TanStack Router's validateSearch + useSearch. Views are shareable — /app/monitor?repo=charles/claude-hooks&state=closed&label=area:dashboard round-trips cleanly.

repo and state forward to the server; milestone / assignee / label filter client-side against the 5-s-cached response so we don't fan out a separate fetch per combination.

Tests

  • Vitest (9 <StagePill /> unit tests + 19 <PipelineList /> + cache-patcher tests): renders 5 fixture issues spanning every stage state, asserts pill data-role / data-state / glyph + hrefs, exercises filter logic + patchPipelineStage's structural mutation (non-matching rows preserved by identity).
  • Playwright e2e/pipeline.spec.ts: /app/monitor → click stage pill → land on /app/monitor/task/$id → browser Back returns to the pipeline list.
  • Existing e2e/monitor.spec.ts retargeted to /app/monitor/tasks (the legacy view's new home).
  • Link mocked once in vitest.setup.tsx so component tests don't need a full <RouterProvider> in context (avoids the act(…) warning flood from async route transitions under happy-dom).

Test plan

  • bun x turbo run typecheck — green across server + shared + web
  • bun x biome check . — 129 files, no issues
  • bun x turbo run test — 761 server tests + 41 web tests pass
  • bun run build (apps/web) — bundle builds clean; routeTree regenerated
  • Manual: operator opens /app/monitor, filters to state=open, clicks a stage pill, lands on the task view, Back returns
  • Manual: SSE pipeline_stage event updates a single row pill without a full table re-render
  • Manual: /app/monitor?repo=x&label=area:dashboard reproduces the filtered view on reload

🤖 Generated with Claude Code

## Summary Turn `/app/monitor` into an issue-centric **Pipeline list** driven by `GET /issues/pipeline` (from #M19-1, shipped in #183). Each row shows one open issue with a horizontal mini-pipeline of stage pills so operators can answer "where is issue N?" at a glance. The legacy M18-3 task-event view is preserved one click away under the **Tasks** tab. Closes #175. ## Routing | URL | Purpose | |---|---| | `/app/monitor` | **Pipeline list** (new default, M19-2) | | `/app/monitor/tasks` | Legacy task-event three-pane view (M18-3 parity) | | `/app/monitor/task/$taskId` | Standalone, bookmarkable single-task view (shared by both surfaces) | The AppShell nav grows **Pipeline** and **Tasks** tabs (Pipeline is `exact`-match so `/monitor/tasks` doesn't also light it up). ### Row-click pattern choice (per acceptance criteria) Clicking an issue row's title sets up the href for `/app/monitor/issue/<repo>/<n>` (the expanded per-issue view landing in #M19-3) but `preventDefault`s until that route exists — so the URL shape is stable/bookmarkable today without 404-ing. Stage pills are the primary interaction and deep-link to `/app/monitor/task/<task_id>` verbatim from M18-3. ## Stage palette Colours resolve through design-token CSS variables (`--stage-*` in `tokens.css`) — no raw hex. Round counter (↺N, purple) and force-merge (★, gold) glyphs survive compact mode so they stay visible at a glance. | State | Glyph | Token | |---|:---:|---| | success | ✓ | `--stage-success` | | running | ● | `--stage-running` (pulses via `@keyframes stage-pulse`) | | pending | ○ | `--stage-pending` | | failure | ✗ | `--stage-failure` | | stalled | ⚠ | `--stage-stalled` | | round > 1 | ↺N | `--stage-round` (purple, reviewer loop) | | force-merge | ★ | `--stage-force-merge` (gold) | The running animation is a shared `@keyframes stage-pulse` keyframe — the Grid (#M19-4) and Gantt (#M19-5) views can reuse it without duplicating CSS. ## Live updates - `pipeline_stage` SSE events (already emitted from `main.ts` and `pipeline.ts` since M19-1) patch the cached response in-place via `patchPipelineStage`. Unmatched issues leave the cache unchanged — a freshly dispatched issue picks up on the next poll. - 60 s poll against `/issues/pipeline` is the backstop. TanStack Query's default `refetchIntervalInBackground: false` (spelled out in the query for audit clarity) satisfies the Page-Visibility acceptance criterion. - `result` events also trigger a targeted `invalidateQueries(["pipeline"])` so `link` / `duration_ms` fields refresh without waiting 60 s. ## Filters Filters (repo / milestone / assignee / label / `open`|`closed`|`all`) sync to URL query params via TanStack Router's `validateSearch` + `useSearch`. Views are shareable — `/app/monitor?repo=charles/claude-hooks&state=closed&label=area:dashboard` round-trips cleanly. `repo` and `state` forward to the server; `milestone` / `assignee` / `label` filter client-side against the 5-s-cached response so we don't fan out a separate fetch per combination. ## Tests - **Vitest** (9 `<StagePill />` unit tests + 19 `<PipelineList />` + cache-patcher tests): renders 5 fixture issues spanning every stage state, asserts pill `data-role` / `data-state` / glyph + hrefs, exercises filter logic + `patchPipelineStage`'s structural mutation (non-matching rows preserved by identity). - **Playwright** `e2e/pipeline.spec.ts`: `/app/monitor` → click stage pill → land on `/app/monitor/task/$id` → browser Back returns to the pipeline list. - Existing `e2e/monitor.spec.ts` retargeted to `/app/monitor/tasks` (the legacy view's new home). - `Link` mocked once in `vitest.setup.tsx` so component tests don't need a full `<RouterProvider>` in context (avoids the `act(…)` warning flood from async route transitions under happy-dom). ## Test plan - [x] `bun x turbo run typecheck` — green across server + shared + web - [x] `bun x biome check .` — 129 files, no issues - [x] `bun x turbo run test` — 761 server tests + 41 web tests pass - [x] `bun run build` (apps/web) — bundle builds clean; routeTree regenerated - [ ] Manual: operator opens `/app/monitor`, filters to `state=open`, clicks a stage pill, lands on the task view, Back returns - [ ] Manual: SSE `pipeline_stage` event updates a single row pill without a full table re-render - [ ] Manual: `/app/monitor?repo=x&label=area:dashboard` reproduces the filtered view on reload 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(web): Pipeline list view at /app/monitor (M19-2)
All checks were successful
qa / qa (pull_request) Successful in 3m7s
qa / dockerfile (pull_request) Successful in 9s
79a993c054
Turn /app/monitor into an issue-centric pipeline list driven by
/issues/pipeline (M19-1) with one horizontal mini-pipeline of stage
pills per row. Legacy task-event view preserved at /app/monitor/tasks;
stage pills deep-link to /app/monitor/task/$taskId (bookmarkable).

Routing:
- /monitor       → pipeline list (new default)
- /monitor/tasks → legacy M18-3 three-pane task view
- /monitor/task/$taskId → standalone task-event view (shared by both)

Stage palette resolves through design tokens (--stage-*) — no raw hex.
Running pills share @keyframes stage-pulse so M19-4 Grid and M19-5
Gantt views can reuse the animation. Force-merge and review-round
glyphs survive compact mode so they stay visible at a glance.

Live updates:
- pipeline_stage SSE events patch the cached response in-place (no
  full re-fetch). Unmatched events are ignored.
- 60 s poll against /issues/pipeline as backstop; pauses when tab is
  hidden (TanStack Query focusManager default).

Filters (repo / milestone / assignee / label / open|closed|all) sync
to URL query params so views are shareable.

Tests:
- 9 stage-pill unit tests + 19 pipeline-list + SSE-patcher tests
- Playwright e2e: /monitor → stage pill → task view → back
- Link stubbed via vitest.setup.tsx so component tests don't need a
  full <RouterProvider> in context

Closes #175

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
reviewer approved these changes 2026-04-20 21:46:31 +00:00
reviewer left a comment

Review — M19-2 Pipeline list view at /app/monitor

CI: green (run #1795, 3m17s)

Acceptance criteria — all met

Criterion Status
/app/monitor → pipeline list (default)
/app/monitor/tasks → legacy tasks view preserved
/app/monitor/task/:id standalone deep-link
Mini-pipeline strip (all 7 palette states + glyph + compact mode)
Hover tooltip: agent, started/finished, duration, cost, turns
Filter bar: repo / milestone / label / assignee / state, URL-synced
Empty state with tasks fallback link
SSE pipeline_stage in-place cache patching
60 s poll backstop; refetchIntervalInBackground: false (Page Visibility)
Design tokens only — no raw hex
@keyframes stage-pulse shared keyframe
Tests: 9 StagePill + 19 PipelineList + SSE patcher (Vitest); Playwright e2e
README Pipeline monitor subsection

Notable correctness details

patchPipelineStage identity contract (monitor.index.tsx): non-matching rows are returned by reference, only the affected IssuePipeline and its affected StageEntry are rebuilt as fresh objects. This correctly triggers TanStack Query's shallow-equality check so only the pill for the changed issue re-renders — not the whole table.

current_stage is only updated on running transitions, not cleared on success/failure. Intentional — the 60 s poll corrects it, and the comment in the code says so. No issue.

Assignee filter reverse-lookup in FilterBar: display names come from roleBaseName(a), selection reverses via assignees.find((a) => roleBaseName(a) === v) to recover the full login before writing to URL. Correct — URL stores the full login, applyFilters compares against i.assignee (the full login).

vitest.setup.tsx global Link mock: trades per-test router isolation for freedom from act(…) warning floods. Components needing real router semantics use TestRouter from lib/test-router.tsx. Acceptable trade-off, documented.

Label facet only surfaces area: / type: prefixed labels: deliberate narrowing to routing-relevant labels; keeps the dropdown from ballooning with priority:* etc. The intent is clear even without a prose comment.

__testables export pattern: component stays the primary export, internal helpers (applyFilters, worstStageState, uniqueSorted, STATE_RANK) are reachable for unit tests without structural refactoring. Clean.

No bugs, no safety issues, no missing acceptance criteria. LGTM.

## Review — M19-2 Pipeline list view at /app/monitor **CI**: ✅ green (run #1795, 3m17s) ### Acceptance criteria — all met | Criterion | Status | |---|---| | `/app/monitor` → pipeline list (default) | ✅ | | `/app/monitor/tasks` → legacy tasks view preserved | ✅ | | `/app/monitor/task/:id` standalone deep-link | ✅ | | Mini-pipeline strip (all 7 palette states + glyph + compact mode) | ✅ | | Hover tooltip: agent, started/finished, duration, cost, turns | ✅ | | Filter bar: repo / milestone / label / assignee / state, URL-synced | ✅ | | Empty state with tasks fallback link | ✅ | | SSE `pipeline_stage` in-place cache patching | ✅ | | 60 s poll backstop; `refetchIntervalInBackground: false` (Page Visibility) | ✅ | | Design tokens only — no raw hex | ✅ | | `@keyframes stage-pulse` shared keyframe | ✅ | | Tests: 9 StagePill + 19 PipelineList + SSE patcher (Vitest); Playwright e2e | ✅ | | README Pipeline monitor subsection | ✅ | ### Notable correctness details **`patchPipelineStage` identity contract** (`monitor.index.tsx`): non-matching rows are returned by reference, only the affected `IssuePipeline` and its affected `StageEntry` are rebuilt as fresh objects. This correctly triggers TanStack Query's shallow-equality check so only the pill for the changed issue re-renders — not the whole table. **`current_stage` is only updated on `running` transitions**, not cleared on `success`/`failure`. Intentional — the 60 s poll corrects it, and the comment in the code says so. No issue. **Assignee filter reverse-lookup** in `FilterBar`: display names come from `roleBaseName(a)`, selection reverses via `assignees.find((a) => roleBaseName(a) === v)` to recover the full login before writing to URL. Correct — URL stores the full login, `applyFilters` compares against `i.assignee` (the full login). **`vitest.setup.tsx` global `Link` mock**: trades per-test router isolation for freedom from `act(…)` warning floods. Components needing real router semantics use `TestRouter` from `lib/test-router.tsx`. Acceptable trade-off, documented. **Label facet only surfaces `area:` / `type:` prefixed labels**: deliberate narrowing to routing-relevant labels; keeps the dropdown from ballooning with `priority:*` etc. The intent is clear even without a prose comment. **`__testables` export pattern**: component stays the primary export, internal helpers (`applyFilters`, `worstStageState`, `uniqueSorted`, `STATE_RANK`) are reachable for unit tests without structural refactoring. Clean. No bugs, no safety issues, no missing acceptance criteria. LGTM.
code-lead deleted branch boss/175 2026-04-20 21:47:21 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks!191
No description provided.