2026-06-28 17:14:51 -04:00
|
|
|
# Development
|
|
|
|
|
|
|
|
|
|
TTRPG Initiative Tracker — fork with self-hosted backend. Monorepo via npm workspaces.
|
|
|
|
|
|
|
|
|
|
## Prerequisites
|
|
|
|
|
|
|
|
|
|
- Node.js 22+
|
|
|
|
|
- npm 10+
|
|
|
|
|
|
|
|
|
|
## Layout
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
/
|
2026-06-29 16:23:34 -04:00
|
|
|
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
|
2026-06-29 17:11:46 -04:00
|
|
|
scripts/ # manual demo tool (NOT test)
|
2026-06-29 16:23:34 -04:00
|
|
|
replay-combat.js # live backend demo
|
2026-06-29 17:11:46 -04:00
|
|
|
tests/
|
|
|
|
|
audit/ # exploratory bug-finders (manual, Math.random)
|
|
|
|
|
audit-rotation.js # rotation invariant
|
|
|
|
|
audit-state.js # 9 invariant classes
|
|
|
|
|
scratch/ # gitignored: throwaway repro/exploration
|
2026-06-28 17:14:51 -04:00
|
|
|
docs/
|
2026-06-29 16:23:34 -04:00
|
|
|
REWORK_PLAN.md
|
|
|
|
|
DEVELOPMENT.md # this file
|
|
|
|
|
GLOSSARY.md # domain terms (turn vs round, etc)
|
2026-07-01 19:16:12 -04:00
|
|
|
ENCOUNTER_BUILDER.md # DM interface guide
|
|
|
|
|
TESTING.md # test + automation ops
|
2026-06-28 17:14:51 -04:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Setup
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
git clone git@github.com:keen99/ttrpg-initiative-tracker.git
|
|
|
|
|
cd ttrpg-initiative-tracker
|
|
|
|
|
npm install
|
2026-06-29 16:23:34 -04:00
|
|
|
git config core.hooksPath .githooks # enable pre-push test gate
|
2026-06-28 17:14:51 -04:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Run
|
|
|
|
|
|
|
|
|
|
### Backend (dev)
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
npm run server:dev # :4001, db: server/data/tracker.sqlite
|
2026-06-29 16:23:34 -04:00
|
|
|
# or direct:
|
|
|
|
|
DB_PATH=./data/tracker.sqlite PORT=4001 node server/index.js
|
2026-06-28 17:14:51 -04:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Smoke check:
|
|
|
|
|
```bash
|
|
|
|
|
curl http://127.0.0.1:4001/health # -> {"ok":true}
|
|
|
|
|
```
|
|
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
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.
|
2026-06-28 17:14:51 -04:00
|
|
|
|
|
|
|
|
## Test
|
|
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
### Commands
|
2026-06-28 17:14:51 -04:00
|
|
|
|
|
|
|
|
```bash
|
2026-06-29 16:23:34 -04:00
|
|
|
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)
|
2026-06-28 17:14:51 -04:00
|
|
|
```
|
|
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
### Suites
|
2026-06-28 17:14:51 -04:00
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
| 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 |
|
2026-06-28 17:14:51 -04:00
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
Total: 134 green + 1 validated RED (skipped).
|
2026-06-28 17:14:51 -04:00
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
### Test types
|
2026-06-28 17:14:51 -04:00
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
- **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
|
2026-06-28 17:16:23 -04:00
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
```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:
|
2026-06-28 17:16:23 -04:00
|
|
|
|
|
|
|
|
```bash
|
2026-06-29 16:23:34 -04:00
|
|
|
CI=true npx react-scripts test --watchAll=false --testPathIgnorePatterns="Combat.scenario"
|
2026-06-28 17:16:23 -04:00
|
|
|
```
|
|
|
|
|
|
2026-06-29 17:11:46 -04:00
|
|
|
## Demo tool (NOT test)
|
2026-06-29 16:23:34 -04:00
|
|
|
|
2026-06-29 17:11:46 -04:00
|
|
|
`scripts/replay-combat.js` = live backend demo. Watch UI react to state changes.
|
2026-06-29 16:23:34 -04:00
|
|
|
|
2026-06-28 17:16:23 -04:00
|
|
|
```bash
|
2026-06-29 17:11:46 -04:00
|
|
|
# start backend + frontend first
|
2026-06-29 16:23:34 -04:00
|
|
|
node scripts/replay-combat.js [rounds] [delayMs]
|
|
|
|
|
# defaults: 100 rounds, 200ms/step
|
2026-06-28 17:16:23 -04:00
|
|
|
```
|
|
|
|
|
|
2026-06-29 17:11:46 -04:00
|
|
|
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)
|
2026-06-29 16:23:34 -04:00
|
|
|
|
2026-06-29 17:11:46 -04:00
|
|
|
`tests/audit/` = exploratory, Math.random, non-deterministic. Manual run.
|
|
|
|
|
Unit tests (`{shared,server,src}/tests/`) lock known bugs deterministically.
|
2026-06-29 16:23:34 -04:00
|
|
|
|
2026-06-29 17:11:46 -04:00
|
|
|
### 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).
|
2026-06-29 16:23:34 -04:00
|
|
|
|
|
|
|
|
```bash
|
2026-06-29 17:11:46 -04:00
|
|
|
node tests/audit/audit-rotation.js
|
2026-06-29 16:23:34 -04:00
|
|
|
```
|
|
|
|
|
|
2026-06-29 17:11:46 -04:00
|
|
|
Bisect: comment/uncomment op blocks to isolate triggering combo.
|
2026-06-29 16:23:34 -04:00
|
|
|
|
2026-06-29 17:11:46 -04:00
|
|
|
### audit-state.js
|
2026-06-29 16:23:34 -04:00
|
|
|
|
|
|
|
|
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
|
2026-06-29 17:11:46 -04:00
|
|
|
node tests/audit/audit-state.js [rounds] # default 100
|
2026-06-29 16:23:34 -04:00
|
|
|
```
|
|
|
|
|
|
2026-06-29 17:11:46 -04:00
|
|
|
Current state (post BUG-1/2 fix): 0 violations / 100 rounds.
|
2026-06-29 16:23:34 -04:00
|
|
|
|
|
|
|
|
See `TODO.md` for known bugs.
|
2026-06-28 17:14:51 -04:00
|
|
|
|
2026-06-29 17:11:46 -04:00
|
|
|
## Scratch
|
|
|
|
|
|
|
|
|
|
`scratch/` = gitignored throwaway. Repro scripts, exploration, debug.
|
|
|
|
|
Not committed. Use freely, delete anytime.
|
|
|
|
|
|
2026-06-28 17:14:51 -04:00
|
|
|
## 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.
|
|
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
## Storage architecture
|
2026-06-28 17:14:51 -04:00
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
### 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.
|
2026-06-28 17:14:51 -04:00
|
|
|
|
|
|
|
|
```
|
|
|
|
|
Client (browser) Server
|
|
|
|
|
| |
|
2026-06-29 16:23:34 -04:00
|
|
|
|-- 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
|
2026-06-28 17:14:51 -04:00
|
|
|
| |
|
|
|
|
|
Display / tablet |
|
|
|
|
|
|<---- WS {change} --------------| same push
|
|
|
|
|
```
|
|
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
### 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).
|
2026-06-28 17:14:51 -04:00
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
### 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
|
|
|
|
|
```
|
2026-06-28 17:14:51 -04:00
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
Already configured on this checkout after `git config core.hooksPath .githooks`.
|
2026-06-28 17:14:51 -04:00
|
|
|
|
|
|
|
|
## Status
|
|
|
|
|
|
|
|
|
|
| Milestone | State |
|
|
|
|
|
|---|---|
|
|
|
|
|
| 0 repo/branch | ✅ done |
|
|
|
|
|
| 1 backend + tests | ✅ done |
|
2026-06-29 16:23:34 -04:00
|
|
|
| 2 frontend WS adapter | ✅ done |
|
|
|
|
|
| 3 characterization tests | ✅ done (134 green) |
|
|
|
|
|
| 4 skip fix + manual override | ⬜ next |
|
2026-06-28 17:14:51 -04:00
|
|
|
| 5 docker compose | ⬜ |
|
|
|
|
|
| 6 undo rework | ⬜ |
|
|
|
|
|
| 7 playwright e2e | ⬜ deferred |
|
|
|
|
|
|
2026-06-29 16:23:34 -04:00
|
|
|
See `docs/REWORK_PLAN.md` for full plan, `TODO.md` for known bugs.
|
2026-06-28 17:14:51 -04:00
|
|
|
|
|
|
|
|
## 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
|
|
|
|
|
```
|