Commit Graph
80 Commits
Author SHA1 Message Date
david raistrick 9f8b09c7d9 feat(ui): first-class undo/redo buttons in combat controls
Undo/redo pills in InitiativeControls, always visible when encounter open.

- Undo = latest non-undone log for this encounter, applies undo.updates.
- Redo = latest undone log, applies undo.redo (forward patch).
- encounterPath added to all 14 logAction contexts (filter key).
- redo:patch added to undoData so redo replays real forward state.
- Disabled when stack empty. Tooltip shows target action message.
- Uses current 2-write undo (non-tx). Race safety deferred to FEAT-LOG.

TODO updated. 84 FE tests green.
2026-07-04 22:48:58 -04:00
david raistrick 9ef3e3bd9e feat(ui): filter already-added chars from participant dropdown
Character dropdown in add-participant picker excluded characters already
in the encounter. Prevents dup-add at the source — user can only select
chars not yet participating.
2026-07-04 22:28:41 -04:00
david raistrick 3423319b9b feat(ui): replace all alert() with toast + info modal
Native alert() vanished instantly (browser focus loss when player display
window or devtools steals focus). Replaced all 23 alert() calls with two
in-app feedback paths via React context:

- ToastStack: bottom-right stack, 6s auto-dismiss + manual X. For transient
  failures (storage throws, catch blocks).
- InfoModal: yellow notice modal with OK button, persistent until dismissed.
  For validations (dup-add, empty/invalid fields).

UIFeedbackProvider wraps all 3 App branches (admin/player/logs).
useUIFeedback() hook in 6 components (CharacterManager, ParticipantManager,
InitiativeControls, EncounterManager, LogsView, AdminView).

244 tests green (App 84, shared 133, server 27).
2026-07-04 22:27:32 -04:00
david raistrick 7bcf01dcf9 fix: custom conditions apply on add; correct server WS env docs
Custom conditions (per-campaign freeform) previously only persisted to the
campaign palette — Add button did NOT apply them to the targeted participant,
and badges rendered via CONDITIONS (missing custom ids) so applied custom
conditions did not display. Now:

- addCustomCondition(label, participantId) persists to palette AND toggles
  onto the participant in one step. Enter + Add button both pass p.id.
- Admin badge lookup uses allConditions (built-ins + custom), not CONDITIONS.

Shared contract: toggleCondition accepts ANY string. Prove it in tests:
- turn.combat.test.js adds CUSTOM_CONDITIONS ('hexed','rager',
  'marked_for_death','🛡️blessed') to the seeded queue + random pool, asserts
  condition-not-string invariant, and a dedicated add/remove round-trip test.
- replay-combat.js mirrors the same CUSTOM_CONDITIONS through the live backend.

Server-mode docs/env: REACT_APP_BACKEND_WS (stale pre-rename) -> correct
REACT_APP_BACKEND_REALTIME_URL=ws://.../ws in README, env.example,
docker/Dockerfile (STORAGE=ws -> server). DEVELOPMENT.md + scripts/README.md
now point to ./scripts/dev-start.sh as the primary dev entrypoint.

133 shared tests green.
2026-07-04 20:41:34 -04:00
david raistrick 1ad4b660a4 feat: custom conditions per campaign
DM can add freeform custom conditions alongside built-ins. Persists to
campaign doc, survives across encounters, shared with display view.

Implementation:
- ParticipantManager subscribes campaign doc via useFirestoreDocument
- customConditions[] merged with built-in CONDITIONS at render
- Input field + Add button in conditions picker (Enter or click)
- Dedup case-insensitive vs built-ins + existing custom
- maxLength 40
- addCustomCondition writes to campaigns/{id}.customConditions
- Badge render: custom conditions show raw label (no emoji)
- DisplayView: same fallback render for custom ids
- toggleCondition unchanged (already accepts any string id)
- campaignId prop passed from EncounterManager -> ParticipantManager

243 tests green. Build clean.
2026-07-04 18:20:32 -04:00
david raistrick 9c90c1500c feat(turn): D&D 5e death saves — success/fail tracking
Old model broken: single deathSaves counter, 3=revive. No fails tracked,
successes conflated with fails. DM couldn't model real death save outcomes.

