Dashboard: tell the projects apart by colour, and narrow the board to one project #575

Merged
mph merged 23 commits from dashboard-control-plane into main 2026-09-17 08:10:44 +00:00
Member

Slice 11 — Tell the projects apart (dashboard-control-plane)

Goal. Michal reads an act on the board and knows which project it is in without reading the card, and narrows the board to one project when he wants to work that project down.

Fit test. On his own server at localhost:9876, he works the board down and acts only from what it shows: nothing he knows about is missing, nothing foreign is present, and "which project is this act in?" is answered by looking.

Fit: passed — called by Michal on his own board on 2026-09-16.

Slices 01–10 of this effort were already shipped and merged; this PR carries slice 11 only.

What ships

  • A project's colour is decided by its name alone (ProjectColour, 18 hues × 3 shades), so it is the same on every page, after a restart and tomorrow. Only the two derived numbers reach the page.
  • Act cards are drawn in their project's colour. Unresolved stays grey; a worktree with no nameable project gets no colour.
  • A chip row lists every project with waiting work and its count. A chip chooses its project through the URL (/?project=<name>), and the board re-reads that address every 5 s, so the choice survives the refresh.
  • The Where the agents are lane follows the same choice. There the agent's state colours the card and the project is a coloured chip inside it — the mirror of an act card.
  • Page order: chips, then agents, then acts. The chip row sits above both halves because it narrows both.
  • The projects page now links back to the board.

Close review

Seven review skills (style, testing, security, scala3, composition, architecture, api): 0 critical. Nine findings fixed in bb84c347, among them one shared rule for the colour-to-CSS attributes and one for narrowing, a length bound on ?project=, escaping and empty-value tests on the route, and a temp-directory leak in a server test. Full report: project-management/efforts/dashboard-control-plane/slices/11-tell-the-projects-apart/review-slice-2026-09-16.md.

The security review confirmed the card's invariant: nothing a worktree controls reaches a style or class attribute.

Lessons and follow-ups

  • Follow-up filed: #573 — the dashboard server does not record its own pid.
  • Two lessons land as a new project hook, .claude/hooks/build-tool-traps.sh (wired in .claude/settings.json), verified firing in a live session:
    • a variable prefixed onto a daemon-backed build command never reaches the tasks it spawns;
    • a build tool's N/N summary counts build tasks, not tests.

Parked (flushed to the effort map)

  • Decide what presentation/views means — pure decision logic is accreting beside the HTML.
  • Write down where a pure type belongs (domain vs presentation).
  • One source for the stylesheet's three-shade scale.

Known red, not from this branch

  • test/start.bats — "the tmux session does not carry iw's internal plumbing" fails on IW_HOME. This is #556, which fails at baseline. Every unit test passes.

🤖 Generated with Claude Code

https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP

