From 835a4663f7a3f3f7871927e40870e1028f6844e8 Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Mon, 6 Jul 2026 23:22:19 -0400 Subject: [PATCH] Docker build cache: .dockerignore, cache mount, layer reorder, restart script - .dockerignore: exclude node_modules/.git/tmp/data/logs - Dockerfile: BuildKit cache mount for npm, --omit=dev at install, drop npm prune step - Reorder layers: shared/src/frontend copy before build, server copy + better-sqlite3 rebuild after. Server/shared changes no longer invalidate frontend build layer unless shared (FE dep) changes - Remove duplicate shared COPY in runtime stage (bundled in FE build) - docker/restart.sh: stop + rebuild + start in one script --- .dockerignore | 11 ++++ TODO.md | 149 ---------------------------------------------- docker/Dockerfile | 19 +++--- docker/restart.sh | 21 +++++++ server/index.js | 1 + 5 files changed, 43 insertions(+), 158 deletions(-) create mode 100644 .dockerignore create mode 100755 docker/restart.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..aba797c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +node_modules +**/node_modules +.git +tmp +data +*.log +npm-debug.log* +.DS_Store +.env* +coverage +.nyc_output diff --git a/TODO.md b/TODO.md index 0ce3028..08c4517 100644 --- a/TODO.md +++ b/TODO.md @@ -65,25 +65,6 @@ not sure good way to do this - Ambiguous label. May expand work based on what NPC means here (ally? monster? display-only? skip in turn order?). Clarify intent before UX changes. -### FEAT: first-class undo/redo UI buttons (B --- do now) -- Toolbar buttons ↶/↷ in AdminView header, not buried in /logs. -- Undo = revert latest non-undone log. Redo = re-apply latest undone. -- Uses current 2-write undo (non-tx). Race safety = log refactor later. -- Disabled when stack empty. Keyboard shortcuts (cmd+z / cmd+shift+z). - -### FEAT-LOG: unified log refactor (was FEAT-2 + M6, batched) -- Single event schema, one source of truth: - `{ ts, type, payload, undo_payload, undone, encounterId, - snapshot:{ round, currentTurnParticipantId, turnOrderIds, activeIds } }` -- Common format consumed by: UI log view, download/copy export, - replay-combat, analyze-turns. One shape, four consumers. -- Transactional undo: server endpoint `POST /api/undo/:eventId`. Single - SQLite tx applies undo_payload + flips `undone`. Replaces fragile 2-write - (log update + encounter update as separate calls). -- Download/copy: exports event stream as JSON for offline analysis. -- replay-combat + analyze-turns rewritten to emit/consume same event shape. -- Migration: keep old log entries readable; new format for new writes. - ### quality of life fix: 2. UI says "Campaign Characters", field is players --- naming mismatch (separate concern, flag for later) @@ -95,133 +76,3 @@ not sure good way to do this ## FEAT - clarify what end encounter does and what initiatives reset means ## Done (history) - -### FEAT: first-class undo/redo UI buttons (DONE) -- ↶/↷ pills in InitiativeControls, always visible when encounter open. -- Undo = latest non-undone log (per encounter). Redo = latest undone. -- encounterPath added to all 14 log contexts (filter key). -- redo:patch (forward) added to undoData. Real redo replays forward state. -- Disabled when stack empty. Tooltip shows target action. -- Uses current 2-write undo (non-tx). Race safety = FEAT-LOG refactor. - -### Architecture: 1-list turn order model (slot, never sort) -- Single source: turnOrderIds === participants.map(id). No re-sort after - startEncounter. nextTurn skips inactive (predicate), inactive stay in slot. -- Drag (reorder) = same-init tie-break only. Cross-init blocked. -- startEncounter sorts ALL participants by init once, then frozen. -- addParticipant/updateParticipant slot by init (slotIndexForInit), preserve - drag order. Display renders participants[] directly (no sort). -- Static guard test errs if `.sort(` reintroduced outside allowlist. -- Design doc: docs/INITIATIVE_ORDERING.md. - -### Single source of truth: combat logic -- All 15 App.js handlers delegate to @ttrpg/shared. ~498 lines inline dupes deleted. -- shared/turn.js = only place turn logic lives. - -### Storage parity (firebase + server adapters) -- Neutral queryConstraints ({__type:'orderBy'|'limit'}) honored by both adapters. -- Shared contract test runs both identically. Memory adapter deleted; factory - throws on unknown mode. -- ws storage mode renamed to server (env var + adapter name). - -### Logging contract -- Every mutating op logs message + undo payload. No-op = null log. -- Structural enforcement: per-op contract test (turn.logging.test.js) + - static source-scan guard (static.no-unlogged.test.js). - -### Custom conditions per campaign (DONE) -- Freeform per-campaign conditions. Add applies to participant + persists to - campaign palette in one step. Badge render uses merged allConditions - (built-ins + custom). toggleCondition accepts any string. Dedup - case-insensitive. maxLength 40. Combat + replay tests prove arbitrary - string ids survive round-trip. - -### Death saves: D&D 5e status model (DONE) -- `status` is source of truth: conscious, dying, stable, dead. -- Characters and NPCs use death saves; monsters skip death saves and become dead/inactive at 0 HP. -- Death-save actions: Success, Fail, Nat1, Nat20, Stabilize. -- Revive: dead → 0 HP, stable/unconscious, active. -- Dead characters/NPCs stay in encounter/initiative until DM removes or marks inactive. -- Player display hides inactive participants; DM display keeps them visible. - -### FEAT-3: initiative first-class entry (DONE) -- Initiative field at add-char, add-monster, edit participant. -- Inline edit wired. Tie-break = drag order. - -### UI feedback: toast + info modal (DONE) -- All 23 native alert() replaced. ToastStack (6s auto-dismiss + manual X) - for transient failures. InfoModal (persistent OK) for validations. - React context provider wraps all 3 App branches. -- Fixed: native alert vanished instantly on browser focus loss. - -### Filter dup chars from add-participant dropdown (DONE) -- Character dropdown excludes chars already in encounter. Prevents - dup-add at source. No more dup alert path needed. - -### Test timeouts (DONE) -- jest.setTimeout(10000) in setupTests.js (CRA blocks config-level timeout). - -### Warning = failure in tests (DONE) -- console.error/warn throw in test env. - -### BUG-1: addParticipant + pause/resume corrupts rotation -- RESOLVED as side effect of BUG-2 fix. - -### BUG-2: addParticipant allows duplicate id -- FIXED (addParticipant throws on dup id). - -### BUG-4: hide-player-HP breaks display view -- FIXED --- mock honors setDoc{merge}, all 5 activeDisplay sites use merge. - -### BUG-5: mid-round addParticipant/revive corrupts rotation -- FIXED --- slot-array + DRY advance core nextActiveAfter. - -### BUG-6: reorderParticipants doesn't update turnOrderIds -- FIXED structurally by 1-list model. - -### BUG-7: reorderParticipants not logged -- FIXED --- returns log:{message, undo}. Handler calls logAction. deathSave, - addParticipants, updateParticipant logging gaps also closed. - -### BUG-8: server adapter has no reconnect -- FIXED --- onclose reconnects + re-subscribes existing paths. - -### BUG-10: deact+reactivate same round double-acts participant -- FIXED --- 1-list model keeps slot position on toggle. Reactivate does not - grant second turn. Test: turn.bug10.test.js. - -### BUG-11: FE Combat.scenario test crashes -- FIXED --- moved to shared/turn.combat.test.js, pure functions, 100 rounds. - -### BUG-12: campaign selection follows activeDisplay -- FIXED. - -### BUG-13: reorderParticipants crossing current pointer = ambiguous -- FIXED --- block cross-pointer reorder during active encounter (both dirs). - Full fix needs actedThisRound tracking. Pragmatic block prevents skip/double. - Pre-combat: free reorder. Test: turn.bug13.test.js. - -### BUG-14: addParticipant init-insertion breaks after drag-reorder -- FIXED --- slotIndexForInit scans current list (post-drag aware). - -### BUG-15: DisplayView re-sorts (drag order not preserved) -- FIXED --- display renders participants[] directly. - -### BUG-16: subscribeCollection hook drops queryConstraints -- FIXED --- neutral builders, both adapters honor orderBy/limit. - -### BUG-17: dead SDK imports in App.js -- FIXED --- trimmed (auth + getFirestore + getStorage remain). - -### BUG-18: stale comments reference deleted memory adapter -- FIXED. - -### FEAT-1: Dead characters/NPCs stay in turn order -- DONE --- character/NPC death does not auto-remove. DM controls inactive/remove. -- Non-NPC monsters still become inactive automatically at death. - -### combat.scenario 100 rounds not turns -- DONE --- loops by actual round-wrap count. - -### feat: add all characters to participants list -- DONE --- addParticipants bulk add wired. diff --git a/docker/Dockerfile b/docker/Dockerfile index 2b75ad1..14d0f5b 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,4 +1,5 @@ -# docker/Dockerfile — single container: caddy (front) + node (back). +# syntax=docker/dockerfile:1 +# docker/Dockerfile --- single container: caddy (front) + node (back). # Build context = repo root. # ---- build stage: frontend + install backend deps ---- FROM node:18-alpine AS build @@ -7,25 +8,26 @@ WORKDIR /app COPY package*.json ./ COPY shared/package.json ./shared/ COPY server/package.json ./server/ -RUN npm install --include-workspace-root +RUN --mount=type=cache,target=/root/.npm \ + npm install --include-workspace-root --omit=dev --prefer-offline +# frontend code (changes often) COPY shared/ ./shared/ -COPY server/ ./server/ COPY src/ ./src/ COPY public/ ./public/ COPY tailwind.config.js postcss.config.js ./ -# better-sqlite3 native build (alpine musl) -RUN cd server && npm rebuild better-sqlite3 - # build frontend (server storage, same-origin /api + /ws via caddy) ARG REACT_APP_TRACKER_APP_ID=ttrpg-initiative-tracker-default ENV REACT_APP_STORAGE=server ENV REACT_APP_TRACKER_APP_ID=$REACT_APP_TRACKER_APP_ID RUN NODE_OPTIONS=--openssl-legacy-provider npm run build -# prune backend dev deps for runtime -RUN npm prune --omit=dev +# server after backend - it's fast. so changing server only doesnt invalidate slow frontend. +COPY server/ ./server/ +# better-sqlite3 native build (alpine musl). Cached if server unchanged. +RUN cd server && npm rebuild better-sqlite3 + # ---- runtime stage: caddy + node ---- FROM node:18-alpine @@ -38,7 +40,6 @@ COPY --from=build /app/server/node_modules ./server/node_modules COPY --from=build /app/package*.json ./ COPY --from=build /app/shared/package.json ./shared/ COPY --from=build /app/server/package.json ./server/ -COPY shared/ ./shared/ COPY server/ ./server/ # built frontend served by caddy COPY --from=build /app/build /srv diff --git a/docker/restart.sh b/docker/restart.sh new file mode 100755 index 0000000..d9d236d --- /dev/null +++ b/docker/restart.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# scripts/docker-restart.sh — rebuild + restart docker container. +# Usage: ./scripts/docker-restart.sh +set -euo pipefail +cd "$(dirname "$0")/.." + +echo "=== stopping existing container ===" +docker compose -f docker/docker-compose.yml down 2>/dev/null || true + +echo "=== rebuilding image ===" +docker compose -f docker/docker-compose.yml build + +echo "=== starting container ===" +docker compose -f docker/docker-compose.yml up -d + +echo "=== status ===" +docker compose -f docker/docker-compose.yml ps + +echo "" +echo "app: http://localhost:${PORT:-8080}" +echo "logs: docker compose -f docker/docker-compose.yml logs -f" diff --git a/server/index.js b/server/index.js index cb9c8c7..ce81cbc 100644 --- a/server/index.js +++ b/server/index.js @@ -1,6 +1,7 @@ // server/index.js — generic KV document store over HTTP + WebSocket. // firebase mirror: doc-tree model. Thin REST, path-based WS push. // Adapter (src/storage/server.js) = passthrough, no shape translation. +// TEST: cache layer rebuild check. 'use strict';