# Development TTRPG Initiative Tracker — fork with self-hosted backend. Monorepo via npm workspaces. ## Prerequisites - Node.js 22+ - npm 10+ ## Layout ``` / package.json # workspaces root src/ # React frontend (CRA) App.js # main app (~2900 lines) storage/ # adapter layer (firebase/ws/memory + contract) __mocks__/firebase/ # firebase SDK mock (Layer 1 tests) tests/ # frontend tests server/ # Backend: generic KV doc store (firebase mirror) index.js # REST (doc/coll/batch) + WS bootstrap db.js # SQLite docs table, KV ops, broadcast tests/ # backend + adapter-vs-live tests shared/ # Pure logic, no I/O (client + server + tests import) turn.js # turn-order state machine tests/ # turn logic tests scripts/ # manual demo/audit tools (NOT unit tests) replay-combat.js # live backend demo audit-rotation.js # exploratory rotation bug-finder audit-state.js # exploratory invariant bug-finder (9 classes) docs/ REWORK_PLAN.md DEVELOPMENT.md # this file GLOSSARY.md # domain terms (turn vs round, etc) ``` ## Setup ```bash git clone git@github.com:keen99/ttrpg-initiative-tracker.git cd ttrpg-initiative-tracker npm install git config core.hooksPath .githooks # enable pre-push test gate ``` ## Run ### Backend (dev) ```bash npm run server:dev # :4001, db: server/data/tracker.sqlite # or direct: DB_PATH=./data/tracker.sqlite PORT=4001 node server/index.js ``` Smoke check: ```bash curl http://127.0.0.1:4001/health # -> {"ok":true} ``` Never put db in `/tmp` (wipe risk). Use `./data/` (gitignored) or docker volume. ### Frontend (dev server, ws mode) ```bash REACT_APP_STORAGE=ws \ REACT_APP_BACKEND_URL=http://127.0.0.1:4001 \ REACT_APP_BACKEND_WS=ws://127.0.0.1:4001/ws \ BROWSER=none PORT=3999 \ npm start ``` Opens http://127.0.0.1:3999/. Admin view `/`, player view `/display`. Firebase mode (default, upstream): set `REACT_APP_FIREBASE_*` in `.env.local` (copy `env.example`). `STORAGE_MODE=firebase` falls through to real SDK. ## Test ### Commands ```bash npm run test:all # shared + server (fast, no frontend) npm run shared:test # pure turn logic npm run server:test # adapter vs live backend npm test # CRA frontend (src/tests/, slow with scenario) ``` ### Suites | Suite | Location | What | Count | |---|---|---|---| | Unit (turn logic) | `shared/tests/` | pure nextTurn, rotation, pause-add | 50 (1 skip) | | Integration (adapter vs backend) | `server/tests/` | ws adapter through live REST/WS | 23 | | Characterization (UI) | `src/tests/` | locks current App.js behavior | 62 | | Scenario | `src/tests/Combat.scenario.test.js` | 100-round full combat (240s) | 289 phases | Total: 134 green + 1 validated RED (skipped). ### Test types - **Unit** = pure logic, fast, no I/O. Locks behavior of single functions. - **Integration** = real backend per test, adapter translation verified. - **Characterization** = render App via mock, assert current (buggy or not) UI behavior. Not desired-state. - **Scenario** = end-to-end flow through rendered App, asserts full sequence completes. - **Contract** = same spec run against every storage impl (memory, ws, firebase). Catches adapter drift. ### Running one file / pattern ```bash npm test --workspace shared -- --testPathPattern=round-rotation CI=true npx react-scripts test --watchAll=false src/tests/App.characterization.test.js ``` ### Scenario test is slow `Combat.scenario.test.js` runs 100 combat rounds through rendered App — 240s timeout by design. Skip when iterating: ```bash CI=true npx react-scripts test --watchAll=false --testPathIgnorePatterns="Combat.scenario" ``` ## Manual tools (NOT tests) `scripts/` = exploratory. Math.random, non-deterministic. Used to find bugs, unit tests lock them. ### replay-combat.js — live demo Drives full combat through real backend via ws adapter (same contract as App). Player display live-updates. ```bash # start backend + frontend first (see Run) node scripts/replay-combat.js [rounds] [delayMs] # defaults: 100 rounds, 200ms/step ``` Coverage per round: damage, heal, all 22 conditions, toggleActive (mark inactive/reactivate), removeParticipant, addParticipant (reinforcements), updateParticipant (edit), pause/resume, reorderParticipants, endEncounter. Revives dead each round to sustain 100 rounds. ### audit-rotation.js — rotation bug-finder Pure turn.js simulation of replay op sequence. Detects rotation violations (skip/dupe per round). Pinpointed BUG-1 (addParticipant + pause corrupts rotation). ```bash node scripts/audit-rotation.js ``` Bisect mode: comment/uncomment op blocks to isolate which combo triggers. ### audit-state.js — expanded invariant bug-finder Runs pure turn.js combat, audits 9 invariant classes per round: 1. rotation integrity (skip/dupe) 2. HP bounds (0 ≤ hp ≤ max, no NaN) 3. isActive consistency (dead = inactive) 4. turnOrder no dup ids 5. turnOrder ids all active 6. currentTurn valid + active 7. deathSave range (0 ≤ saves ≤ 3, reset on revive) 8. removeParticipant orphans 9. undo support ```bash node scripts/audit-state.js [rounds] ``` 100-round run: 128 violations, all BUG-1/BUG-2 family (4 symptom faces). Clean: HP, isActive, deathSave, conditions, removal. See `TODO.md` for known bugs. ## Build ```bash npm run build # CRA production build -> build/ ``` Docker build (existing, frontend-only): ```bash docker build -t ttrpg-initiative-tracker . docker run -p 8080:80 --rm ttrpg-initiative-tracker ``` Full-stack docker-compose arrives in M5. ## Storage architecture ### Generic KV doc store Backend = firebase mirror. Single `docs` table: `path` (PK), `parent`, `data` (JSON), `updated_at`. Opaque JSON at arbitrary path strings. No shape-specific endpoints. App logic stays client-side. ``` Client (browser) Server | | |-- storage.setDoc(path,data) -->| REST PUT /api/doc |<---- 200 ----------------------| | | |-- storage.subscribeDoc(path) -->| WS subscribe |<---- WS {initial} --------------| immediate value | ... | |<---- WS {change} --------------| on any write to path | | Display / tablet | |<---- WS {change} --------------| same push ``` ### Path normalization App passes firebase-prefixed paths (`artifacts/{APP_ID}/public/data/campaigns/...`). Adapter `norm()` strips prefix → bare canonical (`campaigns/...`). All impls share identity (contract test). ### STORAGE_MODE flow `getStorageMode()` reads `REACT_APP_STORAGE` env (default `firebase`). - `firebase` → real SDK init - `ws`/`memory` → stub auth + db sentinel, route via `storage.*` adapter ## Test layers - **Layer 1**: App vs firebase mock. Proves adapter call shape. Never exercises ws adapter. - **Layer 2**: ws adapter vs live backend. Proves translation + path identity. Both required — Layer 1 alone misses adapter bugs (path mismatch, no-op players, ws.on EventEmitter vs browser handlers). ## Local pipeline (pre-push hook) Private repo = no free GitHub Actions. Tests run locally via git hook. `.githooks/pre-push` runs `npm run test:all` (shared + server, fast). Frontend tests not gated (slow). Skip: ```bash git push --no-verify ``` Already configured on this checkout after `git config core.hooksPath .githooks`. ## Status | Milestone | State | |---|---| | 0 repo/branch | ✅ done | | 1 backend + tests | ✅ done | | 2 frontend WS adapter | ✅ done | | 3 characterization tests | ✅ done (134 green) | | 4 skip fix + manual override | ⬜ next | | 5 docker compose | ⬜ | | 6 undo rework | ⬜ | | 7 playwright e2e | ⬜ deferred | See `docs/REWORK_PLAN.md` for full plan, `TODO.md` for known bugs. ## Git - `origin` = `github.com:keen99/ttrpg-initiative-tracker` (this fork) - `upstream` = `code.draft13.com/robert/ttrpg-initiative-tracker` (friend's Gitea, read-only) - work branch: `rework-backend` (off `main`) ```bash git fetch upstream # pull friend's changes git merge upstream/main # rebase our branch onto his ```