Commit Graph

98 Commits

Author SHA1 Message Date
david raistrick 33e0e52789 tests: pause-add rotation corruption + dup-id, log bugs to TODO
- turn.pause-add.test.js: 3 tests isolating addParticipant+pause/resume
  interaction. Clean minimal repro passes (bug needs more state than
  single add+pause). Audit authoritative repro.
- turn.characterization.test.js: RED 'addParticipant rejects duplicate id'.
  Validates current allow-dup behavior.
- TODO.md: BUG-1 (add+pause rotation corruption, 32/100 audit violations),
  BUG-2 (dup id allow). Both confirmed real, NOT fixed.

Audit bisect: dmg+heal+cond+toggle+remove+add+pause = 32 violations.
add+pause alone = 0. Combo needs full state.

No feature code changed.
2026-06-29 15:52:17 -04:00
david raistrick 13490fe3de tests: round-rotation audit, dup-id fail, replay rewrite
- turn.round-rotation.test.js: 7 tests, full round visits each active
  participant once (pure nextTurn clean). Green.
- turn.characterization.test.js: RED 'addParticipant rejects duplicate id'.
  Validates current behavior allows dup ids (self-inflicted in audit via
  loop spin-while-paused re-adding same id; unreachable in app via
  crypto.randomUUID, but documents gap).
- audit-rotation.js: pure turn.js simulation of replay op sequence.
  Detects rotation violations (skip/dupe per round). Pause disabled = 0
  violations across 100 rounds. Pause enabled = 56-77 violations starting
  round 20. Pinpoints addParticipant+pause interaction.
- repro-pause-bug.js: minimal repro scripts.
- replay-combat.js: rewritten for real rounds (full initiative cycles),
  visible damage each turn, all conditions, toggleActive, remove,
  reinforce, edit, pause/resume, reorder, endEncounter. HP bumped for
  100-round sustain + revive dead each round.

No feature code changed.
2026-06-29 15:49:39 -04:00
david raistrick 7866dec83b replay: loop by real rounds, visible damage each turn, faster default
- ROUNDS now = full initiative cycles (not turns). Each round advances
  initiative until round counter ticks (all participants act).
- Visible damage: current actor hits random living target for 3-10 dmg.
  Player view sees HP bars change live.
- Default delay 200ms (was 800ms).
- Reproduces M4 skip bug: rounds shrink as participants die (8→7→2→1).
- Label accuracy: 'turn N (round X)'.
2026-06-29 15:21:48 -04:00
david raistrick 891fc696d9 docs: add glossary (turn vs round, participants, views, backend) 2026-06-29 15:19:18 -04:00
david raistrick 9fd0f3ec38 M3: fix path-shape drift via adapter contract + identity tests
Root cause (HAR-diagnosed): replay script wrote firebase-prefixed paths via
raw REST, bypassing adapter norm(). Two path roots coexisted in db:
  bare 'campaigns/X' (adapter writes, from App)
  prefixed 'artifacts/.../campaigns/X' (replay raw writes)
Adapter read bare, missed prefixed. UI showed stale test1 (legit manual UI
write, not wiped) but replay campaigns invisible.

A. replay-combat.js: use createWsStorage adapter instead of raw fetch. Same
   contract boundary as App. norm() runs on all paths. Can't drift.
   Mirror App.js getPath locally for path construction.
B. contract.js: 4 new identity tests (setDoc prefixed -> getCollection bare,
   setDoc prefixed -> getDoc bare, setDoc prefixed -> getDoc prefixed,
   setDoc bare -> getCollection prefixed). Run against every impl (memory,
   ws). memory.js lacked norm() -> RED first, now GREEN after adding norm.
C. db moved out of /tmp to ./data/tracker.sqlite (gitignored). Never tmp.

Tests: 124 green (39 shared + 23 ws-contract + 62 FE).
2026-06-29 15:13:03 -04:00
david raistrick 6630fd9158 M3: add combat replay script + TODO for M4 skip/dead fixes
scripts/replay-combat.js: drives full combat via live backend REST, computes
turns through shared/turn.js. Player display (subscribed via WS) live-updates.
Usage: node scripts/replay-combat.js [rounds] [delayMs]

TODO.md: tracks M4 work.
  - Dead participants must NOT be skipped (still occupy initiative slot,
    death saves resolve on their turn). Saw in game Saturday.
  - JUMP_TURN_TO manual turn override.
2026-06-29 14:36:02 -04:00
david raistrick 1d4c561c09 M3: add full 100-round combat scenario test
Drives same UI buttons a DM clicks. Exercises:
- campaign + character roster (3 PCs)
- monsters, NPC, add-all, hidden hp toggle
- start combat, 100 rounds of nextTurn
- damage/heal, conditions (stunned), toggle-active
- edit initiative, edit name, remove participant
- pause/resume with reinforcement mid-combat
- damage-to-0 + death saves (x3) + revive
- end combat + confirm

