Lane 3: the story, the timeline, the lines and the numbers (#729) #766
Loading…
Reference in a new issue
No description provided.
Delete branch "build/lane3-story"
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 first UI PR: CAMP-5 Highlights, CAMP-6 Storyline and CAMP-7 Analytics — 48 coverage rows. Frontend, plus one backend hunk explained below.
D47: Storyline was four things and is now two
The story (chapters, generated, GM-editable) and the timeline — sessions, milestones and wiki events, one line each, filterable. The journal leaves (each session's notes live on the session, D30) and the wiki pieces go to the wiki.
The timeline reads the endpoint #765 added. Three things it gets right because the server told it so: an entry's
idis never treated as a primary key (a generated NPC reappearance isnpc:{id}:{session}),editableis taken from the server rather than recomputed (D36 — a target appears only when it can work), and visibility is not re-filtered, because it is already done by the reader on the server.The file split, which needed care
CampaignStoryline.jsxwas the wiki's front page —/campaigns/:id/wiki, storyline body plus category sidebar, search and suggestions. The build plan says that file splits: Storyline here, the wiki landing in lane 8 (WIKI-1, WIKI-2, WIKI-10).So it was moved, not overwritten —
git mvtoWikiFront.jsx, with only the docstring and export renamed. The wiki landing keeps working at its unchanged address for lane 8 to replace, and Storyline gets the file name and an address of its own. Rewriting in place would have deleted a page nothing replaces yet and broken the Contents' "Front page & search" door.The one backend hunk: D14 was decoration, not a rule
CAMP-7 draws exactly that, and this PR built the page that way. But
GET /api/campaigns/{id}/analyticswasrequire_campaign_memberand returned every member's row to every member — so the whole league table of who turns up and who answers was one look at the network tab away, on a page any member can open.A page that hides a row the server sent is not a rule. The filter is on the endpoint now, with a test for each half so it cannot later be widened to everyone or narrowed onto the GM by accident. The campaign-wide figures stay everybody's — D14 keeps the page for every member and takes away only the table.
That is why a frontend PR has a backend hunk: implementing D14 only in the page would have been implementing it wrongly.
Not built as drawn
CA-239's "3 h 38 m a session, on average" — the endpoint reports no average session length and nothing else on the page can stand in for one. The stat is absent rather than invented; it wants a server field.CA-182's count on the Contents' Highlights entry — would cost the frame a read of every campaign's quotes on every screen.manually_edited_atis not surfaced, though the model's comment suggests it should be — built as drawn, reported rather than invented.sessions_last_12_monthsis not shown here. CAMP-7 draws the 30-day figure; the twelve-month sentence is the public share page's (D89). Worth saying because #765 added the field.Two things only the render found
A short rebuilt page left a band of bare colour under it.
AppFramestill paints the Outlet's wrapper for pages no lane has rebuilt, so the Book's paper stopped where the document did. Lane 1 hit this and wrote the fix down inHome.css; all three pages now claim the height. Not findable by reading the new files — it needed a screenshot of the built page and then a second of/dashboardto prove it was not the shell.A milestone with no mark wore the wrong glyph — defaulted to
storyline, which at row size is nearly the wiki rows' book shape, so a generated NPC reappearance read as a wiki line. The drawing's own map saysmilestone → xp.Also fixed, found by looking for it once the pages were real: Storyline and Analytics read their data once on mount, and the route keeps the component alive when only
:idchanges — so walking between two campaigns' Storylines showed the first campaign's story under the second's name. Both read on the id in the address now, with a test that navigates between two campaigns.Checks
lint 0 errors (238 warnings, all in pages no lane has rebuilt; none in this PR's files, which are held at error). 1467 frontend tests in 125 files. Build clean. Backend 4037 passed, 13 skipped after the D14 filter. All three pages served from the built
distand looked at, desktop and ~500px, both themes.🤖 Generated with Claude Code
CAMP-5, CAMP-6 and CAMP-7 — Highlights, Storyline and Analytics — rebuilt in the new frame, from lane 2c's drawing. **Storyline is D47's reduction, and that is why this touches the wiki.** The page was four things: a generated story, milestones on a rail, a journal of everyone's session notes, and a Wiki tab with a suggested-updates row. It is two: the story, and the timeline. The journal goes — each session keeps its own notes (D30) — and the wiki pieces stay with the wiki, which is the half of `CampaignStoryline.jsx` this lane does *not* own (build plan §4: its wiki landing, search and suggestions are lane 8's WIKI-1/2/10). So the file splits rather than being rewritten over: `WikiFront.jsx` is the old page, unchanged, still at `/campaigns/:id/wiki` — an address in people's history and in the Contents — and `CampaignStoryline.jsx` is now the page its name says it is, at `/campaigns/:id/storyline`, with an entry in the campaign's Contents. Three things about the timeline are the server's answers and are not re-derived here, which is the whole reason `GET /campaigns/{id}/timeline` exists: `editable` decides whether a row carries Edit and Remove — true only for a GM and only for a stored milestone, so a generated NPC reappearance and every wiki line are read-only without this page knowing why (D36); an entry's `id` is a string and sometimes composite, so it is a React key and never a thing to write back to; and visibility is filtered by the reader on the server, so nothing that arrives wants hiding again. The list is fetched once and the four filter buttons narrow it in the browser: they are four views of one answer, so pressing one is instant and cannot fail. Removing a milestone is D12's lightest tier — it happens, and the Undo sits where it happened, writing the row back at the instant it sat at. The story keeps the distinction it is most likely to lose (WK-028): a **missing** story is "No story yet" — in the GM-with-recaps, GM-with-nothing and player wordings — and any other failure is an error, said in words with a way to try again. Writing a chapter and rewriting the whole thing are queued work the page cannot wait on, so each says what it is doing and asks again until the answer changes, rather than telling the reader to refresh. Analytics follows D14: a player reads the campaign-wide numbers and their own line; the table of everyone and the public share link are the GM's. Every rate prints the percentage **and** the fraction it came from, the shape the public share page settled (`PublicAnalytics.jsx`), so the colour band is redundant with text already in the cell (IS-295); the one cell with no rate keeps the drawing's dash with its meaning said in words behind it. Making a new link and turning the link off each break an address somebody may be holding, so each says so first; making the first link breaks nothing and asks nothing. Highlights groups the lines under the session they were said in and dates that group from the **session**, not from the day the quote was kept — a page whose whole job is to say which night a line came from should not print the morning after. The speaker filter's choices are built from the unfiltered answer and kept, so picking a name never shrinks the list of names. No page invents a session number (#760): where the drawing reads "S13" or "after Session 13", the build reads the session's own title. Two things the drawing shows are not built, for want of a server that reports them, and are absent rather than faked: Analytics' "3 h 38 m a session, on average" — `GET /campaigns/{id}/analytics` has no average session length — and the count on the Contents' Highlights entry, which would cost the frame a read of every campaign's quotes on every screen. Each page claims the Book's remaining height (`flex: 1`), the workaround lane 1 wrote down in `Home.css`: the frame still paints the Outlet's wrapper white for the pages it has not rebuilt, and a short document left a band of it underneath. Looking at the built pages is what found that; reading them did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>D14: "a player sees the campaign-wide numbers and their own row only. The GM keeps the full table." CAMP-7 draws it that way and this lane built the page that way — but `GET /{id}/analytics` was `require_campaign_member` and returned every member's row to all of them. A page that hides a row the server sent is a decoration, not a rule. The whole league table of who turns up and who answers was one look at the network tab away, on a page every member can open, which is precisely what D14 says it must not be. So the filter is on the endpoint. The campaign-wide figures stay everybody's — D14 keeps the page for every member and takes away only the table — and both halves have a test, so the filter cannot later be widened to everyone or narrowed onto the GM by accident. Found while building CAMP-7 (the page was already correct; the API was not). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>