diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9fe22b8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI + +on: + push: + branches: [main, rework-backend] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + - run: npm ci + - name: shared tests + run: npm test --workspace shared + - name: server tests + run: npm test --workspace server diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..d2b8371 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,155 @@ +# 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 + +```bash +git clone git@github.com:keen99/ttrpg-initiative-tracker.git +cd ttrpg-initiative-tracker +npm install +``` + +## 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) + +```bash +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: +```bash +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: + +```bash +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 ci` +- `shared` tests +- `server` tests + +## 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. + +## 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` (off `main`) + +```bash +git fetch upstream # pull friend's changes +git merge upstream/main # rebase our branch onto his +``` diff --git a/package.json b/package.json index 52a8fea..da9aea7 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "eject": "react-scripts eject", "server:dev": "npm run dev --workspace server", "server:test": "npm test --workspace server", - "shared:test": "npm test --workspace shared" + "shared:test": "npm test --workspace shared", + "test:all": "npm run shared:test && npm run server:test" }, "eslintConfig": { "extends": [