New model (D&D 5e):
- deathSave(enc, id, type, n) — type='success'|'fail', n=1|2|3
- Participant fields: deathSaves (successes), deathFails, isStable, isDying
- 3 successes = stable (0hp unconscious, safe until healed)
- 3 failures = dead (isDying, caller animates removal)
- Toggling same pip = undo that pip
- Returns { patch, log, status } — status: 'stable'|'dead'|'pending'
  isDying back-compat flag kept for App.js removal animation

App.js:
- UI split into green ✓ saves row + red ✕ fails row
- Status badges: Stabilized (emerald) / Dying (red pulse)
- handleDeathSaveChange(participantId, type, saveNumber) — new sig
- makeParticipant init: deathFails + isStable defaults

Data loss: old single-counter participants lose saves (feature didn't work,
no real state to preserve). User confirmed acceptable.

Tests:
- turn.deathsave.test.js: RED-then-GREEN, 3-success stable, 3-fail dead,
  independent tracking, new signature
- Updated 6 callers across characterization/combat/dead-skip/logging/scenario
- Logs UI tests: new title selectors (Success N / Fail N)

243 tests green.
2026-07-04 17:56:14 -04:00
david raistrick 32b8dfd8a1 fix(turn): log addParticipants + updateParticipant (logging gaps)
addParticipants + updateParticipant returned log:null → invisible in
combat log. Contract test caught them. Now both return
log:{ message, undo:{ participants }}.

App.js handlers wired (handleAddAllCharacters, handleUpdateParticipant,
handleInlineInitiative) — call logAction on logged mutations.

Logging contract now complete: every mutating combat op produces a log
entry. No gaps. turn.logging.test.js proves it.

238 tests green.
2026-07-04 17:25:22 -04:00
david raistrick ab9f8fa2e7 fix(turn): log reorder + deathSave (BUG-7)
reorderParticipants returned log:null → handler skipped logAction → drag
invisible in combat log, no undo payload. Now returns
log:{ message, undo:{ participants, turnOrderIds, currentTurnParticipantId }}.
App.js handleDrop calls logAction on logged reorders.

Found second gap: deathSave also returned null log (both branches).
Fixed — message + undo (participants snapshot).

Added logging contract test (turn.logging.test.js):
- all mutating ops logged (start/next/pause/add/remove/toggle/hp/deathsave/
  condition/reorder/end)
- no-ops return null log (same-id, cross-init, cross-pointer blocks)
- undo payloads valid + restore prior state
- documents addParticipants + updateParticipant gaps (null log, intentional)

235 tests green.
2026-07-04 17:22:52 -04:00
david raistrick 4da5ed9bcc Merge remote-tracking branch 'upstream/main' into single-source
# Conflicts:
#	src/App.js
2026-07-04 16:43:14 -04:00
david raistrick c556860e49 refactor(App): remove dead SDK imports (BUG-17)
App.js imported 9 Firestore SDK functions it never called standalone:
doc, setDoc, addDoc, collection, onSnapshot, updateDoc, deleteDoc, query,
writeBatch. All writes go through storage adapter (storage.*). Auth pieces
(initializeApp, getAuth, signInAnonymously, onAuthStateChanged,
signInWithCustomToken, getFirestore) stay — real SDK init in firebase mode.

orderBy/limit stay — now neutral builders from index.js (prev commit).
2026-07-04 16:07:21 -04:00
david raistrick 79af61cb8b match main firebase behavior; fix contract + mismatches
Contract rewritten to match main prod truth:
- require id injection in all doc/collection results
- honor setDoc({merge:true}) (main L1624 + 4 activeDisplay sites)
- drop invented bare<->prefixed cross-lookup tests (main never does this)
- add setDoc{merge}, batch set-only/update-only contract coverage

Fix mismatches vs main:
- subscribeCollection hook now forwards queryConstraints (was dropped;
  LOG_QUERY sort+limit honored again). Mock onSnapshot honors orderBy+limit.
  2 new contract tests prove the chain.
- subscribeDoc/subscribeCollection adapters now forward errCb. App hooks +
  DisplayView propagate subscribe errors to UI (match main onSnapshot 3rd arg).
- ws adapter signatures accept queryConstraints + errCb (interface match).

activeDisplay writes match main:
- 5 unguarded sites -> setDoc({merge:true}) (create-if-missing, not updateDoc)
- 3 guarded sites stay updateDoc

