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.
296 lines
9.3 KiB
Markdown
296 lines
9.3 KiB
Markdown
# 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
|
|
|
|
```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
|
|
|
|
### Local dev stack (one command)
|
|
|
|
```bash
|
|
./scripts/dev-start.sh # backend :4001 + frontend :3999, server mode
|
|
./scripts/dev-stop.sh # stop both
|
|
```
|
|
|
|
- backend: `npm run server:dev` (:4001, sqlite `data/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:
|
|
```bash
|
|
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:
|
|
```bash
|
|
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):
|
|
```bash
|
|
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
|
|
|
|
```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"
|
|
```
|
|
|
|
## Demo tool (NOT test)
|
|
|
|
`scripts/replay-combat.js` = live backend demo. Watch UI react to state changes.
|
|
|
|
```bash
|
|
# 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).
|
|
|
|
```bash
|
|
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:
|
|
|
|
1. rotation integrity (skip/dupe)
|
|
2. HP bounds (0 ≤ hp ≤ max, no NaN)
|
|
3. isActive consistency (inactive skipped; monster death auto-inactive; character/NPC death DM-controlled)
|
|
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 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
|
|
|
|
```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
|
|
- `server` → stub auth + db sentinel, route via `storage.*` 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:
|
|
```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
|
|
```
|