[Frontend] "Append to beat notes" silently replaces them — the session list never carried beat_notes #454
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Severity: HIGH — active data loss, not a latent race.
Found while wiring conditional writes for #403 (PR #450).
GET /api/campaigns/{id}/sessionsreturnsSessionListItem, which has never carriedbeat_notes(webapp/backend/app/schemas/session.py— the model hasid,campaign_id,title,scheduling_mode,status,confirmed_time,end_time,created_at,has_summary, and nowupdated_at). Two frontend call sites readbeat_notesoff those list items anyway, so both silently seeundefined.Symptom 1 — the "Append" button replaces (data loss)
webapp/frontend/src/components/workbenchTools.jsx:542-546, inPrepResult.handleAppend:currentis always"", sonextis always just the newly generated prep sheet. The write replaces whatever the GM had written. The UI then reports "Appended to beat notes."A GM with a session's worth of hand-written prep who generates a prep sheet and clicks Append loses all of it, and is told the opposite. There is no undo and no version history for
beat_notes.handleReplace(same file, ~line 559) is honest about replacing, so it is fine.Symptom 2 — the beat-notes editor always opens empty
webapp/frontend/src/pages/CampaignPlanning.jsx,BeatPlanner: both the initialuseEffectandhandleSessionChangedosetDraft(s?.beat_notes || "")against the same list items, so the textarea opens blank no matter what is stored. Typing into that blank box and saving replaces the stored notes.Less sharp than symptom 1 (the GM sees an empty box rather than being told a false success), but the same root cause and the same outcome.
Why #403's conditional writes do not cover this
expected_updated_atstops a stale write. These writes are not stale — they are current writes built from data that was never fetched. The token matches, the guard passes, and the clobber goes through. Nothing about #403 helps here, which is exactly why it deserves its own issue rather than being folded in.Proposed fix
Read the real value instead of inventing an empty one:
beat_notestoSessionListItem, or have both call sites fetch the session before composing an append. Adding it to the list schema is the smaller change and removes a round trip;beat_notesis GM-only, and the campaign session list is already GM/member-gated, so it exposes nothing new to a player — worth confirming that gating explicitly before doing it, since the list is served to every campaign member and beat notes are GM planning material."". A button labelled "Append" must never be able to replace.beat_notes, click Append, assert the stored value still contains the original text.Also
CampaignPlanning.jsx:251-254carries a comment claiming the session list does not exposeupdated_atand that it is fetched per selection. That was true of an earlier draft of PR #450 and is not true of what shipped — the list does carry it now and it is read directly. Stale comment, worth correcting in the same change.Acceptance criteria
PrepResult.handleAppendcomposes against the session's actual storedbeat_notes, or refuses to run.BeatPlanner's editor opens with the stored beat notes rather than blank.beat_notesis confirmed acceptable if it is added to the list schema.updated_atcomment inCampaignPlanning.jsxis corrected.