fix(planning): "Append to beat notes" replaced them instead (#454) #455
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/454-beat-notes-append"
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?
Closes #454 (HIGH — active data loss).
The defect
PrepResult.handleAppendcomposed its append fromsessions.find(...)?.beat_notes, read off the campaign session list — which has never carried that field.currentwas therefore always"", so the "Append" button wrote the generated prep sheet alone, over whatever the GM had written, and then reported "Appended to beat notes."A GM with an evening of hand-written prep who generates a sheet and clicks Append loses all of it and is told the opposite. There is no undo and no version history for
beat_notes.The same root cause left
BeatPlanner's editor opening blank regardless of what was stored, so typing into that empty box and saving replaced the notes too.Why it could not simply be read
beat_notesappeared in no read schema anywhere — the column was writable and never readable. That is why both call sites invented an empty string: there was no value to fetch.It is now on
SessionResponse, gated onis_gmexactly astranscriptalready is. That gate is load-bearing:GET /api/sessions/{id}is member-readable, and beat notes are the GM's plan for the evening — a player who can read them has been handed the session's surprises.Putting
beat_notesonSessionListItemwas the smaller change and removes a round trip. Rejected: that list is served to every campaign member with no per-field gating, so it would have published the GM's plan to the table. A test pins that decision, so if someone later adds it there for convenience it fails rather than quietly leaking.I checked every endpoint with
response_model=SessionResponsebefore doing this — all of them areget_session_for_gmorrequire_gmexcept the one member-readable detail endpoint, which is the one that applies the gate.What changed
Both call sites read the real value. A failed read aborts the append with the error rather than composing against
""— a button labelled Append must never be able to replace.BeatPlannerdisables its editor and Save while the notes are loading, for the same reason. Replace still replaces, which is what its label says.Why this survived
There was already a green test for it:
It handed
PrepResulta sessions array withbeat_noteson it and asserted the composition. The composition logic was correct — the test simply fabricated a source shape that has never existed, so it passed while the feature was broken in production. This is exactly the pattern #441 ("Audit the test suite for tests that cannot fail") exists to hunt, and worth remembering as a concrete example when that work starts: the test was not weak, it was testing against invented input.Rewritten against
fetchSession, plus a new test asserting a failed read writes nothing at all.Verification
Mutation-checked: dropping the
is_gmgate fails the player-visibility test.1,453 backend tests pass (was 1,450), 454 frontend (was 452). Lint clean at pinned ruff 0.4.4; eslint clean across
src/apart from one pre-existing warning in a file this PR does not touch.Also in here
Corrects a stale comment I left in
CampaignPlanning.jsxduring #403 — it claimed the session list does not exposeupdated_atand that it is fetched per selection. True of an earlier draft, not of what shipped.🤖 Generated with Claude Code
4dfe2730b6dd425a9b96