Files
ttrpg-initiative-tracker/TODO.md
T
david raistrick 2569cc4497 Builds replayable combat logs with first-class undo/redo, unified verification tooling, fast indexed log queries, and stricter CI.
feat(combat): add first-class undo and redo controls

Add undo and redo controls to the combat UI so the DM can recover from recent
actions without leaving the encounter view.

Undo and redo operate on the current encounter's combat history. Empty stacks
produce clear feedback instead of failing silently. Redo order follows normal
stack behavior after multiple undos.

This makes combat history actionable during play, not just visible in the log.

feat(logs): make combat logs replayable

Replace plain combat log messages with structured combat events that can be
used by the UI, exported as JSON, replayed, and verified.

Each new log entry records the action type, encounter identity, participant
identity, a small action delta, undo intent, and a turn snapshot. Download and
copy now export the event stream as JSON so a saved combat log is useful for
offline analysis and debugging.

Legacy logs remain viewable, but new logs use the structured event format.

feat(logs): make undo and redo transactional

Apply undo and redo as single storage operations so the encounter state and log
state cannot drift apart.

Server storage applies the encounter update and the log undone flag inside one
SQLite transaction. Firebase storage uses a batch write for the same behavior.
The storage contract now includes undo/redo semantics.

This replaces fragile multi-write undo behavior where a failure could update
the encounter without marking the log, or mark the log without updating the
encounter.

feat(combat): add unified replay and verification tool

Add one combat CLI for replaying live combat and verifying combat logs.

Replay drives the live backend through the same shared combat logic used by the
app, writes a JSON event log to an explicit output path, and automatically
verifies the result. Verification checks for DM-visible combat problems such as
skipped turns, double actions, bad round changes, and unexpected turn order
changes.

The tool uses the same JSON event stream produced by log downloads, supports
verbose turn output, and handles Ctrl-C by ending the encounter, writing the
partial log, and verifying what was captured.

fix(perf): keep long combat logging fast

Remove the combat-time log query bottleneck that made long replays slow as log
volume grew.

Combat controls no longer subscribe to the log collection just to keep undo and
redo state warm. Undo and redo now query the latest matching encounter log only
when clicked. Server collection queries support exact filters, ordering,
limits, and offsets, and SQLite indexes keep latest-log and per-encounter log
lookups fast.

Also fix duplicate WebSocket handler registration so realtime updates do not
double-fire under write load.

fix(turns): make toggle active a status change

Make toggle active a roster/status edit instead of a turn advance.

Deactivating the current participant no longer passes the turn or increments
the round. The current turn stays where it is until the DM explicitly clicks
Next Turn, and Next Turn skips inactive participants during normal rotation.

This matches the initiative design: slot order is stable, toggle active does
not move participants, and round changes only come from explicit turn advance.

chore(ci): make warnings and hangs fail fast

Tighten test and build checks so failures are visible instead of noisy or
silent.

Builds run with CI enabled so warnings fail production builds. The full test
command runs app, shared, and server suites with hard timeouts so hangs fail
quickly. Static eslint coverage fails on warnings as well as errors.

Tests were updated around the new async combat logging flow, structured log
events, transactional undo, replay verification, and toggle-active semantics.
2026-07-06 10:33:28 -04:00

9.6 KiB

TODO

Backlog of bugs + long-term items. Milestones live in REWORK_PLAN.md.

Open

TEST GAP: current branch changes need focused coverage

  • Storage where() contract: firebase + server adapters should honor [where('encounterPath','==',x), orderBy('ts','desc'), limit(n)].
  • Server SQL query test: where + orderBy + limit should return latest logs for one encounter only.
  • Undo/redo stack order: undo A3 then A2, redo must replay A2 first, then A3.
  • Combat controls should not subscribe to logs while mounted; undo/redo should query logs only on click.
  • Unified CLI smoke: node scripts/combat.js verify <fixture.json> returns CLEAN on known-good log.
  • Unified CLI replay smoke: node scripts/combat.js replay ... --out tmp/x.json writes JSON array and auto-verifies.
  • Ctrl-C replay behavior: SIGINT during replay should end encounter, clear active display, write partial log, run verify.
  • SQLite schema/index test: idx_docs_parent_ts and idx_docs_parent_encounter_ts exist for server DB.

confirm warnings treated as error = fail in all tests, build pipeline, linters, everything. again.

