Lane 3: This week, the sessions list, and the clock D17 actually decided (#729) #767
Loading…
Reference in a new issue
No description provided.
Delete branch "build/lane3-thisweek"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Lane 3's second UI PR: CAMP-1 This week and CAMP-2 Sessions list — 47 coverage rows — plus a correction to the clock that reaches back into lane 1.
The clock, which is the part to read
D17 says one time, always the reader's own, carrying its zone abbreviation, with a line reconciling to the table's day only when the days differ. Its stated reason: the abbreviation is what tells a traveller their device has moved.
Lane 1 shipped the opposite — Home renders a session's time in the campaign's zone, under a comment saying "that is the night the table agreed on". That comment reads like a rule and is not one; it is a rationale written during the build, and it contradicts a decision already made, for a reason that directly answers it.
I then propagated it: in #762 I moved Needs you onto the campaign's zone so it would agree with the card beside it, and described that to the owner as following an existing rule. The disagreement was real; I resolved it in the direction D17 rules out.
This PR's delegate built This week the same wrong way, because my brief said to — and flagged that the CAMP-1 drawing disagreed, which is how it surfaced. The owner has confirmed D17 stands.
So, in one helper that this PR had already consolidated:
whenText(iso)formats in the reader's own zone with its abbreviation, and no longer takes a zone parameter at all — it had one, all four callers passed the campaign's, and a parameter that can reinstate a retired decision is a parameter that will.Home.jsx's old rationale is replaced by one citing D17. No trace of "the night the table agreed on" remains in the tree./api/me/attention'stimezonefrom #762 stays — it is what the reconciliation is drawn from. Only its use changed.Three tests were asserting the old direction, one of them with it in the title; all are rewritten, and the file now pins the reader to London against the fiction's Chicago table so the two can never accidentally be the same answer.
CAMP-1 and CAMP-2
/campaigns/:idis This week now, answering the three questions the drawing's caption names, off the singlethis-weekendpoint #765 added./campaigns/:id/sessionsis the list, with its three tabs and six row states.The old
CampaignDetail.jsxmoves to/campaigns/:id/manage— "Members & settings" in the Contents — because paths are contracts and the campaign's landing role had to move. It keeps members, characters, invites, the settings form, the new-session form andSeriesManager, each named in its header for the PR that takes it. That Contents entry is deliberately temporary and says so invocabulary.js; the alternative was no door at all to two unbuilt surfaces.The first-run checklist moves onto This week and keeps its old dismissal key, so a GM who dismissed it does not meet it again. This week also draws the three campaign-level banners (archived, no GM, Discord failing) that would otherwise have left the landing page when the old one moved.
Not built as drawn
IS-205) and "Nudge on Discord" — neither a safety registry nor a nudge endpoint exists anywhere. D36: a target appears only when it can work, so the controls are absent rather than dead.SessionResponsehas nocancelled_reason.CA-095/CA-096/CA-098re-homed rather than rebuilt — D16 moved search to the app bar, which lane 1 already built, so the list's filter has no minimum, no button and no server call.What the render found
SectionBoundary, since the tab counts are computed from them, so a bad response took the whole page down. Guarded at both read sites. Reading the code I had assumed the boundary covered it.Bannerspads itself to the frame's gutter because it is chrome, which is wrong inside a document.Two things for whoever writes the next zone test
A zone's abbreviation is locale data, not a constant.
Europe/Londonrenders as "GMT+1" under en-US CLDR — which is what the CI image runs — and as "BST" under en-GB. The assertions are therefore that a zone marker is present and is not the table's, with the reason in a comment.TZ=…does not reach a headless browser on Windows; it reads the OS zone, so the first render silently showed the machine's own zone and looked like the flip had failed. Move the table instead.Left for lane 4, deliberately
utils/datetime.js'sviewerSecondaryTimeisIS-003— the campaign-zone-plus-viewer-zone pair D17 retires. Its only callers areSessionDetail.jsxandVotingGrid.jsx, both of which lane 4 replaces, and it should be deleted there rather than converted. Noted on #730 so that lane starts from a list rather than a surprise.Checks
lint 0 errors (234 warnings, all pre-existing in pages no lane has rebuilt; none in this PR's files, held at error). 1457 frontend tests in 124 files. Build clean. Both pages served from the built
distand looked at, desktop and ~500px, both themes, with the reader in and out of the table's zone.🤖 Generated with Claude Code
The two pages a campaign is actually used through, rebuilt in the new shell. **This week (CAMP-1) is what `/campaigns/:id` is now.** Paths are contracts (build plan §2.4), so the address every bookmark and every link already points at still opens the campaign; what is read there is what changed. It answers three questions in the order people ask them — when, who is coming, and what still needs doing — out of the one read the server PR added, and it does not work around what that read deliberately leaves out. The tally, the member count and your own answer are the roster read one way rather than fields, because two copies of a fact can disagree and one cannot. Whether the bot is recording is `/live`'s, asked for on its own and only while a session is actually under way, which is the only time the answer changes anything. The campaign's own name, system and description are `GET /api/campaigns/{id}`, which the page loads anyway. Four of its decisions are not layout. **One clock:** the time is printed once, in the campaign's zone with the zone named — the same string Home's cards print, from the same helper, because a card reading "Mon, 8:00 PM CDT" beside a page reading "Tue, 2:00 AM BST" is two clocks and D17 exists to stop that. The line reconciling it to the reader's own appears only when the two fall on different days. **Not answering is not saying no:** silence gets a grey disc, a grey dot and the words "No answer", because a GM chasing an answer has to be able to tell the two apart. **A player's pronouns are not their character's:** the bold name on a roster row is the character for a player and the person for whoever runs the table, and the pronouns follow whichever noun is on the line. And **the page changes its mind once the session starts** — the answer buttons, the roster, what is still needed and the calendar all go, since none of them is any use after eight o'clock. It has three shapes rather than one: a campaign with nothing scheduled becomes the four-item getting-started checklist and says what the page will become without naming a Monday it has never had; an archived campaign keeps its page, reading what happened last instead of what happens next, and says who can bring it back; and everything else is the session. **The sessions list (CAMP-2) is a page of its own**, at `/campaigns/:id/sessions`. Three tabs whose counts agree with the list under them, a row per status, the GM's actions on the row rather than behind a hover because a phone has no hover, and the merge-notes modal. It keeps a **filter** — narrowing the list already on screen — and searching the campaign stays the app bar's, which is D16 and #593's one-box rule. There is no session numbering (#760): a row is its title, and one without a title says so rather than being given an ordinal the product does not have. **What was not replaced still works.** `CampaignDetail` keeps members and characters, the nine settings sections, the new-session form and the milestone timeline — four surfaces lane 3 has not reached — at `/campaigns/:id/manage`, in the Contents as "Members & settings" until their own PRs split it up. The doors the departing pages still need are `?open=timezone|invite|discord|session` (with `?reuse=` for a cancelled session's title and description), so a checklist item still lands on the control that completes it rather than near it. That file is deleted, not refactored, when the last of the four is built. Four things the drawing has are not built, each because nothing behind it exists and D36 says a target is drawn only when it can work: "Nudge on Discord" (no endpoint), the Table rules & safety card and its "Flag a concern" (no safety registry at all — D2 is unbuilt on both sides), "four at the table" on the live header (presence is Table mode's, lane 5), and the "Prep this session" card, whose content is the GM's own line about the session and which without it is a second button to where "Prep sheet in progress · Continue" already goes. `FirstRunChecklist.jsx` is gone; This week is where the checklist lives now, on the same per-campaign localStorage key, so a GM who dismissed it before this does not meet it again. Home's four time helpers move to `utils/when.js` unchanged, so the pages that print a session's time all print the same string. Two things found by looking at the built pages rather than at the source: - **The sessions page did not draw at all** against a list response it did not expect. The rows are read outside every section boundary — the tab counts and the lead are computed from them — so a body that is not the list this asked for now becomes an empty list rather than a page that will not draw. - **"in 5 days" under the time was the same sentence twice.** The countdown belongs beside the chip, where the drawing has it; under the one time it read as a second reading of a fact already on the line above. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>D17 settled this and both sides of it were built backwards. It supersedes `IS-003` — which asked for the campaign's zone and the reader's side by side — with **one time, always the reader's own, always carrying its timezone abbreviation**, and a line reconciling it to the table's only when the reader's *day* differs. Lane 1 shipped the campaign's zone instead, with a comment explaining why ("that is the night the table agreed on") that reads like a rule and is not one: it is a rationale written while building, against a decision already made for a stated reason. #762 then moved Home's Needs you rows onto the campaign's zone so a row and the card beside it would agree. The disagreement was real; it was resolved in the direction D17 rules out. So the direction flips, in the one place lane 3 had just consolidated it. `whenText` takes the reader's clock and **no zone parameter at all**. A parameter that can reinstate a retired decision is a parameter that will, and this one had four callers all passing the campaign's. The campaign's zone is still on every payload carrying a session time, and is still needed — to be reconciled *to*. Two new helpers read it: `tableDay` for the day the table calls it, and `zoneShift` for how far off the reader is, computed from the two wall-clock readings of that one instant rather than from the zones in general, because one of them may be on summer time and the other not. The reconciling line is D17's own sentence — "The table calls this **Monday's** session — you are 6 hours ahead" — and it still appears only when the days differ. Two zones three hours apart on the same date have nothing to reconcile, and a line saying so on every visit is furniture on the most-opened page in the app. Everything that prints a session's time flips with the helper: Home's campaign cards, Home's Needs you rows (including the live row's "started 8:14 PM", which gains the abbreviation it was the one exception to), This week's hero and live header, and the sessions list's rows. Nothing needed special-casing — every consumer is "a session's time", which is exactly what D17 is about. `utils/datetime.js` is **not** flipped. `formatSessionTime` and `viewerSecondaryTime` are the old pages' pair — the latter *is* `IS-003`, the side-by-side rendering D17 retires — and their only callers are `SessionDetail` and `VotingGrid`, which lane 4 replaces. Changing them here would edit a page this PR does not own, to a rule its own lane has to apply anyway. The sessions list no longer reads `GET /api/campaigns/{id}` at all: the zone was the only thing it ever wanted from it. One fewer request per visit, and one fewer thing that can fail. Three tests were asserting the old direction and now assert D17: - `Home.test.jsx` "renders a Needs you row's time in the campaign's zone, not the reader's (#727)" — the name said it. It is the reader's zone now, and the file pins `TZ` to London against the fiction's Chicago table so the two are never accidentally the same answer. - `ThisWeek.test.jsx` "prints one time, in the campaign's zone" and "reconciles it only when the reader's day is not the table's", which asserted the "Where you are, that is…" wording this replaces. - `Sessions.test.jsx`'s row-time assertion held by luck — its reader was already in the table's zone — and its comment said the campaign's. Added: the reader's own clock when the two zones differ; the reconciling line's wording and its offset; **its absence when the day is the same** (two zones, one date); and a campaign with no zone set, which still draws the time and draws no half-written line. One thing the tests had to be taught: **a zone's abbreviation is locale data, not a constant.** `Europe/London` renders as "BST" where CLDR has a short name for it and "GMT+1" where it does not — en-GB does, en-US does not, and the CI image runs en-US. Both say the same thing to a traveller, which is the whole point of printing it, so the assertions are on the marker being there and not being the table's rather than on either string. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>2e9f452cbf50f3c981c6