Wake lock (Prevent Sleep) toggles now persist across reloads via localStorage in both AdminView and DisplayView. Buttons repositioned inline in AdminView campaigns header bar (was floating overlay causing overlap on tablets). DisplayView buttons persist localStorage too. Wake lock acquire failure now shows toast with fix hint (HTTPS or Chrome flag). Fullscreenchange listener re-acquires wake lock (Android discards on screen off). dev-start.sh: auto-detects LAN IP (en0/en1), frontend binds 0.0.0.0, backend URL inlined as LAN IP so phones reach backend. DANGEROUSLY_DISABLE_HOST_CHECK for LAN access. Outputs LAN URL + wake lock flag instructions. Docs: README 'Prevent Sleep (Wake Lock)' section covering secure context requirement, Android Chrome flag workaround for LAN testing, iOS Safari standalone PWA bug. DEVELOPMENT.md LAN access + wake lock note.
9.3 KiB
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 tool (NOT test)
replay-combat.js # live backend demo
tests/
audit/ # exploratory bug-finders (manual, Math.random)
audit-rotation.js # rotation invariant
audit-state.js # 9 invariant classes
scratch/ # gitignored: throwaway repro/exploration
docs/
REWORK_PLAN.md
DEVELOPMENT.md # this file
GLOSSARY.md # domain terms (turn vs round, etc)
ENCOUNTER_BUILDER.md # DM interface guide
TESTING.md # test + automation ops
Setup
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
Local dev stack (one command)
./scripts/dev-start.sh # backend :4001 + frontend :3999, server mode
./scripts/dev-stop.sh # stop both
- backend:
npm run server:dev(:4001, sqlitedata/tracker.sqlite) - frontend:
npm start(:3999,REACT_APP_STORAGE=server) - logs:
tmp/server.log,tmp/fe.log
Idempotent: if port busy, leaves existing proc as-is.
LAN access (phones): frontend binds 0.0.0.0, LAN IP auto-detected.
Other devices hit http://<lan-ip>:3999.
Wake Lock (Prevent Sleep) on LAN IP: requires secure context.
Plain HTTP on LAN IP = wake lock silently fails. Android Chrome flag workaround:
chrome://flags/#unsafely-treat-insecure-origin-as-secure → add
http://<lan-ip>:3999 (with port), enable, relaunch. See README for full details.
Smoke check:
curl http://127.0.0.1:4001/health # -> {"ok":true}
Never put db in /tmp (wipe risk). Use ./data/ (gitignored) or docker volume.
Manual (fallback)
Backend:
npm run server:dev # :4001, db: server/data/tracker.sqlite
# or direct:
DB_PATH=./data/tracker.sqlite PORT=4001 node server/index.js
Frontend (server mode):
REACT_APP_STORAGE=server \
REACT_APP_BACKEND_URL=http://127.0.0.1:4001 \
REACT_APP_BACKEND_REALTIME_URL=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
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
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:
CI=true npx react-scripts test --watchAll=false --testPathIgnorePatterns="Combat.scenario"
Demo tool (NOT test)
scripts/replay-combat.js = live backend demo. Watch UI react to state changes.
# start backend + frontend first
node scripts/replay-combat.js [rounds] [delayMs]
# defaults: 100 rounds, 200ms/step
Coverage per round: damage, heal, all 22 conditions, toggleActive, removeParticipant, addParticipant (reinforcements), updateParticipant, pause/resume, reorderParticipants, endEncounter. Revives dead each round to sustain full round count.
Audit tools (NOT unit tests)
tests/audit/ = exploratory, Math.random, non-deterministic. Manual run.
Unit tests ({shared,server,src}/tests/) lock known bugs deterministically.
audit-rotation.js
Pure turn.js simulation of replay op sequence. Detects rotation violations (skip/dupe per round). Found BUG-1 (addParticipant + pause corrupts rotation).
node tests/audit/audit-rotation.js
Bisect: comment/uncomment op blocks to isolate triggering combo.
audit-state.js
Runs pure turn.js combat, audits 9 invariant classes per round:
- rotation integrity (skip/dupe)
- HP bounds (0 ≤ hp ≤ max, no NaN)
- isActive consistency (inactive skipped; monster death auto-inactive; character/NPC death DM-controlled)
- turnOrder no dup ids
- turnOrder ids all active
- currentTurn valid + active
- deathSave range (0 ≤ saves ≤ 3, reset on revive)
- removeParticipant orphans
- undo support
node tests/audit/audit-state.js [rounds] # default 100
Current state (post BUG-1/2 fix): 0 violations / 100 rounds.
See TODO.md for known bugs.
Scratch
scratch/ = gitignored throwaway. Repro scripts, exploration, debug.
Not committed. Use freely, delete anytime.
Build
npm run build # CRA production build -> build/
Docker build (existing, frontend-only):
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 initserver→ stub auth + db sentinel, route viastorage.*adapter
Test layers
- Layer 1: App vs firebase mock. Proves adapter call shape. Never exercises server adapter.
- Layer 2: server 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:
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(offmain)
git fetch upstream # pull friend's changes
git merge upstream/main # rebase our branch onto his