Rework backend #1
Reference in New Issue
Block a user
Delete Branch "rework-backend"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
- WebSocketImpl: native browser WebSocket if present, else ws pkg (node/jest) - ensureWs: use onopen/onmessage/onerror/onclose property handlers (browser API) instead of ws.on('open') EventEmitter (node-only) — was silent no-op in browser - norm(): strip 'artifacts/{APP_ID}/public/data/' prefix from all paths App passes firebase-prefixed paths; backend uses canonical campaigns/... - apply norm() to getDoc/getCollection/setDoc/updateDoc/deleteDoc/addDoc/ subscribeDoc/subscribeCollection/changeTypesForDocPath/changeTypesForCollPath Verified: STORAGE=ws boots, WS subscribe fires, backend broadcast -> browser live update (curl POST campaign -> appears without reload). Cross-device sync confirmed end-to-end.ws/memory storage mode never set the module-level `db` variable. 24 handlers guarded with `if (!db) return` early-exited, silently dropping all writes (create campaign, add encounter, participant CRUD, combat, logs). db stays a truthy sentinel object { __localStub: true } in non-firebase mode. All real reads/writes route through storage.*; db only used by guards. 56 frontend tests green. Verified via headed browser: create campaign flow works end-to-end (modal closes, campaign appears via WS realtime push).DisplayView missed in original M2 refactor — raw onSnapshot(doc(db,path)) survived. In ws/memory mode db is a stub sentinel, so raw SDK calls crash ('Expected first argument to collection() to be a CollectionReference...'). Reported by human testing player display after start combat. TDD: 1. RED: DisplayView.characterization.test.js asserts adapter.subscribeDoc called for campaign + encounter + activeDisplay paths. Adapter recorder (getAdapterCalls/resetAdapterCalls in firebase.js) instruments subscribe calls — catches raw-SDK bypass that firebase mock alone cannot (mock db satisfies raw onSnapshot, hiding the bug). 2. Fix: 2 raw onSnapshot sites in DisplayView -> storage.subscribeDoc. 3. GREEN: 2 new tests pass, 116 total green. Audit confirmed DisplayView was the ONLY remaining raw SDK site in App.js.scripts/replay-combat.js: drives full combat via live backend REST, computes turns through shared/turn.js. Player display (subscribed via WS) live-updates. Usage: node scripts/replay-combat.js [rounds] [delayMs] TODO.md: tracks M4 work. - Dead participants must NOT be skipped (still occupy initiative slot, death saves resolve on their turn). Saw in game Saturday. - JUMP_TURN_TO manual turn override.Root cause: addParticipant appended participant to participants[] without checking id uniqueness. Two participants with same id in array. On togglePause resume, turnOrderIds rebuilt via sort → dup id appears twice. nextTurn then stuck repeating that id (rotation breaks). This was the enabling step for BUG-1's full corruption (audit chain): pause blocks advance → totalTurns frozen → addParticipant re-adds same r${totalTurns} id → resume dup → nextTurn stuck. Fix: throw on duplicate id in addParticipant. Caller must use fresh id (crypto.randomUUID in App, replay already does). Evidence: - Test: 'addParticipant rejects duplicate id' (was test.skip, now live). - Pre-fix: 1 RED (Received function did not throw). - Post-fix: 50 green (shared), 23 green (server), 62 green (FE). - Reachability in normal app: low (App uses crypto.randomUUID) but no guard existed before. Defensive + unblocks BUG-1 isolation. No other behavior changed.src/tests/HideHpToggle.test.js: renders App, selects campaign, toggles hide-player-HP switch, asserts setDoc data includes activeCampaignId + activeEncounterId. RED: data only {hidePlayerHp:true}, both clobbered. Root cause proven with evidence (recorder): setDoc(activeDisplay/status, {hidePlayerHp:true}, {merge:true}) data written = {hidePlayerHp:true} ONLY activeCampaignId = undefined activeEncounterId = undefined setDoc = replace per contract. {merge:true} arg ignored. Toggle wipes encounter pointer → DisplayView reads null → 'Game Session Paused'. Fix: use updateDoc (patch), not setDoc. src/storage/firebase.js: adapter recorder now captures setDoc + updateDoc (data + opts). Was subscribe-only. Enables write-path assertions.{merge:true} ignored by setDoc (replace per contract). Each write wipes other fields on activeDisplay/status doc.REWORK_PLAN: M4 → ✅ (slot-array + DRY core, 500 rounds clean). M6 undo moved to TODO (feature work, not infra). M5 docker: nginx → Caddy (simpler WS config). Milestone numbering clarified. TODO: BUG-5 → FIXED. Added FEAT-M6 (transactional undo from plan), BUG-10 (deact+reactivate double-act, distinct from BUG-5), BUG-11 (FE Combat.scenario pre-existing crash). Pipeline updated.docker-compose.yml: two profiles. - backend: backend (node+ws+better-sqlite3, /data volume) + frontend (Caddy static build, STORAGE=ws, same-origin proxy) - firebase: existing Dockerfile + nginx (upstream path, untouched) Run: docker compose --profile backend up --build. OrbStack local now, remote docker context later. server/Dockerfile: node:18-alpine, workspaces (shared dep), rebuild better-sqlite3 for musl, DB at /data/tracker.sqlite. Dockerfile.ws: CRA build STORAGE=ws → caddy:2-alpine serves /srv. No backend URL baked (same-origin). Caddyfile: handle /api/* + handle /ws → backend:4001 (path preserved, mutually-exclusive handles so try_files SPA fallback never shadows proxy). handle { static try_files } last. HTTP basic auth block optional. src/storage/ws.js: same-origin defaults. Empty baseUrl = relative fetch (Caddy proxy). wsUrl derives from window.location (http→ws/https→wss). Fallback localhost for bare npm start dev. .dockerignore: add data/ scratch/ tmp/ (never bake into image). Keep Caddyfile in context (frontend build COPYs it). Smoke verified via OrbStack: - GET / → 200 (static SPA) - PUT/GET /api/doc roundtrip → JSON persists - WS /ws subscribe + change push → both work through proxy Firebase profile: pre-existing Dockerfile requires .env.local (hardcoded COPY on main, not changed here). User must create file. Not a regression.