Pre-Round Briefings
Shows every persisted "Pre-Round Intelligence Panel" PreRoundBriefing —
opponent scouting, judge tendencies, head-to-head record, and team prep
notes combined into one briefing per round — with a "Clear" action per
round, and a form to create a new one by hand.
- Route:
/briefings - Nav: the Tools page's Prep & Practice group; the Reason Editor's
Workspace menu (
t briefingsin Ctrl/Cmd-Shift-Space's command palette) - Package:
debate-round
What it shows
A "create briefing" form sits above the list: round ID, tournament,
division, round label, side (Aff/Neg), an optional room and opponent
label, an optional pick from every already-persisted Opponent Team
Profile / Judge Profile, and free-text team prep notes (one per line).
Saving validates the required fields inline and, on success, composes and
persists a new PreRoundBriefingRecord and clears the form (keeping the
tournament/division filled in, for entering several rounds of the same
tournament in a row).
Each persisted PreRoundBriefingRecord (keyed by roundId) renders as its
own card below the form, sorted by roundId for a stable order. A card's
header shows the round's event line (tournament, division, round label) and
a badge with the prior head-to-head record against that opponent. Below the
header, every briefing section (Event, Opponent scouting, Prior meetings,
Judge tendencies, Team prep notes) renders as its own labeled block, showing
"no data on file" for whichever pieces the briefing wasn't built with.
A second "Log a round" form sits below the "create briefing" form: this
team's own tournament, date, division, side, opponent (picked from an
already-persisted Opponent Team Profile), and result (Won/Lost). Saving
appends a record to a persisted own-round-history store; every logged round
lists below the form with a "Remove" action. When a briefing is created (or
re-generated via buildPreRoundBriefingFromStores) with a matching
opponentTeamId, the briefing's "Prior meetings" section and badge now show
the real head-to-head record built from these logged rounds, instead of
always reading "No recorded prior meetings."
Data flow
Creating a briefing:
panels/PreRoundBriefingsPanel.tsx (form state)
→ buildPreRoundBriefingRecordFromDraft(draft) — state/preRoundBriefings.ts
→ buildPreRoundBriefingFromStores(...) — round/pre-round-briefing.ts
resolves opponentTeamId/judgeId against the persisted
opponentTeamProfiles.ts / judgeProfiles.ts stores by id, and
resolves ownRecords against the persisted own-round-history
history via getOwnRoundHistoryAgainst(opponentTeamId)
→ savePreRoundBriefing(record) — state/preRoundBriefings.ts
→ panel re-reads buildPreRoundBriefingsPanelView() to refresh
state/preRoundBriefings.ts (localStorage: preRoundBriefings)
→ buildPreRoundBriefingsPanelView() — sorts every persisted
PreRoundBriefingRecord by roundId
→ panels/PreRoundBriefingsPanel.tsx — renders it, one card per round
→ apps/debate-ai.com/app/briefings/page.tsx — mounts the panel as a route
Clearing a round's briefing:
panels/PreRoundBriefingsPanel.tsx
→ deletePreRoundBriefing(roundId) — state/preRoundBriefings.ts
→ panel re-reads buildPreRoundBriefingsPanelView() to refresh
Logging this team's own round history:
panels/PreRoundBriefingsPanel.tsx ("Log a round" form state)
→ saveOwnRoundHistoryRecord(record) — state/ownRoundHistory.ts
(localStorage: ownRoundHistory)
→ panel re-reads listOwnRoundHistory() to refresh the logged-rounds list
→ round/pre-round-briefing.ts's buildPreRoundBriefingFromStores reads it
back via getOwnRoundHistoryAgainst(opponentTeamId) the next time a
briefing is created/generated for that opponent
Removing a logged round:
panels/PreRoundBriefingsPanel.tsx
→ deleteOwnRoundHistoryRecord(id) — state/ownRoundHistory.ts
→ panel re-reads listOwnRoundHistory() to refreshEvery briefing-composition and persistence rule already existed and was
Vitest-covered; the panel UI closed follow-up (b), "a briefing panel UI
that renders it on a round-information page," named under idea #12
("Pre-Round Intelligence Panel") in TODO.md. The "create briefing" form
closes this doc's own previously-listed "no affordance … to generate a new
briefing" gap, adding one new pure, testable helper —
buildPreRoundBriefingRecordFromDraft in state/preRoundBriefings.ts —
that validates the draft and delegates to the existing
buildPreRoundBriefingFromStores rather than introducing new
briefing-composition logic. Vitest-covered in
packages/debate-round/test/preRoundBriefings.test.ts.
The "Log a round"/own-round-history store closes this doc's own
previously-listed "Prior meetings" gap below: a new
state/ownRoundHistory.ts persists OpponentRoundRecords logged from this
team's own perspective (an id-per-round, append-only store mirroring
debate-data-sync's tournamentResults.ts convention), and
round/pre-round-briefing.ts's buildPreRoundBriefingFromStores now
resolves ownRecords from it by opponentTeamId — the same
store-resolution pattern it already used for opponentProfile/
judgeProfile — introducing no new head-to-head computation logic (it
still delegates to the existing getHeadToHeadRecords/
summarizePriorMeetings). Vitest-covered in
packages/debate-round/test/ownRoundHistory.test.ts and the new
buildPreRoundBriefingFromStores cases in
packages/debate-round/test/pre-round-briefing.test.ts.
Download a briefing
Closes idea #12's "a print/export view of a briefing for offline use before a round" follow-up: each round card now has a "Download" action next to "Clear" that saves the briefing as a plain-text file, for reading offline (no network/app needed) before or during a round.
panels/PreRoundBriefingsPanel.tsx — "Download" button
→ buildPreRoundBriefingText(briefing, roundId) — round/pre-round-briefing.ts
(prepends a "Pre-Round Briefing — Round <id>" header line when a
roundId is passed, then every section as already rendered on-page)
→ Blob + anchor download, named via preRoundBriefingFilename(roundId)
— round/pre-round-briefing.ts
(same anchor+Blob
pattern
VulnerabilityChartsPanel.tsx's
"Download report" and
AiVersusRoundPanel.tsx's
"Download transcript"
already use)buildPreRoundBriefingText already existed (fully Vitest-covered) but was
never called from any panel — this is the first thing to actually invoke
it. Its roundId parameter is optional and additive, so the one existing
call site (the Vitest suite) is unaffected. No new download-mechanics code
was introduced; this reuses the exact anchor+Blob pattern already
established by every prior "export/download" follow-up in this repo rather
than adding a window.print()-based print view, since a plain-text
download is what every other completed export follow-up in this codebase
has settled on. Vitest-covered in
packages/debate-round/test/pre-round-briefing.test.ts's
buildPreRoundBriefingText/preRoundBriefingFilename suites (round-header
presence, filename sanitization/collapsing/trimming/fallback).
Freshness indicator
Closes idea #12's "a 'last updated' freshness indicator so a stale briefing is obvious" follow-up: every round card now shows how long it's been since its briefing was last saved, and flags it once that gets too old to trust — pre-round facts like room assignments, lineups, or scouting notes can shift within a single tournament day.
state/preRoundBriefings.ts#savePreRoundBriefing(record, now = Date.now())
→ stamps record.updatedAt = now on every save (ignoring any updatedAt
already on the passed-in record), whether creating or overwriting
round/pre-round-briefing.ts
→ getBriefingAgeHours(updatedAt, now) — whole hours since last save,
undefined when updatedAt isn't set
→ isBriefingStale(updatedAt, now, thresholdHours = STALE_BRIEFING_THRESHOLD_HOURS)
— true once age ≥ thresholdHours (24h)
panels/PreRoundBriefingsPanel.tsx
→ renders a badge next to each round's prior-meetings badge:
outline while fresh, destructive once isBriefingStale reports true,
hidden entirely when ageHours is undefined (a record persisted before
updatedAt existed)updatedAt is optional on PreRoundBriefingRecord so a briefing saved
before this field existed still deserializes — getBriefingAgeHours/
isBriefingStale simply report no age for it, and the panel hides the
badge rather than showing a wrong or made-up age. This mirrors
debate-research-evidence's Peer Review System review-aging indicator
(peer-review.ts#getReviewAgeDays/isReviewStale,
ReviewQueuePanel.tsx's age badge) almost exactly, down to the
now-injectable pure functions and the undefined-age-hides-the-badge
guard. Vitest-covered in packages/debate-round/test/pre-round-briefing.test.ts's
getBriefingAgeHours/isBriefingStale suites (undefined-updatedAt,
hour flooring, a future updatedAt clamping to zero, the exact-threshold
boundary, and a custom threshold) and
packages/debate-round/test/preRoundBriefings.test.ts's updated
savePreRoundBriefing suite (the default-Date.now() stamp, an injected
now, and a caller-supplied updatedAt being overwritten rather than
respected).
Manual pairing/room assignments
Closes idea #12's "A manual pairing/room-assignment entry form as the
practical stand-in" follow-up, since real Tabroom pairings stay blocked
behind a login wall (tourn/results/postings pages 302-redirect to
/user/login/login.mhtml for automated requests — see TODO.md's
"Confirmed blocker: Tabroom results/pairings/ballot data" note). A
"Pairing schedule" section sits between the "create briefing" form and the
"Log a round" form: round ID, tournament, division, round label, side
(Aff/Neg), and optional room/opponent-label/judge-name text fields —
mirroring RoundEventInfo's shape, plus a free-text judge name (a pairing
sheet lists a judge by name, not by an already-persisted JudgeProfile
id). Saving validates the same required fields as the briefing form and
upserts a RoundPairingRecord keyed by roundId, so re-logging a round
whose room changed overwrites rather than duplicates it.
Unlike the rest of this feature, pairings are account-synced: signed in,
hooks/useRoundPairings.ts merges local and remote pairings by roundId
(filling gaps only, same as useWordCountRounds) and best-effort pushes
local saves/deletes to the account, backed by a new saved_round_pairings
D1 table (drizzle/0021_nostalgic_northstar.sql) and /api/round-pairings
routes — so a team's pairing schedule for a tournament follows a signed-in
user across devices.
Each saved pairing in the list has a "Use for briefing" action that prefills the "create briefing" form's round ID/tournament/division/round label/side/room/opponent-label fields from it, so the same information doesn't have to be typed twice when writing up a full briefing for that round.
Logging a pairing:
panels/PreRoundBriefingsPanel.tsx (pairing form state)
→ buildRoundPairingRecordFromDraft(draft) — state/roundPairings.ts
→ savePairing(record) — hooks/useRoundPairings.ts
→ saveRoundPairing(record) — state/roundPairings.ts
(localStorage: roundPairings)
→ when signed in: saveRoundPairingToAccount(record) (best-effort)
— round/round-pairings-client.ts
→ PUT /api/round-pairings/:pairingId
→ saved_round_pairings (D1)
→ panel re-renders from the hook's pairings state
Using a pairing to prefill a briefing:
panels/PreRoundBriefingsPanel.tsx
→ handleUsePairingForBriefing(pairing) copies the pairing's fields into
the "create briefing" form's draft state (no store changes)
Removing a pairing:
panels/PreRoundBriefingsPanel.tsx
→ deletePairing(roundId) — hooks/useRoundPairings.ts
→ deleteRoundPairing(roundId) — state/roundPairings.ts
→ when signed in: deleteSavedRoundPairingFromAccount(roundId) (best-effort)Vitest-covered in packages/debate-round/test/roundPairings.test.ts (the
pure store and draft validation), test/savedRoundPairings.test.ts (the
structural validator shared by the API routes and the hook), and
test/round-pairings-client.test.ts (the fetch wrapper, mocking fetch).
hooks/useRoundPairings.ts itself is untested, matching this repo's
existing convention for account-synced, localStorage-backed hooks (e.g.
useWordCountRounds, useCounselPanelAssessments).
Receiving a Scout-to-Strategy export
Scout-to-Strategy's Send to Pre-Round Briefing action lets a team append a strategy recommendation's one-line summary (recommended case plus overall risk level) as a new "Team prep notes" bullet on an already-saved briefing here — see that doc's "Exporting a recommendation into a Pre-Round Briefing" section. It only targets a briefing already created via the form above (there's no round event info to compose a fresh one from a matchup id alone), so create the briefing here first if it doesn't exist yet.
Cross-tab live update
PreRoundBriefingsPanel previously read buildPreRoundBriefingsPanelView,
listOwnRoundHistory, and (via useRoundPairings) buildRoundPairingsPanelView
on mount only, so a briefing saved/cleared, a round logged/removed, or a
pairing saved/removed in another browser tab left the panel showing a stale
view until something else forced a re-render. The panel (for the first two)
and useRoundPairings (for the third) now subscribe to the browser's
storage event, which the spec fires only in other same-origin
tabs/windows, never the one that made the write. A pure helper,
flow/live-update.ts's isPreRoundBriefingsPanelLiveUpdateStorageEvent,
checks whether the event's key is one of state/preRoundBriefings.ts's
"preRoundBriefings", state/ownRoundHistory.ts's "ownRoundHistory",
state/roundPairings.ts's "roundPairings", or null (a
localStorage.clear()); when it is, the panel re-reads whichever of
buildPreRoundBriefingsPanelView()/listOwnRoundHistory() the changed key
(or a null key) matches, and useRoundPairings re-reads
buildRoundPairingsPanelView(). This closes the matching entry in
shared-flow-sync.md's Known gap: "every other
localStorage-backed panel in this repo still has no cross-tab live-update
mechanism." Vitest-covered in packages/debate-round/test/live-update.test.ts
(every backing key, the null-key clear-all case, and unrelated/substring-
matching keys). The panel's and useRoundPairings's own storage-listener
wiring remain intentionally untested, matching every other panel/hook in
this repo whose wiring is exercised only through the shared pure predicate's
own tests.
Known gaps
- Saved briefings sync to a signed-in user's account as the
preRoundBriefingscollection (see Tool Data Sync); a signed-out browser still keeps them inlocalStorageonly. - No real data source for tournament results, event details, or ballots —
a briefing's opponent-scouting/judge-tendency data still has to be
entered by hand (via the Opponent Team Profiles / Judge Profiles stores)
or supplied by a caller of
buildPreRoundBriefing/buildPreRoundBriefingFromStoresdirectly, and own round history is entered by hand via the "Log a round" form. Pairings/room assignments now have a dedicated manual-entry form (see above) as the practical stand-in for the still-blocked live Tabroom data.