BUG: addParticipants (batch add) does not slot by initiative

  • shared/turn.js addParticipants appends [...existing, ...new], no slotIndexForInit.
  • Violates INIT doc: "Add = insert into slot by initiative."
  • Single addParticipant slots correct. Batch (add-all-chars) appends.
  • Pre-start batch add = wrong order. Post-start worse.

BUG: nextTurn throws on solo combatant

  • nextActiveAfter loop for step=1; step<n skips when n=1 → {nextId:null}.
  • nextTurn throws "Could not determine next participant."
  • Solo active combat cannot pass turn.

BUG: reorderParticipants cross-pointer drag = silent no-op

  • Cross-pointer drag returns encounter unchanged, no log, no toast.
  • DM drags across current turn → nothing happens, no feedback.

BUG: addParticipant undo missing currentTurnParticipantId when started

  • undo saves participants + conditional turnOrderIds, no currentTurnParticipantId.
  • Pointer can misalign on undo if added near pointer region.

BUG: computeTurnOrderAfterRemoval isActive uses find() not boolean

  • isActive = id => updatedParticipants.find(p => p.id === id && p.isActive)
  • Returns participant obj (truthy) not boolean. Works by accident, fragile.

BUG: select campaign during active combat = screen flicker, no action

  • In active encounter, click different campaign → flicker, no nav, no end.
  • Either block (toast: end encounter first) or auto-end current + switch.
  • Decide UX before fix.

death saves feature - dead PCs should not get removed from initiative! DAMMIT!

FEAT: clarify "Is NPC" in add-participant

  • Ambiguous label. May expand work based on what NPC means here (ally? monster? display-only? skip in turn order?). Clarify intent before UX changes.

FEAT: first-class undo/redo UI buttons (B --- do now)

  • Toolbar buttons ↶/↷ in AdminView header, not buried in /logs.
  • Undo = revert latest non-undone log. Redo = re-apply latest undone.
  • Uses current 2-write undo (non-tx). Race safety = log refactor later.
  • Disabled when stack empty. Keyboard shortcuts (cmd+z / cmd+shift+z).

FEAT-LOG: unified log refactor (was FEAT-2 + M6, batched)

  • Single event schema, one source of truth: { ts, type, payload, undo_payload, undone, encounterId, snapshot:{ round, currentTurnParticipantId, turnOrderIds, activeIds } }
  • Common format consumed by: UI log view, download/copy export, replay-combat, analyze-turns. One shape, four consumers.
  • Transactional undo: server endpoint POST /api/undo/:eventId. Single SQLite tx applies undo_payload + flips undone. Replaces fragile 2-write (log update + encounter update as separate calls).
  • Download/copy: exports event stream as JSON for offline analysis.
  • replay-combat + analyze-turns rewritten to emit/consume same event shape.
  • Migration: keep old log entries readable; new format for new writes.

FEAT - parallel campaigns

FEAT - multi user

FEAT - clarify what end encounter does and what initiatives reset means

Done (history)

FEAT: first-class undo/redo UI buttons (DONE)

  • ↶/↷ pills in InitiativeControls, always visible when encounter open.
  • Undo = latest non-undone log (per encounter). Redo = latest undone.
  • encounterPath added to all 14 log contexts (filter key).
  • redo:patch (forward) added to undoData. Real redo replays forward state.
  • Disabled when stack empty. Tooltip shows target action.
  • Uses current 2-write undo (non-tx). Race safety = FEAT-LOG refactor.