Delete memory adapter: third storage system added complexity, zero value.
firebase-mock covers fast tests, ws covers server path. Factory throws on
unknown mode now.

Delete phantom 'Phase A/B' comment in firebase.js header.

Tests updated to assert setDoc{merge} (not updateDoc) for activeDisplay writes.
83/83 green.
2026-07-04 12:22:19 -04:00
david raistrick e650cd2710 refactor: single-source display lifecycle + scenario test no React
Move activeDisplay lifecycle to shared (one path, not duplicated in App):
- activateDisplay({campaignId, encounterId})
- clearDisplay()
- toggleHidePlayerHp(current)
All 6 App sites wired (start/end/2×delete-encounter/delete-campaign/hp-toggle).
Existing {patch,log} return shape preserved. No interface changes.

Combat.scenario.test.js rewritten to call shared directly, no React:
- Same actions as old UI version (roster chars, monsters, addAll, hp-toggle×2,
  start, 100 rounds of damage/heal/conditions/toggle/edit/deathsave/pause/
  resume/add/remove, end). Same funcs, same order, same data.
- Models two firestore docs (encounter + activeDisplay) via shared patches.
- Run time: 72s -> 4ms.

Both suites green: shared 91/91, App 77/77.
2026-07-03 18:26:02 -04:00
david raistrick d83d1383c0 refactor: single-source combat logic (App handlers call shared, slot model)
App.js no longer inlines combat logic. All handlers delegate to @ttrpg/shared:
startEncounter, nextTurn, togglePause, endEncounter, addParticipant(s),
updateParticipant, removeParticipant, toggleParticipantActive, applyHpChange,
deathSave, toggleCondition, reorderParticipants. ONE code path for UI.

shared/turn.js aligned to slot model (docs/INITIATIVE_ORDERING.md):
- addParticipant: splice into slot by initiative (no sort)
- updateParticipant: re-slot on initiative change (no sort)
- reorderParticipants: same-init drag only (cross-init blocked)
- applyHpChange: death flips isActive=false (matches App), revive reactivates

No renames, no API changes. Shared func signatures unchanged.

Tests updated to match unified behavior:
- dead-skip: death deactivates (was FEAT-1 stays active)
- characterization: death deactivates + revive reactivates
- reorder/invariant: cross-init drag blocked, same-init allowed

Both suites green: shared 91/91, App 77/77, 0 warnings.
2026-07-03 17:59:41 -04:00
david raistrick 31d19c5912 test: kill act env warnings + remove debug console noise
Root cause: mock _notify cb fires during waitFor poll, but testing-library
asyncWrapper flips IS_REACT_ACT_ENVIRONMENT=false. Raw react act() then
warns 'not configured to support act' (146 warnings).

Fix: _notify sets globalThis.IS_REACT_ACT_ENVIRONMENT=true around act(),
restores after. setupTests sets flag globally. Both together = 0 warnings.

Also:
- Remove 4 debug console.log from App.js (encounter start/end, drag, add chars)
- Remove always-fires scenario summary console.log
- Add fastWaitFor helper (5ms poll) to testHelpers
- Swap scenario waitFor -> fastWaitFor

