Lane 3: the new-session form and the recurring series (#729, CAMP-3, CAMP-4) #774

Merged
claude-bot merged 4 commits from build/lane3-sessionform into main 2026-09-23 10:19:20 +00:00
Collaborator

The thing a GM does more than anything else, and the rule that saves them doing it.

How this PR was built, because it matters for how you read it

Two delegates died mid-task on the same API safeguard, both times on the model's own reasoning and never on anything in the code. Neither produced a report. The work was preserved as it stood — twice — and I verified it myself rather than receiving a row-by-row account. So: the checks below are mine and were actually run, but there is no delegate narrative behind them, and the review is correspondingly the code's rather than a summary's. The two WIP commits are in the history and say exactly that.

A page, not a dialog

CA-123 is classified replaced for this reason: the drawing opens New session as a page of its own, and Cancel there is the way back.

  • /campaigns/:id/sessions/new — the form.
  • /sessions/:id/edit — the same form editing one (SE-011, SE-012). Its own address rather than the campaign's, because a session is reached from its own page.

Neither is a Contents entry; both are reached from the list, This week's checklist, and a session.

Two clock rules that are not the same rule

This is the part to read, and the build got it right — including a step past the brief.

D17 governs displaying an instant: whenText formats in the reader's own zone with its abbreviation and takes no zone parameter. CA-137 is not that. A recurrence rule is a wall clock in a named zone — "8:00 PM, in the campaign's time, CDT, whatever the clocks do" — and putting it through the reader's zone would turn a schedule into a timestamp and make the series drift an hour twice a year.

components/seriesText.js keeps the two apart by construction, and its file note says so. Two details worth calling out:

  • The abbreviation printed is the campaign zone's at the series' first occurrence, not "now" — because CDT and CST are the same rule in different months.
  • A campaign with no timezone prints no abbreviation at all. zoneShortLabel falls back to the reader's zone, so a rule in no named zone would otherwise render "8:00 PM BST" and assert something untrue of it — which is the exact confusion CA-137 exists to prevent. That one is not in the drawing; it is a consequence of the drawing taken seriously.

The sessions a series makes are instants, and those go through whenText like everything else.

The series moves to where its sessions are

IS-368 puts "Manage the series" on the Sessions list and under the title of any session it made, so SeriesManager is removed from the old campaign page — it is not a section anybody goes looking for, it is the reason the next two evenings are already on the board. That is the one edit to CampaignDetail.jsx in this PR, and it is a deletion.

The read it needs is GET /api/series/{id}/sessions, added in #765 precisely for this: CA-154's "Change which sessions?" turns on which occurrences were individually rescheduled, because update_series's future_all will not touch those.

No session numbering

#760 is not this PR. Nothing here invents a number: where the drawing reads "Session 15", the build reads the session's title. grep for session_number in the new files returns nothing.

Checks

  • Frontend 1700 tests in 130 files, green, on the tree with main merged in.
  • eslint src 0 errors, 227 warnings — seven fewer than main's 234, from the old page's deleted section.
  • vite build green.
  • coverage/lane2.py → {'drawn': 99, 'replaced': 7, 'deferred': 1, 'elsewhere': 11} total 118 — unclassified: none.
  • coverage/lane2c.py → {'drawn': 65, 'elsewhere': 27, 'replaced': 10, 'behaviour': 5} total 107 — unclassified: none.
  • Neither script's classifications changed.

A render check is running separately and its result will be added as a comment. The CSS carries evidence that one was done during the build — .qb-series__under-title .nb's white-space: nowrap exists because the separators were seen stranding at line ends and "Manage the series" coming apart across two — but I have not personally looked at these pages, and I would rather say so than imply otherwise. Each .nb is a short phrase, so the line wraps between phrases and never inside one; that is by inspection, not by eye.

🤖 Generated with Claude Code

The thing a GM does more than anything else, and the rule that saves them doing it. ## How this PR was built, because it matters for how you read it **Two delegates died mid-task on the same API safeguard**, both times on the model's own reasoning and never on anything in the code. Neither produced a report. The work was preserved as it stood — twice — and I verified it myself rather than receiving a row-by-row account. So: the checks below are mine and were actually run, but there is no delegate narrative behind them, and the review is correspondingly the code's rather than a summary's. The two WIP commits are in the history and say exactly that. ## A page, not a dialog `CA-123` is classified *replaced* for this reason: the drawing opens New session as a page of its own, and Cancel there is the way back. - `/campaigns/:id/sessions/new` — the form. - `/sessions/:id/edit` — the same form editing one (`SE-011`, `SE-012`). Its own address rather than the campaign's, because a session is reached from its own page. Neither is a Contents entry; both are reached from the list, This week's checklist, and a session. ## Two clock rules that are not the same rule This is the part to read, and the build got it right — including a step past the brief. **D17** governs displaying an instant: `whenText` formats in the reader's own zone with its abbreviation and takes no zone parameter. **`CA-137` is not that.** A recurrence rule is a wall clock in a named zone — *"8:00 PM, in the campaign's time, CDT, whatever the clocks do"* — and putting it through the reader's zone would turn a schedule into a timestamp and make the series drift an hour twice a year. `components/seriesText.js` keeps the two apart by construction, and its file note says so. Two details worth calling out: - **The abbreviation printed is the campaign zone's at the series' *first occurrence***, not "now" — because CDT and CST are the same rule in different months. - **A campaign with no timezone prints no abbreviation at all.** `zoneShortLabel` falls back to the *reader's* zone, so a rule in no named zone would otherwise render "8:00 PM BST" and assert something untrue of it — which is the exact confusion `CA-137` exists to prevent. That one is not in the drawing; it is a consequence of the drawing taken seriously. The sessions a series *makes* are instants, and those go through `whenText` like everything else. ## The series moves to where its sessions are `IS-368` puts "Manage the series" on the Sessions list and under the title of any session it made, so `SeriesManager` is **removed from the old campaign page** — it is not a section anybody goes looking for, it is the reason the next two evenings are already on the board. That is the one edit to `CampaignDetail.jsx` in this PR, and it is a deletion. The read it needs is `GET /api/series/{id}/sessions`, added in #765 precisely for this: `CA-154`'s "Change which sessions?" turns on which occurrences were individually rescheduled, because `update_series`'s `future_all` will not touch those. ## No session numbering `#760` is not this PR. Nothing here invents a number: where the drawing reads "Session 15", the build reads the session's title. `grep` for `session_number` in the new files returns nothing. ## Checks - Frontend **1700 tests in 130 files**, green, on the tree with main merged in. - `eslint src` **0 errors**, 227 warnings — *seven fewer than main's 234*, from the old page's deleted section. - `vite build` green. - `coverage/lane2.py` → `{'drawn': 99, 'replaced': 7, 'deferred': 1, 'elsewhere': 11} total 118 — unclassified: none`. - `coverage/lane2c.py` → `{'drawn': 65, 'elsewhere': 27, 'replaced': 10, 'behaviour': 5} total 107 — unclassified: none`. - Neither script's classifications changed. **A render check is running separately and its result will be added as a comment.** The CSS carries evidence that one was done during the build — `.qb-series__under-title .nb`'s `white-space: nowrap` exists because the separators were seen stranding at line ends and "Manage the series" coming apart across two — but I have not personally looked at these pages, and I would rather say so than imply otherwise. Each `.nb` is a short phrase, so the line wraps *between* phrases and never inside one; that is by inspection, not by eye. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Partial work preserved after a delegate stopped mid-task on an API error, not
on anything in the code. The pages and helpers exist; the routes, the Contents
entries and every test are still to do, and nothing here has been run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	webapp/frontend/src/shell/useContents.js
WIP(frontend): tests for the session form and the series (#729, lane 3)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m25s
CI / Backend lint (ruff) (pull_request) Successful in 24s
CI / Docker image build (pull_request) Successful in 50s
CI / Bot/backend version sync (pull_request) Successful in 52s
CI / Bot tests and audit (pull_request) Successful in 2m20s
CI / Feedback site tests (pull_request) Successful in 1m28s
CI / Frontend tests, audit, and build (pull_request) Successful in 4m43s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 3m58s
CI / Backend migration, tests, and audit (pull_request) Successful in 10m26s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 18m36s
5a1dfb3012
Second tranche, preserved after the delegate stopped on the same API safeguard
as the first — again nothing to do with the code. Main is merged in, the test
files exist, and a CSS rule plus the whole verification pass are outstanding.
Nothing here has been run.

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

The render check: clean, and measured rather than eyeballed

Sixteen renders of the built app — the new-session form, the edit form, the sessions list with the series card collapsed, and the same list with it expanded (?series=…, which exercises SeriesEdit's whole field set) — each at desktop and phone width, in both themes. All sixteen looked at.

No layout faults. No overflow, no mid-word wrap, no horizontal scroll, nothing overlapping or outside its card, nothing invisible in either theme. .slist__rule ("Ninety Miles of Fog · every other Monday") wraps cleanly onto a second line at 390px without colliding with the chip below it.

Two things about how it was checked matter more than the result.

The phone-width shots this project has been taking are not evidence

Headless Edge on Windows clamps the viewport to a 492px floor — below ~500 it clamps to 492, above it you get request − 24 — and then saves the PNG at the requested width. A --window-size=390 "phone" shot is therefore a crop of a 492px render, and a crop and a real overflow look identical: text truncated at the right edge, "Pencil i[t in]", a search box cut off. That is a manufactured fault, and this run reproduced it before catching it.

The repo's own notes already knew about the clamp and concluded that ~492px was the narrowest honest render available. It isn't. Put the page in an iframe — a wrapper page whose own window is requested comfortably wide, holding <iframe style="width:390px">. The iframe is laid out in CSS pixels by the parent document, so the clamp does not reach it, and the page inside genuinely lays out at 390.

Then prove it from inside the iframe's own document rather than trusting the picture:

[d.clientWidth, d.scrollWidth]   // → [390, 390]

scrollWidth > clientWidth is horizontal overflow, measured. Every page above returns 390 === 390. That is a better test than looking at a screenshot in any case.

A stale build nearly got reported as a pass

The scratchpad already held a dist/ and a set of screenshots from the first delegate's interrupted attempt. Checking their timestamps against the git log showed that build predated the commit that wrote SessionForm.jsx at all — they were pictures of a page that did not yet exist. Discarded and everything rebuilt from 5a1dfb3. Worth saying because "screenshots exist" is not the same as "this code was rendered", and the two are indistinguishable without checking.

Two things deliberately not covered

  • SeriesUnderTitle in its real home. The .nb non-breaking-phrase CSS exists for the 320px left rail on SessionDetail.jsx, which is a large legacy page with ~10 further API dependencies; standing it up was out of proportion to a render check of two other pages. The sibling mechanism — the same ruleLine() text beside a title in the sessions list — was checked and wraps cleanly, and the CSS structurally does what it claims: each phrase is an atomic span, so wrapping only ever happens between phrases, never inside one and never stranding a bare "·". Confident, but not photographed.
  • The "I know the time" + recurring-toggle sub-state, which needs a click; this project's render checks are static loads by URL. The equivalent fields were verified in the series-edit form, which shares the components and the CSS and is the higher-risk surface anyway (795 lines of rewrite against a small inline block).

Nothing under webapp/ was modified by the check; the fixture server and shot helper stayed in the scratchpad.

🤖 Generated with Claude Code

## The render check: clean, and measured rather than eyeballed Sixteen renders of the built app — the new-session form, the edit form, the sessions list with the series card collapsed, and the same list with it expanded (`?series=…`, which exercises `SeriesEdit`'s whole field set) — each at desktop and phone width, in both themes. All sixteen looked at. **No layout faults.** No overflow, no mid-word wrap, no horizontal scroll, nothing overlapping or outside its card, nothing invisible in either theme. `.slist__rule` ("Ninety Miles of Fog · every other Monday") wraps cleanly onto a second line at 390px without colliding with the chip below it. Two things about *how* it was checked matter more than the result. ### The phone-width shots this project has been taking are not evidence Headless Edge on Windows **clamps the viewport to a 492px floor** — below ~500 it clamps to 492, above it you get `request − 24` — and then saves the PNG at the *requested* width. A `--window-size=390` "phone" shot is therefore a **crop of a 492px render**, and a crop and a real overflow look identical: text truncated at the right edge, "Pencil i[t in]", a search box cut off. That is a manufactured fault, and this run reproduced it before catching it. The repo's own notes already knew about the clamp and concluded that ~492px was the narrowest honest render available. **It isn't.** Put the page in an iframe — a wrapper page whose own window is requested comfortably wide, holding `<iframe style="width:390px">`. The iframe is laid out in CSS pixels by the parent document, so the clamp does not reach it, and the page inside genuinely lays out at 390. Then prove it from inside the iframe's own document rather than trusting the picture: ```js [d.clientWidth, d.scrollWidth] // → [390, 390] ``` `scrollWidth > clientWidth` *is* horizontal overflow, measured. Every page above returns `390 === 390`. That is a better test than looking at a screenshot in any case. ### A stale build nearly got reported as a pass The scratchpad already held a `dist/` and a set of screenshots from the first delegate's interrupted attempt. Checking their timestamps against the git log showed that build **predated the commit that wrote `SessionForm.jsx` at all** — they were pictures of a page that did not yet exist. Discarded and everything rebuilt from `5a1dfb3`. Worth saying because "screenshots exist" is not the same as "this code was rendered", and the two are indistinguishable without checking. ### Two things deliberately not covered - **`SeriesUnderTitle` in its real home.** The `.nb` non-breaking-phrase CSS exists for the 320px left rail on `SessionDetail.jsx`, which is a large legacy page with ~10 further API dependencies; standing it up was out of proportion to a render check of two other pages. The *sibling* mechanism — the same `ruleLine()` text beside a title in the sessions list — was checked and wraps cleanly, and the CSS structurally does what it claims: each phrase is an atomic span, so wrapping only ever happens between phrases, never inside one and never stranding a bare "·". Confident, but not photographed. - **The "I know the time" + recurring-toggle sub-state**, which needs a click; this project's render checks are static loads by URL. The equivalent fields were verified in the series-edit form, which shares the components and the CSS and is the higher-risk surface anyway (795 lines of rewrite against a small inline block). Nothing under `webapp/` was modified by the check; the fixture server and shot helper stayed in the scratchpad. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Merge main into build/lane3-sessionform (#729)
All checks were successful
CI / Bot/backend version sync (pull_request) Successful in 56s
CI / Backend lint (ruff) (pull_request) Successful in 59s
CI / Feedback site tests (pull_request) Successful in 1m12s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m31s
CI / Bot tests and audit (pull_request) Successful in 2m31s
CI / Docker image build (pull_request) Successful in 3m42s
CI / Frontend tests, audit, and build (pull_request) Successful in 4m36s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 3m47s
CI / Backend migration, tests, and audit (pull_request) Successful in 10m19s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 12m17s
0d05e53da0
#769 landed Settings' frame underneath this branch. One conflict, in
`CHANGELOG.md`; both entries kept. `App.jsx`, `App.test.jsx`,
`useContents.js` and `AppFrame.test.jsx` all auto-merged.

Full frontend suite run on the merged tree — 1781 tests in 134 files, green.
That step is not a formality on this lane: `AppFrame.test.jsx` has twice now
auto-merged without a conflict and been left asserting something the other
side had just made false.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.