## Slice 11 — Tell the projects apart (`dashboard-control-plane`) **Goal.** Michal reads an act on the board and knows which project it is in without reading the card, and narrows the board to one project when he wants to work that project down. **Fit test.** On his own server at `localhost:9876`, he works the board down and acts only from what it shows: nothing he knows about is missing, nothing foreign is present, and "which project is this act in?" is answered by looking. **Fit: passed** — called by Michal on his own board on 2026-09-16. Slices 01–10 of this effort were already shipped and merged; this PR carries slice 11 only. ## What ships - A project's colour is decided by its name alone (`ProjectColour`, 18 hues × 3 shades), so it is the same on every page, after a restart and tomorrow. Only the two derived numbers reach the page. - Act cards are drawn in their project's colour. `Unresolved` stays grey; a worktree with no nameable project gets no colour. - A chip row lists every project with waiting work and its count. A chip chooses its project through the URL (`/?project=<name>`), and the board re-reads that address every 5 s, so the choice survives the refresh. - The `Where the agents are` lane follows the same choice. There the agent's **state** colours the card and the project is a coloured chip inside it — the mirror of an act card. - Page order: chips, then agents, then acts. The chip row sits above both halves because it narrows both. - The projects page now links back to the board. ## Close review Seven review skills (style, testing, security, scala3, composition, architecture, api): **0 critical**. Nine findings fixed in `bb84c347`, among them one shared rule for the colour-to-CSS attributes and one for narrowing, a length bound on `?project=`, escaping and empty-value tests on the route, and a temp-directory leak in a server test. Full report: `project-management/efforts/dashboard-control-plane/slices/11-tell-the-projects-apart/review-slice-2026-09-16.md`. The security review confirmed the card's invariant: nothing a worktree controls reaches a `style` or `class` attribute. ## Lessons and follow-ups - Follow-up filed: #573 — the dashboard server does not record its own pid. - Two lessons land as a new project hook, `.claude/hooks/build-tool-traps.sh` (wired in `.claude/settings.json`), verified firing in a live session: - a variable prefixed onto a daemon-backed build command never reaches the tasks it spawns; - a build tool's `N/N` summary counts build tasks, not tests. ## Parked (flushed to the effort map) - [ ] Decide what `presentation/views` means — pure decision logic is accreting beside the HTML. - [ ] Write down where a pure type belongs (`domain` vs `presentation`). - [ ] One source for the stylesheet's three-shade scale. ## Known red, not from this branch - `test/start.bats` — "the tmux session does not carry iw's internal plumbing" fails on `IW_HOME`. This is #556, which fails at baseline. Every unit test passes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
A project's name decides its colour, so a project keeps the same colour
across pages, restarts and days. Hue alone does not reach far enough:
the eye separates about eighteen hues, and there are more projects than
that. Each hue is drawn at three depths, giving fifty-four colours while
neighbouring hues stay a full twenty degrees apart. Every project
registered today takes a colour of its own.

The name is untrusted text from a directory the dashboard does not own,
so it decides two numbers and only those numbers reach the page.

An unresolved act stays grey - its lane already names it - and a
worktree whose project cannot be named is given no colour rather than an
invented one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Usable increment 1 reached: the board shows the projects apart. Awaiting
Michal's fit read, which needs an approved restart of his own server.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
A chip row above the board names every project with waiting work and how
many acts it holds. A chip is drawn in the same colour its project's
cards are drawn in, so the row is where the name and the colour are read
together.

The chosen project rides in the URL (/?project=<name>), and the board
re-reads that same URL every five seconds. A choice held only in the
page would be erased by the next reading.

The chips are read from the whole board and only the cards are narrowed,
so a choice never removes the way back out of it. A chosen project with
nothing waiting names the quiet project rather than claiming he is done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
The `Where the agents are` cards are drawn in the same colour their
project's act cards are drawn in, so one project reads the same wherever
it appears on the page. A worktree with no nameable project is given no
colour rather than an invented one.

The lane also follows the chosen project. Narrowing the acts while the
lane below kept every project would leave half the page answering a
question he did not ask. The lane counts what it shows, not what the
choice hides, and falls silent when the chosen project has no agent open
in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
The way back to the whole board sat on a background almost identical to
the page's own, so it read as a line of text rather than as the control
that leaves a narrowed board. Every chip now carries a border.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Increment 2 reached: the board narrows to one project. The chip row, the
URL-borne choice and the agent lane following it are all DONE; no OPEN
step remains until Michal reacts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
The board links to the projects, and nothing led back, so the projects
page could be reached but not left. The title now carries a link to the
board. The two sit together at the left of the header, which leaves the
SSH host control where it was at the right.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
The agent lane exists to say what an agent is doing, so the state is what
must be read first there. Taking the project colour onto the card had
overridden the state colour, leaving working, stopped and finished
apart only by a small word.

The card now carries its state again, and the project colour moves to a
chip inside it. This is the mirror of an act card, where the project
colours the card and the state is the chip inside. One project still
reads as one colour on both halves of the page, and a worktree with no
nameable project is still given no colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
What is already moving is what he wants to know before he decides to
pick up more work, so the agent lane now reads first.

