Dashboard: the landing page is the work waiting for you, in lanes #416

Merged
mph merged 13 commits from dashboard-control-plane into main 2026-08-13 06:19:15 +00:00
Owner

The card (frozen)

Goal. I open the dashboard and the first thing I see is every worktree that wants me, across
all my projects, each naming what it wants. I work down that one list until it is empty, and I
never open a project to find out whether there is work inside it.

Fit test. I land on http://localhost:9876/ cold, after a break, and work my way through
everything waiting — without visiting a project page to discover work.

Fit passed 2026-08-12, on Michal's read of the running board.

At definition the same work cost ten project pages carrying 51 worktree cards — 2.4 cards
read per act, plus the overview to learn which ten to open. It is now one page.

What ships

  • GET / is the work waiting on you. One board across every project, read through
    DashboardService.pendingActFor — the same resolution the per-project counts count, so the
    rows and the counts cannot disagree. A test pins that equality.
  • One lane per kind of act. The act is read once at the head of a lane instead of once per
    row. PendingAct.kind names the lane, PendingAct.detail carries what the kind drops
    (#415, 2 of 5), and PendingAct.rank puts closing-out behind the live work and the
    undecided behind everything. Recency orders the rest, and the lanes wrap rather than scroll
    sideways.
  • A card leads with the worktree key, then its project and how long it has waited, and the
    whole card clicks through to the worktree.
  • A waiting workflow claims you in its own words. A claim is needs_attention: true, or
    activity: waiting with a published status. Twelve worktrees were invisible to every count in
    the dashboard because their workflow only ever published the second — MEDECA-407 among them.
    An explicit false still withdraws the claim; absence leaves the activity to speak. 20 acts
    became 33.
  • GET /projects is the project list, unchanged, with the SSH-host form. Every breadcrumb
    that reached it still reaches it. Both pages reconcile with disk on load.
  • An empty board says you are done, not that something broke.

Two card amendments, approved at the fit reads

The card stayed frozen; both amendments are recorded in the tracker with Michal's words.

  1. The don't-care "anything that changes which worktrees want me" was lifted for one named
    case — the claim rule above. Still one rule in PendingAct.resolve, no second rule for the
    board.
  2. Scope forks "Row shape: dense — one line per worktree" and "Grouping: flat" were replaced
    by lanes of cards. Grouping by project stays refused; the amendment groups by act.

Review

project-management/efforts/dashboard-control-plane/slices/05-the-work-not-the-projects/review-slice-2026-08-12.md

Seven skills, one round: 3 critical, 8 warnings, 11 suggestions. All three criticals fixed
in-loop — a lane whose rank depended on which card was newest, /projects losing the disk
reconciliation, and the pull-request lane having no test through WaitingWork.all.

Gate: dashboard 241 · dashboard itest 240 · core 186 · E2E 176 BATS tests, 0 failures.
CI run 384 green.

Correction: the E2E gate was reported green in the first version of this body before it
was verified. test/dashboard-dev-mode.bats still asserted href="/" on the not-found
page, whose breadcrumb this slice moved to /projects; CI caught it and it is fixed in
e6fd8ea.

Follow-ups (deferred, in the effort map)

  • PendingAct.rank as a lane type carrying name and rank together, so kind and rank cannot
    drift apart by convention (the fixed critical is guarded by a test, not by the types).
  • Move WaitingWork/WaitingLane out of presentation.views into application/, with
    ProjectSummary — they call back into DashboardService and hide file I/O.
  • Extract a named PendingActPresentation instead of widening WorktreeCardRenderer.
  • CaskServer as a god object (carried debt).
  • No authentication on a LAN-bound server: GET / now discloses a cross-project inventory in
    one unauthenticated request. Pre-existing gap, widened surface — effort-level.
  • tell-the-projects-apart — project chips to filter by, and a colour per project instead of
    one orange for everything. Named by Michal at the close read; the next slice.
  • liveness-is-not-a-claim — the board is accurate when you return from a break and stale
    while you work, because a review state is a deliberate signal and an agent running is a
    mechanical one. Lifecycle hooks in their own channel, or running the agents under our own
    system.

🤖 Generated with Claude Code

## The card (frozen) **Goal.** I open the dashboard and the first thing I see is every worktree that wants me, across all my projects, each naming what it wants. I work down that one list until it is empty, and I never open a project to find out whether there is work inside it. **Fit test.** I land on `http://localhost:9876/` cold, after a break, and work my way through everything waiting — without visiting a project page to discover work. **Fit passed 2026-08-12**, on Michal's read of the running board. At definition the same work cost **ten project pages carrying 51 worktree cards** — 2.4 cards read per act, plus the overview to learn which ten to open. It is now one page. ## What ships - **`GET /` is the work waiting on you.** One board across every project, read through `DashboardService.pendingActFor` — the same resolution the per-project counts count, so the rows and the counts cannot disagree. A test pins that equality. - **One lane per kind of act.** The act is read once at the head of a lane instead of once per row. `PendingAct.kind` names the lane, `PendingAct.detail` carries what the kind drops (`#415`, `2 of 5`), and `PendingAct.rank` puts closing-out behind the live work and the undecided behind everything. Recency orders the rest, and the lanes wrap rather than scroll sideways. - **A card leads with the worktree key**, then its project and how long it has waited, and the whole card clicks through to the worktree. - **A waiting workflow claims you in its own words.** A claim is `needs_attention: true`, or `activity: waiting` with a published status. Twelve worktrees were invisible to every count in the dashboard because their workflow only ever published the second — `MEDECA-407` among them. An explicit `false` still withdraws the claim; absence leaves the activity to speak. **20 acts became 33.** - **`GET /projects` is the project list**, unchanged, with the SSH-host form. Every breadcrumb that reached it still reaches it. Both pages reconcile with disk on load. - **An empty board says you are done**, not that something broke. ## Two card amendments, approved at the fit reads The card stayed frozen; both amendments are recorded in the tracker with Michal's words. 1. The don't-care *"anything that changes which worktrees want me"* was lifted for one named case — the claim rule above. Still one rule in `PendingAct.resolve`, no second rule for the board. 2. Scope forks *"Row shape: dense — one line per worktree"* and *"Grouping: flat"* were replaced by lanes of cards. Grouping by **project** stays refused; the amendment groups by **act**. ## Review `project-management/efforts/dashboard-control-plane/slices/05-the-work-not-the-projects/review-slice-2026-08-12.md` Seven skills, one round: 3 critical, 8 warnings, 11 suggestions. All three criticals fixed in-loop — a lane whose rank depended on which card was newest, `/projects` losing the disk reconciliation, and the pull-request lane having no test through `WaitingWork.all`. Gate: dashboard 241 · dashboard itest 240 · core 186 · E2E 176 BATS tests, 0 failures. CI run 384 green. Correction: the E2E gate was reported green in the first version of this body before it was verified. `test/dashboard-dev-mode.bats` still asserted `href="/"` on the not-found page, whose breadcrumb this slice moved to `/projects`; CI caught it and it is fixed in `e6fd8ea`. ## Follow-ups (deferred, in the effort map) - [ ] `PendingAct.rank` as a lane type carrying name and rank together, so kind and rank cannot drift apart by convention (the fixed critical is guarded by a test, not by the types). - [ ] Move `WaitingWork`/`WaitingLane` out of `presentation.views` into `application/`, with `ProjectSummary` — they call back into `DashboardService` and hide file I/O. - [ ] Extract a named `PendingActPresentation` instead of widening `WorktreeCardRenderer`. - [ ] `CaskServer` as a god object (carried debt). - [ ] No authentication on a LAN-bound server: `GET /` now discloses a cross-project inventory in one unauthenticated request. Pre-existing gap, widened surface — effort-level. - [ ] `tell-the-projects-apart` — project chips to filter by, and a colour per project instead of one orange for everything. Named by Michal at the close read; the next slice. - [ ] `liveness-is-not-a-claim` — the board is accurate when you return from a break and stale while you work, because a review state is a deliberate signal and an agent running is a mechanical one. Lifecycle hooks in their own channel, or running the agents under our own system. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Slice 04 parked a single cross-project waiting list and the dimming of
worktrees that want nothing. The close flush carried the review debts
across and dropped both. The waiting list is now the next candidate,
carrying Michal's reading: he works in a worktree, and a list of
projects makes him click through one to reach the work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first screen is every worktree that names an act, flat across all
projects, most recently handed over first. A row says the act, the
worktree, its project and how long it has waited, and clicks through.
The project list moves to /projects, unchanged, with every breadcrumb
that reached it.

The list reads the acts through DashboardService.pendingActFor, the one
resolution the per-project counts already count, so the rows and the
counts cannot disagree. The prune, the disk sweep and the fresh
review-state read stay on GET /.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two card amendments Michal approved on the first fit read.

A workflow that publishes 'activity: waiting' and a status, but never
raises needs_attention, was invisible to every count in the dashboard.
Twelve worktrees were hidden that way, MEDECA-407 among them. A claim is
now either the raised flag or the workflow saying it waits and naming
the status it waits in. An explicit false still withdraws the claim;
absence leaves the activity to speak. 20 acts became 33.

The list became a board: one lane per kind of act, so the act is read
once at the head of a lane instead of once per row, and the card leads
with the worktree key. PendingAct.kind names the lane and
PendingAct.detail carries what the kind drops. The lanes wrap rather
than scroll sideways.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recency alone put the bookkeeping in front of the work. PendingAct.rank
puts closing out a finished workflow behind the live lanes, and an act
the published facts never decided behind all of them. The sort is
stable, so recency still orders the lanes of one rank.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Michal read the board against the card and closed the fit test. Two
wants parked as tell-the-projects-apart: project chips to filter by, and
a colour per project instead of one orange for everything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(dashboard): one lane, one place on the board — and both pages meet disk
Some checks failed
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 7s
CI / lint (pull_request) Successful in 7s
CI / compile (pull_request) Successful in 1m17s
CI / test (pull_request) Has been cancelled
CI / dashboard-build (pull_request) Has been cancelled
a5f9c45df7
Code review of the slice found three things.

A worktree publishing all_complete with no pull request resolved to
ReviewInWorktree, whose lane name is byte-identical to FinishWorkflow's
but whose rank is not. The two shared a lane whose position then
depended on which card was newest, so the lane moved for no visible
reason. A workflow with no phases left asks to be finished either way,
so it now resolves to the one act.

Splitting GET / left the prune and the disk sweep on the landing page
alone, so /projects reached directly showed worktrees that were gone.
Both pages now reconcile with disk, through one helper.

The fresh review-state read, written out three times after the split, is
also one helper now. Tests added for the pull request lane, for
renderWaitingList, for HTML escaping on the board, and for the singular
count, the unattributed project and a moment published in the future.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
workflow(sl): close 05-the-work-not-the-projects (flush parked items)
Some checks failed
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 6s
CI / lint (pull_request) Successful in 7s
CI / compile (pull_request) Successful in 1m14s
CI / test (pull_request) Has been cancelled
CI / dashboard-build (pull_request) Has been cancelled
086fb0acb7
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
workflow(sl): name the pull request on the slice card
Some checks failed
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 6s
CI / lint (pull_request) Successful in 7s
CI / dashboard-build (pull_request) Successful in 57s
CI / test (pull_request) Failing after 8m18s
CI / compile (pull_request) Successful in 1m11s
7cee0eb865
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
test(e2e): the not-found page links to the project list
All checks were successful
CI / contract (pull_request) Has been skipped
CI / compile (pull_request) Successful in 1m20s
CI / dashboard-build (pull_request) Successful in 1m2s
CI / format (pull_request) Successful in 7s
CI / lint (pull_request) Successful in 7s
CI / test (pull_request) Successful in 12m42s
e6fd8ea57e
The breadcrumb moved to /projects with the page it points at; the BATS
assertion still pinned the old root link.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
workflow(sl): correct the E2E gate claim in the review report
All checks were successful
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 13s
CI / lint (pull_request) Successful in 16s
CI / compile (pull_request) Successful in 1m25s
CI / dashboard-build (pull_request) Successful in 1m17s
CI / test (pull_request) Successful in 9m51s
6ab6da794b
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
mph merged commit 6e505bcffc into main 2026-08-13 06:19:15 +00:00
Sign in to join this conversation.
No description provided.