# 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
```