The chip row moved out of the board section into the page, as its own
view. The row narrows the agents as well as the acts, so a control left
inside the board section would have sat below one of the two things it
filters. The chips are still read from the whole board, so a choice
never deletes the way back out of it, and a chip's count still says what
choosing it leaves rather than what the current choice left.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Seven reviews, no critical findings. What is fixed here:

One rule for the colour, not three. The act card, the chip inside an
agent card and the chip in the row above each wrote the same shade class
and the same custom property by hand; a rename would have had to happen
in three places at once. ProjectColourStyle writes them.

One rule for narrowing, not two. The agent lane carried its own copy of
the rule that decides what a choice leaves, so the two halves of one
reading could have come to disagree about which projects exist.

A query value is read as a choice by one rule. An empty value is no
choice, and neither is a value longer than a directory name can be: it
was reflected into the page and into the address the board re-reads
every five seconds, growing with every reading.

A comment that promised more than the code delivers. The shade was said
to come from bits the hue did not consume. Eighteen is not a power of
two, so the hue reads all of them; the mix is what leaves the two
independent.

The route test drove a literal address, so the route and the link the
chips emit agreed only by coincidence. It now asserts they agree.

The new server test left its temporary directory behind on every run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
Follow-ups filed: 1 (#573), commented: 0, struck: 0
Lessons: 1 approved, 0 rejected, 1 still pending

Promoted:
- A build daemon holds the environment its own subprocesses see ->
  .claude/hooks/build-daemon-env.sh, a project PreToolUse hook. The
  author proposed skill:iw-cli-ops, which is maintained in dev-docs and
  cannot land in this PR; kanon needs FORGEJO_API_TOKEN, which is unset.
  This repo had no hook mechanism, so the settings file and the script
  are both new. The script is verified by hand against six command
  shapes; its wiring is verified after a session restart.

The walk is not finished: one entry is still _pending_, so the final PR
must not be created yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
workflow: triage lessons for dashboard-control-plane
All checks were successful
CI / changes (pull_request) Successful in 5s
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 11s
CI / compile (pull_request) Successful in 1m27s
CI / lint (pull_request) Successful in 9s
CI / dashboard-build (pull_request) Successful in 1m2s
CI / test (pull_request) Successful in 14m25s
1d96535359
Follow-ups filed: 1 (#573), commented: 0, struck: 0
Lessons: 2 approved, 0 rejected, 0 still pending

Promoted:
- A build daemon holds the environment its own subprocesses see ->
  .claude/hooks/build-tool-traps.sh
- A build tool's progress counter is not a count of tests ->
  .claude/hooks/build-tool-traps.sh, second rule

Both authors proposed skill:iw-cli-ops, which is maintained in dev-docs
and cannot land in this PR; kanon needs FORGEJO_API_TOKEN, which is
unset. This repository had no hook mechanism, so the settings file and
the script are both new. The script is verified by hand against eight
command shapes and fails open on anything it cannot parse; its wiring is
verified after a session restart.

Three items from the close review are parked rather than fixed: what
presentation/views means, where a pure type belongs, and one source for
the stylesheet's shade scale. All three are decisions to write down, not
refactors to make during a close.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
workflow(sl): close dashboard-control-plane/11-tell-the-projects-apart (flush parked items)
All checks were successful
CI / changes (pull_request) Successful in 3s
CI / contract (pull_request) Has been skipped
CI / format (pull_request) Successful in 10s
CI / lint (pull_request) Successful in 9s
CI / compile (pull_request) Successful in 1m23s
CI / dashboard-build (pull_request) Successful in 1m4s
CI / test (pull_request) Successful in 14m3s
071438740f
Fit passed on Michal's own board. The slice's map item is marked done
with its outcome, its new parked items are flushed to the map's Parked
section (those the map already carried are left alone), the tracker is
closed, and the review-state points at PR #575.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YzFVVxfMSRMHwR7qfFRxP
mph merged commit 83ac0984d3 into main 2026-09-17 08:10:44 +00:00
Sign in to join this conversation.
No description provided.