Harness: each phase wrapped in recordAsync try/catch. Failures collected,
reported at end, do NOT abort run. Found no app bugs; selector bugs in harness
fixed (char form ids vs placeholders, encounter row scope via Init: marker,
conditions panel async render, dead-participant damage skip as expected game
state, end-encounter modal title 'End Encounter?' not 'End Combat').

289/289 phases ok. 58 other frontend + 39 shared + 19 ws-contract = 116 total green.
2026-06-29 13:51:32 -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 52866784b2 M2: replace shape-specific backend with generic KV doc store (firebase mirror)
Backend was endpoint-mapper (POST /api/campaigns ignores path id, etc). Adapter
translation layer brittle, untested, lost doc identity. Generic contract (Layer 2)
test caught 15 bugs immediately.

Rewrite to firebase-mirror KV model:
- server/db.js: docs table (path PK, parent, data JSON). getDoc/setDoc/updateDoc/
  deleteDoc/getCollection/batchWrite. parent = path prefix for collection queries.
- server/index.js: generic REST (GET/PUT/PATCH/DELETE /api/doc, GET /api/collection,
  POST /api/collection addDoc, POST /api/batch). WS subscribe by path (doc|collection),
  broadcast to doc subs at changed path + collection subs at parent path.
- src/storage/ws.js: thin passthrough adapter. norm() strips firebase prefix.
  initial value via REST (independent of WS connect), subsequent changes via WS.
- shared/turn.js kept (M4 use). server/handlers.js + server.test.js removed (logic
  now in App, backend is dumb KV).
- src/storage/contract.js: flush() bumped to 50ms (WS roundtrip > setTimeout(0)).

Layer 2 test (server/ws-contract.test.js): spins fresh backend per test, runs same
storage contract spec against createWsStorage. Catches adapter translation bugs
that firebase-mock Layer 1 tests cannot.

nanoid v5 ESM breaks jest CJS -> replaced with crypto.randomUUID (Node builtin).

Tests: 114 green (39 shared + 19 ws-contract + 56 frontend).
2026-06-29 13:00:24 -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 ed67535b1f Reapply "M2: fix ws adapter for browser WebSocket + firebase path prefix"
This reverts commit 74b4c2c42d.
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 74b4c2c42d Revert "M2: fix ws adapter for browser WebSocket + firebase path prefix"
This reverts commit e743d40e8d.
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 b095e37bfe M3: remove project dev-serve skill (moving to generic global skill) 2026-06-29 11:25:13 -04:00
david raistrick 54e8df9ffa M3: add dev-serve skill (boot stack + headed browser for human testing)
- pkill stale procs, boot backend + frontend in background
- STORAGE=ws default (firebase opt-in)
- agent_browser --headed fresh session for user-visible window
- troubleshooting table + teardown

Skill loads via /skill:dev-serve
2026-06-29 11:24:23 -04:00
david raistrick e743d40e8d M2: fix ws adapter for browser WebSocket + firebase path prefix
- WebSocketImpl: native browser WebSocket if present, else ws pkg (node/jest)
- ensureWs: use onopen/onmessage/onerror/onclose property handlers (browser API)
  instead of ws.on('open') EventEmitter (node-only) — was silent no-op in browser
- norm(): strip 'artifacts/{APP_ID}/public/data/' prefix from all paths
  App passes firebase-prefixed paths; backend uses canonical campaigns/...
- apply norm() to getDoc/getCollection/setDoc/updateDoc/deleteDoc/addDoc/
  subscribeDoc/subscribeCollection/changeTypesForDocPath/changeTypesForCollPath

Verified: STORAGE=ws boots, WS subscribe fires, backend broadcast -> browser
live update (curl POST campaign -> appears without reload). Cross-device sync
confirmed end-to-end.
2026-06-28 22:43:30 -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 35b5a1d238 test: logs + deathSave characterization (6 tests)
- Logs.characterization.test.js: logAction (write + undo payload), clearLogs batch delete, undo (updateDoc encounter + mark undone), deathSave increment + isDying
- mock firestore getDocs: return .ref.path on docs (batch.delete support)
- mock addDoc: record full doc path not collection path

All write sites characterized. 56 frontend tests green.
2026-06-28 19:00:08 -04:00
david raistrick d581e60ba3 test: combat characterization (9 tests)
- Combat.characterization.test.js: startEncounter (state + activeDisplay), nextTurn, round wrap, pause, resume, endEncounter (reset + clear activeDisplay), toggleHidePlayerHp

