Rework backend #1

Merged
robert merged 86 commits from rework-backend into main 2026-07-01 19:29:34 -04:00
2 changed files with 206 additions and 60 deletions
Showing only changes of commit 912c493974 - Show all commits
+154 -60
View File
@@ -11,19 +11,27 @@ TTRPG Initiative Tracker — fork with self-hosted backend. Monorepo via npm wor
``` ```
/ /
package.json # workspaces root package.json # workspaces root
src/ # React frontend (CRA, existing) src/ # React frontend (CRA)
App.js # ~2935 lines, Firebase direct (M2 abstracts this) App.js # main app (~2900 lines)
server/ # Backend: generic KV doc store (firebase mirror) storage/ # adapter layer (firebase/ws/memory + contract)
index.js # REST (doc/coll/batch) + WS bootstrap __mocks__/firebase/ # firebase SDK mock (Layer 1 tests)
db.js # SQLite docs table, KV ops, broadcast tests/ # frontend tests
ws-contract.test.js # adapter vs live backend (Layer 2) server/ # Backend: generic KV doc store (firebase mirror)
shared/ # Pure logic, no I/O (client + server + tests import) index.js # REST (doc/coll/batch) + WS bootstrap
turn.js # turn-order state machine db.js # SQLite docs table, KV ops, broadcast
turn.characterization.test.js 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/ docs/
REWORK_PLAN.md # milestone plan REWORK_PLAN.md
DEVELOPMENT.md # this file DEVELOPMENT.md # this file
GLOSSARY.md # domain terms (turn vs round, etc)
``` ```
## Setup ## Setup
@@ -32,24 +40,17 @@ TTRPG Initiative Tracker — fork with self-hosted backend. Monorepo via npm wor
git clone git@github.com:keen99/ttrpg-initiative-tracker.git git clone git@github.com:keen99/ttrpg-initiative-tracker.git
cd ttrpg-initiative-tracker cd ttrpg-initiative-tracker
npm install npm install
git config core.hooksPath .githooks # enable pre-push test gate
``` ```
## Run ## Run
### Frontend (dev server)
```bash
npm start # http://localhost:3000
```
Still uses Firebase by default. Set `REACT_APP_FIREBASE_*` in `.env.local` (copy `env.example`).
### Backend (dev) ### Backend (dev)
```bash ```bash
npm run server:dev # :4001, db: server/data/tracker.sqlite npm run server:dev # :4001, db: server/data/tracker.sqlite
# or direct with env: # or direct:
DB_PATH=/tmp/tracker.sqlite PORT=4001 node server/index.js DB_PATH=./data/tracker.sqlite PORT=4001 node server/index.js
``` ```
Smoke check: Smoke check:
@@ -57,44 +58,114 @@ Smoke check:
curl http://127.0.0.1:4001/health # -> {"ok":true} curl http://127.0.0.1:4001/health # -> {"ok":true}
``` ```
Frontend not yet wired to backend — that is M2 (storage adapter + WS client). 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 ## Test
Three commands: ### Commands
```bash ```bash
npm run test:all # runs shared/ + server/ suites in sequence npm run test:all # shared + server (fast, no frontend)
npm run shared:test # turn logic only (shared/ folder) npm run shared:test # pure turn logic
npm run server:test # backend ws-contract (adapter vs live backend) npm run server:test # adapter vs live backend
npm test # CRA frontend (src/tests/, slow with scenario)
``` ```
What each runs: ### Suites
| Suite | What | Count | | Suite | Location | What | Count |
|---|---|---| |---|---|---|---|
| `shared/*.test.js` | turn FSM, pure functions | 39 | | Unit (turn logic) | `shared/tests/` | pure nextTurn, rotation, pause-add | 50 (1 skip) |
| `server/*.test.js` | REST + combat flow, in-memory db | 7 | | 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 |
Server tests use `--forceExit` (open WS handles). Tests spin server on random port, in-memory sqlite, tear down per test. Total: 134 green + 1 validated RED (skipped).
### Local pipeline (pre-push hook) ### Test types
Private repo = no free GitHub Actions. Tests run locally via git hook. - **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.
`.githooks/pre-push` runs `npm run test:all` before every push. ### Running one file / pattern
Enable on clone (do once):
```bash ```bash
git config core.hooksPath .githooks npm test --workspace shared -- --testPathPattern=round-rotation
CI=true npx react-scripts test --watchAll=false src/tests/App.characterization.test.js
``` ```
Already configured on this checkout. Skip with: ### Scenario test is slow
`Combat.scenario.test.js` runs 100 combat rounds through rendered App — 240s timeout by design. Skip when iterating:
```bash ```bash
git push --no-verify CI=true npx react-scripts test --watchAll=false --testPathIgnorePatterns="Combat.scenario"
``` ```
Future: when repo goes public, free GH Actions viable. Then add `.github/workflows/ci.yml`. ## 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 ## Build
@@ -110,33 +181,56 @@ docker run -p 8080:80 --rm ttrpg-initiative-tracker
Full-stack docker-compose arrives in M5. Full-stack docker-compose arrives in M5.
## Backend architecture ## Storage architecture
Server-authoritative. Kills client-side last-write-wins races (root cause of skip bug). ### 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 Client (browser) Server
| | | |
|-- POST /api/.../nextTurn ----->| action |-- storage.setDoc(path,data) -->| REST PUT /api/doc
| | store.nextTurn(): |<---- 200 ----------------------|
| | shared.nextTurn(enc) -> patch | |
| | db tx: apply patch |-- storage.subscribeDoc(path) -->| WS subscribe
| | addLog |<---- WS {initial} --------------| immediate value
| | broadcast(change) | ... |
|<---- WS {change} --------------| push to all subscribers |<---- WS {change} --------------| on any write to path
| | | |
Display / tablet | Display / tablet |
|<---- WS {change} --------------| same push |<---- WS {change} --------------| same push
``` ```
- **SQLite** owns truth. Single writer (server). WAL mode. ### Path normalization
- **shared/turn.js** = pure logic, ported verbatim from `App.js`. Bugs preserved for M3 characterization, fixed in M4.
- **WS** = real-time push (replaces Firebase `onSnapshot`). Client subscribes to a key (`'campaigns'`, `'encounter:id'`, `'activeDisplay'`...), server pushes on change.
- **Actions not results.** Client sends "do X", server computes X, persists, broadcasts. No client-side state mutation.
## Storage backend choice App passes firebase-prefixed paths (`artifacts/{APP_ID}/public/data/campaigns/...`). Adapter `norm()` strips prefix → bare canonical (`campaigns/...`). All impls share identity (contract test).
Browser sandbox cannot touch filesystem. Cross-device (DM + tablet + player view) requires a real backend owning the DB file. SQLite = single file, docker volume, trivial backup. Postgres deferred until public multiuser exposure. ### 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 ## Status
@@ -144,14 +238,14 @@ Browser sandbox cannot touch filesystem. Cross-device (DM + tablet + player view
|---|---| |---|---|
| 0 repo/branch | ✅ done | | 0 repo/branch | ✅ done |
| 1 backend + tests | ✅ done | | 1 backend + tests | ✅ done |
| 2 frontend WS adapter | ⬜ next | | 2 frontend WS adapter | ✅ done |
| 3 characterization tests | | | 3 characterization tests | ✅ done (134 green) |
| 4 skip fix + manual override | ⬜ | | 4 skip fix + manual override | ⬜ next |
| 5 docker compose | ⬜ | | 5 docker compose | ⬜ |
| 6 undo rework | ⬜ | | 6 undo rework | ⬜ |
| 7 playwright e2e | ⬜ deferred | | 7 playwright e2e | ⬜ deferred |
See `docs/REWORK_PLAN.md` for full plan. See `docs/REWORK_PLAN.md` for full plan, `TODO.md` for known bugs.
## Git ## Git
+52
View File
@@ -0,0 +1,52 @@
# scripts/
Manual tools. NOT unit tests. Math.random, non-deterministic.
Used to FIND bugs. Unit tests (in `*/tests/`) LOCK them.
## replay-combat.js
Drives full combat through live backend via ws adapter (same contract as App).
Player display live-updates. Use to watch UI react to state changes.
```bash
# start backend + frontend first (see docs/DEVELOPMENT.md)
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-rotation.js
Pure turn.js simulation of replay op sequence. Detects rotation violations
(skip/dupe per round). Pinpointed BUG-1 (addParticipant + pause corrupts).
```bash
node scripts/audit-rotation.js
```
Bisect: comment/uncomment op blocks to isolate triggering combo.
## audit-state.js
Expanded invariant bug-finder. 9 check classes per round:
1. rotation integrity
2. HP bounds
3. isActive consistency
4. turnOrder no dup ids
5. turnOrder ids all active
6. currentTurn valid + active
7. deathSave range + reset on revive
8. removeParticipant orphans
9. undo support
```bash
node scripts/audit-state.js [rounds] # default 100
```
See TODO.md for bugs found.