Architecture: 1-list turn order model (slot, never sort)

  • Single source: turnOrderIds === participants.map(id). No re-sort after startEncounter. nextTurn skips inactive (predicate), inactive stay in slot.
  • Drag (reorder) = same-init tie-break only. Cross-init blocked.
  • startEncounter sorts ALL participants by init once, then frozen.
  • addParticipant/updateParticipant slot by init (slotIndexForInit), preserve drag order. Display renders participants[] directly (no sort).
  • Static guard test errs if .sort( reintroduced outside allowlist.
  • Design doc: docs/INITIATIVE_ORDERING.md.

Single source of truth: combat logic

  • All 15 App.js handlers delegate to @ttrpg/shared. ~498 lines inline dupes deleted.
  • shared/turn.js = only place turn logic lives.

Storage parity (firebase + server adapters)

  • Neutral queryConstraints ({__type:'orderBy'|'limit'}) honored by both adapters.
  • Shared contract test runs both identically. Memory adapter deleted; factory throws on unknown mode.
  • ws storage mode renamed to server (env var + adapter name).

Logging contract

  • Every mutating op logs message + undo payload. No-op = null log.
  • Structural enforcement: per-op contract test (turn.logging.test.js) + static source-scan guard (static.no-unlogged.test.js).

Custom conditions per campaign (DONE)

  • Freeform per-campaign conditions. Add applies to participant + persists to campaign palette in one step. Badge render uses merged allConditions (built-ins + custom). toggleCondition accepts any string. Dedup case-insensitive. maxLength 40. Combat + replay tests prove arbitrary string ids survive round-trip.

Death saves: D&D 5e model (DONE)

  • Separate success/fail tracking. deathSave(enc, id, type, n), type='success'|'fail'. Fields: deathSaves, deathFails, isStable, isDying. 3 success=stable, 3 fail=dead. Returns {patch, log, status, isDying}. Old single-counter broken (successes missing). Old data lost (feature never worked in prod). UI: green ✓ + red ✕ rows, Stabilized/Dying badges.

FEAT-3: initiative first-class entry (DONE)

  • Initiative field at add-char, add-monster, edit participant.
  • Inline edit wired. Tie-break = drag order.

UI feedback: toast + info modal (DONE)

  • All 23 native alert() replaced. ToastStack (6s auto-dismiss + manual X) for transient failures. InfoModal (persistent OK) for validations. React context provider wraps all 3 App branches.
  • Fixed: native alert vanished instantly on browser focus loss.

Filter dup chars from add-participant dropdown (DONE)

  • Character dropdown excludes chars already in encounter. Prevents dup-add at source. No more dup alert path needed.

Test timeouts (DONE)

  • jest.setTimeout(10000) in setupTests.js (CRA blocks config-level timeout).

Warning = failure in tests (DONE)

  • console.error/warn throw in test env.

BUG-1: addParticipant + pause/resume corrupts rotation

  • RESOLVED as side effect of BUG-2 fix.

BUG-2: addParticipant allows duplicate id

  • FIXED (addParticipant throws on dup id).

BUG-4: hide-player-HP breaks display view

  • FIXED --- mock honors setDoc{merge}, all 5 activeDisplay sites use merge.

BUG-5: mid-round addParticipant/revive corrupts rotation

  • FIXED --- slot-array + DRY advance core nextActiveAfter.

BUG-6: reorderParticipants doesn't update turnOrderIds

  • FIXED structurally by 1-list model.

BUG-7: reorderParticipants not logged

  • FIXED --- returns log:{message, undo}. Handler calls logAction. deathSave, addParticipants, updateParticipant logging gaps also closed.

BUG-8: server adapter has no reconnect

  • FIXED --- onclose reconnects + re-subscribes existing paths.

BUG-10: deact+reactivate same round double-acts participant

  • FIXED --- 1-list model keeps slot position on toggle. Reactivate does not grant second turn. Test: turn.bug10.test.js.

BUG-11: FE Combat.scenario test crashes

  • FIXED --- moved to shared/turn.combat.test.js, pure functions, 100 rounds.

BUG-12: campaign selection follows activeDisplay

  • FIXED.

BUG-13: reorderParticipants crossing current pointer = ambiguous

  • FIXED --- block cross-pointer reorder during active encounter (both dirs). Full fix needs actedThisRound tracking. Pragmatic block prevents skip/double. Pre-combat: free reorder. Test: turn.bug13.test.js.

BUG-14: addParticipant init-insertion breaks after drag-reorder

  • FIXED --- slotIndexForInit scans current list (post-drag aware).

BUG-15: DisplayView re-sorts (drag order not preserved)

  • FIXED --- display renders participants[] directly.

BUG-16: subscribeCollection hook drops queryConstraints

  • FIXED --- neutral builders, both adapters honor orderBy/limit.

BUG-17: dead SDK imports in App.js

  • FIXED --- trimmed (auth + getFirestore + getStorage remain).

BUG-18: stale comments reference deleted memory adapter

  • FIXED.

FEAT-1: Dead participants stay in turn order

  • DONE --- applyHpChange no longer flips isActive on death. Dead stay in rotation, nextTurn visits them, PCs get death-save turn.

combat.scenario 100 rounds not turns

  • DONE --- loops by actual round-wrap count.

feat: add all characters to participants list

  • DONE --- addParticipants bulk add wired.