Locks combat control write paths.
2026-06-28 18:52:49 -04:00
david raistrick 4158a1634d test: participant characterization (9 tests)
- Participant.characterization.test.js: addMonster (shape, initiative range, NPC), deleteParticipant, toggleActive, applyDamage, damage-to-0, heal revive, toggleCondition
- testHelpers.js: getParticipantForm (scoped), addMonsterViaUI, setupReady, startCombatViaUI

Locks participant write paths + payload shapes. Refactor guard.
2026-06-28 18:50:42 -04:00
david raistrick 0c1196aee1 test: encounter characterization (6 tests)
- Encounter.characterization.test.js: createEncounter, path nesting, togglePlayerDisplay on/off, deleteEncounter + clears activeDisplay
- testHelpers.js: createEncounterViaUI, selectEncounterByName

Locks encounter write paths + payload shapes.
2026-06-28 18:30:57 -04:00
david raistrick 672f042b60 chore: alphabetize package.json deps after install/uninstall churn
trivial reorder, no version changes
2026-06-28 18:29:09 -04:00
david raistrick b6555648ee test: campaign characterization (7 tests)
- src/testHelpers.js: renderApp, createCampaignViaUI, selectCampaignByName
- App.characterization.test.js: createCampaign, addCharacter, updateCharacter, deleteCharacter, deleteCampaign + path namespace + bg url
- mock firestore writeBatch sync (was async, app no-await)

Locks path + payload shape per action. Refactor guard.
2026-06-28 18:12:27 -04:00
david raistrick 84dd17e174 test: Firebase mock harness + createCampaign characterization
- src/__mocks__/firebase/*: jest manual mocks (app/auth/firestore)
- src/__mocks__/firebase/_mock-db.js: in-memory DB + call recorder
- src/setupTests.js: jest-dom, env stubs, crypto polyfill, DB reset
- src/App.characterization.test.js: createCampaign -> setDoc path/payload locked
- src/storage/contract.js (renamed from .test.js, helper not suite)

21 tests green (memory 19 + createCampaign 2).
2026-06-28 17:59:50 -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
david raistrick 2ee2bba93b M2 (TDD): storage contract test + memory impl
- src/storage/contract.test.js: storage interface spec (19 assertions)
- src/storage/memory.js: in-process impl (Map + EventEmitter)
- src/storage/storage.test.js: runner, memory first

TDD: contract RED first, memory built to satisfy, 19/19 green.
Next impls (ws, firebase) run same contract.
2026-06-28 17:18:14 -04:00
david raistrick 9457f48b23 ci: local pre-push hook instead of GH Actions (private repo)
- remove .github/workflows/ci.yml
- add .githooks/pre-push: runs npm run test:all
- git config core.hooksPath .githooks (set)
- docs/DEVELOPMENT.md: document local pipeline

Private repo = no free Actions. Revisit when public.
2026-06-28 17:16:23 -04:00
david raistrick fa19913e23 ci: add GitHub Actions workflow + dev docs + test:all script
- .github/workflows/ci.yml: runs shared + server tests on push/PR
- docs/DEVELOPMENT.md: setup, run, test, architecture, status
- package.json: test:all script (shared + server suites)
2026-06-28 17:14:51 -04:00
david raistrick 0e76fb2fc7 M1: backend (Express+ws+better-sqlite3) + integration tests
- server/db.js: SQLite schema mirroring Firestore doc tree
- server/handlers.js: action -> shared turn fn -> tx persist -> broadcast
- server/index.js: REST endpoints + WebSocket real-time push
- server/server.test.js: 7 integration tests (REST CRUD + combat flow)
- --forceExit for jest (open WS handles)

Backend boots, serves state, persists to SQLite.
2026-06-28 17:01:53 -04:00
david raistrick e06adaa081 M1: shared turn logic + characterization tests (39 green)
- npm workspaces: shared/, server/
- shared/turn.js: port turn logic verbatim from App.js (bugs preserved)
- 39 characterization tests lock current behavior
- gitignore: sqlite data, logs
2026-06-28 16:57:43 -04:00
david raistrick d679c9d1e9 docs: restore approved milestone plan (backend=M1, drop FSM-extract milestone) 2026-06-28 16:53:18 -04:00
david raistrick ad7979d8fd docs: add rework plan (backend-first, test-ecosystem baseline) 2026-06-28 16:47:48 -04:00
robert 33b27775b4 Bump version to v0.3
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
v0.3
2026-06-27 20:15:20 -04:00
robert 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>
v.03
2026-06-27 17:19:52 -04:00
robert 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
robert 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
robert b11fbe4715 Update README to reflect current feature set
Documents conditions list (now 22), combat log at /logs, hide player HP
toggle, inactive monster hiding, fullscreen button, and wake lock toggle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 11:59:36 -04:00
robert 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
robert 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
robert 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
robert 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
robert 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
robert 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