- .github/workflows/ci.yml: runs shared + server tests on push/PR - docs/DEVELOPMENT.md: setup, run, test, architecture, status - package.json: test:all script (shared + server suites)
4.5 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, existing)
App.js # ~2935 lines, Firebase direct (M2 abstracts this)
server/ # Backend: Express + ws + better-sqlite3
index.js # REST + WS bootstrap
db.js # SQLite schema, row mappers
handlers.js # action -> shared turn fn -> tx persist -> broadcast
server.test.js # integration tests
shared/ # Pure logic, no I/O (client + server + tests import)
turn.js # turn-order state machine
turn.characterization.test.js
docs/
REWORK_PLAN.md # milestone plan
DEVELOPMENT.md # this file
Setup
git clone git@github.com:keen99/ttrpg-initiative-tracker.git
cd ttrpg-initiative-tracker
npm install
Run
Frontend (dev server)
npm start # http://localhost:3000
Still uses Firebase by default. Set REACT_APP_FIREBASE_* in .env.local (copy env.example).
Backend (dev)
npm run server:dev # :4001, db: server/data/tracker.sqlite
# or direct with env:
DB_PATH=/tmp/tracker.sqlite PORT=4001 node server/index.js
Smoke check:
curl http://127.0.0.1:4001/health # -> {"ok":true}
Frontend not yet wired to backend — that is M2 (storage adapter + WS client).
Test
Three commands:
npm run test:all # runs shared/ + server/ suites in sequence
npm run shared:test # turn logic only (shared/ folder)
npm run server:test # backend REST + combat flow (server/ folder)
What each runs:
| Suite | What | Count |
|---|---|---|
shared/*.test.js |
turn FSM, pure functions | 39 |
server/*.test.js |
REST + combat flow, in-memory db | 7 |
Server tests use --forceExit (open WS handles). Tests spin server on random port, in-memory sqlite, tear down per test.
CI
.github/workflows/ci.yml runs on push/PR to main + rework-backend:
npm cisharedtestsservertests
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.
Backend architecture
Server-authoritative. Kills client-side last-write-wins races (root cause of skip bug).
Client (browser) Server
| |
|-- POST /api/.../nextTurn ----->| action
| | store.nextTurn():
| | shared.nextTurn(enc) -> patch
| | db tx: apply patch
| | addLog
| | broadcast(change)
|<---- WS {change} --------------| push to all subscribers
| |
Display / tablet |
|<---- WS {change} --------------| same push
- SQLite owns truth. Single writer (server). WAL mode.
- 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
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.
Status
| Milestone | State |
|---|---|
| 0 repo/branch | ✅ done |
| 1 backend + tests | ✅ done |
| 2 frontend WS adapter | ⬜ next |
| 3 characterization tests | ⬜ |
| 4 skip fix + manual override | ⬜ |
| 5 docker compose | ⬜ |
| 6 undo rework | ⬜ |
| 7 playwright e2e | ⬜ deferred |
See docs/REWORK_PLAN.md for full plan.
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