Tests: 77 pass / 0 fail / 0 warnings / 0 console noise.
2026-07-03 15:59:14 -04:00
robert 8cf3dad5b6 Merge pull request 'Rework backend' (#2) from rework-backend into main
Reviewed-on: #2
2026-07-03 08:35:32 -04:00
david raistrick 3d0dc79206 feat(UI): hide NPC/monster HP toggle (player display)
Mirrors hidePlayerHp. New hideNpcHp flag on activeDisplay doc.
- AdminView: 'Hide NPC/monster HP' switch in Player Display settings.
- DisplayView: HP bar gated by !(hideNpcHp && p.type !== 'character').
  Covers monsters + NPCs (all non-player).
- Default false (show NPC HP) — opposite of player HP default true.

updateDoc patch (BUG-4 pattern). No clobber.

Test: HideHpToggle selector scoped to 'hide player hp' (now 2 switches).
2026-07-02 11:29:02 -04:00
david raistrick 4b8b8bccfb feat(UI): collapsible Campaigns section
Campaigns heading -> clickable button. ChevronDown expanded,
ChevronRight collapsed. Toggles grid visibility. Shows count
(e.g. 'Campaigns (61)'). Preserves Create Campaign button.

State: campaignsCollapsed in AdminView. aria-expanded/controls for a11y.
2026-07-02 11:17:04 -04:00
david raistrick c0998da0a7 fix(BUG-4): updateDoc patch on activeDisplay (not setDoc replace)
All 5 storage.setDoc(activeDisplay, {...}, {merge:true}) →
storage.updateDoc(activeDisplay, {...}).

setDoc merge:true worked in prod (firebase honors merge) but ws adapter
+ mock ignore opts arg entirely → clobbers doc. updateDoc uses PATCH
across all adapters (firebase real updateDoc, ws PATCH endpoint, mock
merge). Consistent, no clobber.

Sites fixed:
- hidePlayerHp toggle
- startEncounter (set active ids)
- endEncounter (null active ids)
- deactivate active display
- activate new display

TDD: HideHpToggle.test RED first (assert updateDoc patch, impl still
setDoc → 0 calls found). GREEN after switch.

Char tests updated: Encounter.characterization (2) + Combat.characterization
(2) assert updateDoc on activeDisplay, not setDoc.

BUG-4: prod was already fixed (merge:true), test was RED due to mock
ignoring opts. Now all 3 adapters consistent via updateDoc.
2026-07-01 23:32:23 -04:00
david raistrick d00cc104c9 feat(FEAT-3): reslot on all participant mutation paths
Reslot (stable sort by init desc, tie-break = original array index) now
fires on all 4 paths that can change order:

1. Add participant — sortParticipantsByInitiative([...parts, new], parts)
2. Edit modal save (handleUpdateParticipant) — reslot + syncTurnOrder
3. Drag reorder — splice move (already correct, untouched)
4. Inline init field — reslot (already committed 08c27c1)

Before: add appended (ignored init), edit modal overwrote value without
moving slot. Both caused list order to drift from init order until
startEncounter (sorts once). Now any init change immediately reslots
into correct position. Display + AdminView reflect order.

Stable sort preserves drag order within ties (tie-break = original index
= reflects prior drag). Move-one semantics: only changed element moves.

EditParticipantModal: added htmlFor/id link on Initiative label (was
missing — a11y + testable).

Tests: ReslotAllPaths.test.js (2). RED first (add appended, edit modal
no reslot), green after impl.
2026-07-01 23:00:40 -04:00
david raistrick 08c27c1ca5 feat(FEAT-3): reslot on inline init change + gate field
Reslot: handleInlineInitiative now sorts participants[] by init desc
(stable, tie-break original index) via sortParticipantsByInitiative.
Display + AdminView reflect new order after init edit. Not a blind
re-sort — only moved element changes position.

Gate: inline init field disabled when combat active + not paused.
Matches drag gating. DM must pause to edit initiative mid-combat.

Tests: InitiativeReslot.test.js (2). RED first (no reslot, Goblin stayed
at idx 1), green after impl (reslots to idx 0). Field gate test.
2026-07-01 22:29:38 -04:00
david raistrick 0514939c51 feat(FEAT-3): initiative first-class field (add + inline edit)
Add form: optional initiative field (monster + character). Empty = roll
d20+mod (current behavior). Filled = use value, skip roll. 'blank=roll'
hint + 'auto' placeholder for clarity.

Inline edit: ALL participants. Number input in participant row. Blur or
Enter commits. Capped 2 digits (max 99). Auto-select on focus for quick
overwrite. Styled to match other fields (border-stone-700, rounded-md,
shadow-sm, w-10).

handleAddParticipant: manualInit detects set value. lastRollDetails
adapts display (manual flag shows 'Set initiative' vs 'Rolled d20').

Campaign card date: Clock icon + 'Created:' prefix, own row, muted
stone-300 opacity-70. Was crammed inline with character/encounter counts.

Tests: InitiativeField.test.js (2 tests - set value + empty=roll).
RED first (field missing), then green after impl. App + Participant
characterization still green (18 total).

Note: inline init edit does NOT re-sort. Known followup — displays + list
order must reflect changed initiative. Tracked separately.
2026-07-01 22:25:52 -04:00
robertandClaude Sonnet 5 ec578eeef5 Bump to v0.4, document self-hosted backend in README
The rework-backend merge added an optional self-hosted Express/ws/SQLite
backend (npm workspaces, single-container Docker deployment) alongside
the existing Firebase default. Bump APP_VERSION and refresh README to
cover both storage modes and the new repo layout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-01 19:59:46 -04:00
david raistrick 58ae04b400 fix(BUG-15): DisplayView no longer re-sorts participants by initiative
DisplayView called sortParticipantsByInitiative() on visibleParticipants,
ignoring DM drag order. 1-list model = participants[] IS display source.
After cross-init drag, player view diverged from AdminView/turnOrderIds.

Repro: round 4 replay. [reorder Summon1(10)→before Merchant(11)] made
turnOrderIds = [...,Summon2,Summon1,Merchant,OrcBoss]. AdminView correct.
DisplayView re-sorted = Summon2,Merchant,Summon1 (init order) = visually
Merchant appeared between Summon2 and Summon1, NOT at end. DM confused.

Fix: removed sort. DisplayView now renders participants[] order directly
(filter inactive monsters only), matching AdminView line 1222.

Test: RED → GREEN (src/tests/DisplayView.drag-order.test.js). Seeds 3
monsters in drag order [High:20, Low:10, Mid:11]. Asserts DOM order =
participants[] order, not init-sorted. No DisplayView regressions.
2026-07-01 17:31:40 -04:00
david raistrick dbd0c75792 fix(BUG-12): campaign selection follows activeDisplay
Selection effect had `!selectedCampaignId` guard — once any campaign
selected, new activeDisplay.activeCampaignId writes ignored. Replay tool
writes activeDisplay to new campaign each run; UI stayed on old selection
=> displayed wrong campaign data.

Removed guard. Selection now syncs when activeCampaignId differs from
current selection. Manual deselect (null) does not force-select (RED test
locks this).

Also fixed test helper bug: createCampaignViaUI/createEncounterViaUI
returned FIRST setDoc match (idA for all creates). Now filters by name +
.pop() for latest. This masked the real bug for several debug cycles.

Tests: 2 new (SelectionFollowsActiveDisplay), both green. No regressions
in full FE suite (App, Combat, DisplayView, Encounter, HideHpToggle,
Logs, Participant, storage all pass). Combat.scenario = pre-existing
BUG-11 crash, not regression.
2026-07-01 17:05:00 -04:00
david raistrick 750ee99080 feat: display campaign createdAt in UI card
Campaign card now shows created date/time next to char/encounter counts.
Lets DM tell newest campaign apart (replay tool creates many).

createdAt already set at campaign create (line 2174). Display renders
formatted: 'Jul 1, 2026, 16:32'.

replay-combat.js: campaign + encounter names now include timestamp
(new Date().toLocaleString) for easy identification.

WS collection push verified live (injected test campaigns appeared
without reload).
2026-07-01 16:41:17 -04:00
david raistrick 3ea67019d2 refactor: App.js 3 sites to shared 1-list contract
delete/toggle/hp sites used OLD computeTurnOrderAfterRemoval/Addition
contract (return turnOrderIds). New 1-list contract: helpers return
advance-only + insertAt; list sync via syncTurnOrder at call site.

- delete: syncTurnOrder(updated) + advance-only removal
- toggle: stay-in-slot, flip isActive, sync, advance only if deact==current
- hp: FEAT-1 unchanged (death/revive no turn changes)

shared exports syncTurnOrder. Build green.
2026-07-01 16:05:18 -04:00
david raistrick 7c3ec105d5 refactor: App.js 1-list display + start/resume (no re-sort)
3 sites fixed to match shared 1-list model:
- line 1216 display: sortedParticipants = participants[] (no re-sort). GM
  list renders participants[] directly = turnOrderIds.
- startCombat inline: sort ALL participants by init (active+inactive),
  first active = current, persist participants[] reordered + turnOrderIds.
- resume inline: no re-sort on resume. turnOrderIds unchanged.

Display === rotation === turnOrderIds by construction (1-list invariant).
Build green.
2026-07-01 16:03:38 -04:00
david raistrick d1cbe7091a refactor: App.js imports shared turn funcs (DRY), delete duplicates
Delete duplicate consts (DEFAULT_MAX_HP/INIT_MOD/MONSTER_DEFAULT_INIT_MOD,
generateId, rollD20, formatInitMod) + funcs (sortParticipantsByInitiative,
computeTurnOrderAfterRemoval, computeTurnOrderAfterAddition) from App.js.
Import from @ttrpg/shared (1-list model). Kills second drift source.

CRA resolves @ttrpg/shared via npm workspaces symlink. Build green.
2026-07-01 16:02:35 -04:00
david raistrick 84a8b78acd M2: refactor DisplayView to storage adapter (red test first)
DisplayView missed in original M2 refactor — raw onSnapshot(doc(db,path))
survived. In ws/memory mode db is a stub sentinel, so raw SDK calls crash
('Expected first argument to collection() to be a CollectionReference...').
Reported by human testing player display after start combat.

TDD:
1. RED: DisplayView.characterization.test.js asserts adapter.subscribeDoc
   called for campaign + encounter + activeDisplay paths. Adapter recorder
   (getAdapterCalls/resetAdapterCalls in firebase.js) instruments subscribe
   calls — catches raw-SDK bypass that firebase mock alone cannot (mock db
   satisfies raw onSnapshot, hiding the bug).
2. Fix: 2 raw onSnapshot sites in DisplayView -> storage.subscribeDoc.
3. GREEN: 2 new tests pass, 116 total green.

Audit confirmed DisplayView was the ONLY remaining raw SDK site in App.js.
2026-06-29 13:13:46 -04:00
david raistrick 35cd1581e3 Reapply "M3: stub db sentinel in ws/memory mode so legacy guards pass"
This reverts commit d1ee69a70a.
2026-06-29 12:36:16 -04:00
david raistrick 17245dfa1b Reapply "M2: gate storage init on STORAGE mode (firebase vs ws/memory)"
This reverts commit e843acdf8a.
2026-06-29 12:36:16 -04:00
david raistrick e843acdf8a Revert "M2: gate storage init on STORAGE mode (firebase vs ws/memory)"
This reverts commit 3e84f28325.
2026-06-29 12:03:57 -04:00
david raistrick d1ee69a70a Revert "M3: stub db sentinel in ws/memory mode so legacy guards pass"
This reverts commit a5a4df78f0.
2026-06-29 12:03:57 -04:00
david raistrick a5a4df78f0 M3: stub db sentinel in ws/memory mode so legacy guards pass
ws/memory storage mode never set the module-level `db` variable. 24 handlers
guarded with `if (!db) return` early-exited, silently dropping all writes
(create campaign, add encounter, participant CRUD, combat, logs).

db stays a truthy sentinel object { __localStub: true } in non-firebase mode.
All real reads/writes route through storage.*; db only used by guards.

56 frontend tests green. Verified via headed browser: create campaign flow
works end-to-end (modal closes, campaign appears via WS realtime push).
2026-06-29 11:30:08 -04:00
david raistrick 3e84f28325 M2: gate storage init on STORAGE mode (firebase vs ws/memory)
- STORAGE_MODE = getStorageMode()
- initializeStorage(): firebase = real SDK init; ws/memory = stub auth
- App auth flow: ws/memory skip signInAnonymously, unblock UI
- error screen mode-aware message

56 tests green. STORAGE=ws now boots past config error.
2026-06-28 21:11:56 -04:00
david raistrick 812298fa73 M2: refactor all firebase write sites to storage adapter
- 37 call sites: setDoc/updateDoc/deleteDoc/addDoc/getDocs/writeBatch -> storage.*
- adapter wraps SDK, path-string interface
- storage instance app-wide (getStorage)
- firebase.js: static imports (getDoc/getDocs alias), no dynamic import

56 frontend tests green. STORAGE=firebase = identical behavior.
2026-06-28 21:05:39 -04:00
david raistrick 5bb9e5fc19 M2: refactor hooks to storage adapter (subscribe)
- src/storage/index.js: getStorage() factory + SDK re-exports
- App.js: useFirestoreDocument/Collection call storage.subscribeDoc/Collection
- getStorage import added

56 frontend tests green. Hooks now impl-agnostic (firebase vs ws).
2026-06-28 19:03:44 -04:00
david raistrick 12b24eb707 M2 (C): storage barrel re-export, App.js imports swapped
- src/storage/index.js: re-exports Firebase SDK
- App.js: imports from ./storage (was firebase/* direct)
- STORAGE=firebase = identical behavior
- dev server compiles clean

Safe refactor proof. Next: per-call-site path-based rewrite for ws adapter.
2026-06-28 17:51:39 -04:00
robertandClaude Sonnet 4.6 33b27775b4 Bump version to v0.3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 20:15:20 -04:00
robertandClaude Sonnet 4.6 d96d3a6cf2 Guard against undefined values in combat log undo payloads
Firestore rejects writes containing undefined values. The pause/resume
and end-combat undo snapshots read encounter fields that may not yet
exist in Firestore, so add ?? false / ?? null / ?? 0 fallbacks to
match the pattern already used in the start-combat undo path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 17:19:52 -04:00
robertandClaude Sonnet 4.6 439f48871e Fix mid-round activation causing participants to be skipped in round 2+
When a participant was activated mid-combat, computeTurnOrderAfterAddition
appended them to the end of turnOrderIds. The visual display sorted by
initiative (putting them at the top), but the turn pointer followed the
append order, making it look like the top-initiative participants were
skipped when the round wrapped.

Fix: at the round boundary in handleNextTurn, rebuild turnOrderIds from
all active participants sorted by initiative. Mid-round additions go last
in round 1 (standard D&D ruling), then slot into proper initiative order
from round 2 onwards. Also adds turnOrderIds to the next-turn undo snapshot.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 16:57:08 -04:00
robertandClaude Sonnet 4.6 35990f588e Add per-entry undo buttons to combat log, bump to v0.2.5
Each logged action now stores a Firestore snapshot of the affected
encounter state. The /logs page shows an ↩ Undo button on any entry
with undo data; clicking it restores the encounter to its pre-action
state and marks the entry as rolled back (greyed out, strikethrough).

Covered actions: damage/heal, condition toggle, activate/deactivate,
add/remove participant, next turn, start/pause/resume/end combat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 16:45:07 -04:00
robertandClaude Sonnet 4.6 7b52480329 Add wake lock toggle to prevent screen sleep on player display
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 11:56:11 -04:00
robertandClaude Sonnet 4.6 58cc588726 Add fullscreen button to player display view
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 11:53:56 -04:00
robertandClaude Sonnet 4.6 e200f59b7e Hide inactive monsters from player display view
Inactive monsters are now filtered out of the DisplayView so DMs can
pre-stage summoned/reserve monsters without spoiling them for players.
Inactive characters remain visible since their inactive state is
player-relevant.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 14:39:59 -04:00
robert bb65709e26 Added shield to list of conditions. 2026-05-16 15:29:27 -04:00
robertandClaude Sonnet 4.6 33d831af54 Fix race condition causing permissions error on /display page
onAuthStateChanged fires with null before signInAnonymously completes,
causing DisplayView to query Firestore unauthenticated. Now only marks
auth ready when an actual user is present; auth failures are handled in
the catch block to avoid hanging the UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 10:53:03 -04:00
robertandClaude Sonnet 4.6 4150267925 Add combat action log at /logs
Instruments 9 handlers (combat start/end/pause/resume, next turn,
participant add/remove/toggle, HP changes, conditions) to write
timestamped entries to a Firestore logs collection. New LogsView
at /logs shows entries newest-first with encounter context, and
includes a Clear Log button. Adds a View Logs link in the header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 10:25:17 -04:00
robertandClaude Sonnet 4.6 e23cea205a Add HP toggle, new conditions, fix turn order sync bug
- Add DM toggle (default on) to hide player HP bars on player display;
  persisted in activeDisplay Firestore doc for real-time sync
- Add Alchemist Fire and Bardic Inspiration conditions; sort all
  conditions alphabetically
- Fix turn order skipping when participants are deleted, deactivated,
  or killed mid-combat: turnOrderIds was never updated, causing
  handleNextTurn to resolve currentIndex as -1 and snap back to the
  first participant. Now all mutation paths (delete, toggle active,
  HP death/resurrection) keep turnOrderIds in sync and advance the
  turn pointer correctly when the current participant is removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 10:00:17 -04:00
robert 6cd25dadaa Alchemist Fire and Bardic Inspiration conditions, sort alphabetically. 2026-05-16 09:37:11 -04:00