diff --git a/docs/superpowers/evidence/kcd2-dice-solver-deploy-responsive-io-2026-07-26.json b/docs/superpowers/evidence/kcd2-dice-solver-deploy-responsive-io-2026-07-26.json index a2866139..9d9b9eb6 100644 --- a/docs/superpowers/evidence/kcd2-dice-solver-deploy-responsive-io-2026-07-26.json +++ b/docs/superpowers/evidence/kcd2-dice-solver-deploy-responsive-io-2026-07-26.json @@ -67,6 +67,11 @@ "command": "Headless Chrome: measure 8 viewports incl. 2560x1296 and 2560x1440, on dev then the live site", "result": "pass", "evidence": "All 8 fit with no inner scroll and zero clipped names. 2560x1296: font 20px, row 52px, buttons 44px, list 1194px, 61px slack. 2560x1440: row 56px, list 1274px. 1920x1080: font 17px, row 43px. Phone and stacked layouts unchanged. Live site re-verified: 43 rows, solver EV 2,675, console clean. 283 tests, 100% coverage, lint clean." + }, + { + "command": "Concurrency and mount-guard probes on deploy_build.sh", + "result": "pass", + "evidence": "Race reproduced pre-fix (rsync rc=24, live left with index.html + 2 of 6 chunks, stale asset already deleted). Post-fix two concurrent deploy_build.sh runs both published a complete 4-file bundle at rc=0. Lock proven non-vacuous: a contender blocks until the holder releases, and times out under -w. rsync --delete-after --delay-updates confirmed to write before deleting and to leave no .~tmp~ behind on a clean run." } ], "risks": [ @@ -75,7 +80,8 @@ ".git/hooks/post-commit is not version-controlled and is lost on a fresh clone. 'setup_kcd2_dice_solver.sh status' reports it and re-running setup restores it.", "Sub-44px tap targets on phone (16px stepper buttons) are a deliberate, user-accepted trade for fitting 43 dice. Mitigated: the whole cell is an add target and '+' is hidden on narrow layouts because the cell supersedes it.", "Three safe-design-rules are knowingly broken inside .die-list only: body text below 16px (11-13px names), square icon buttons rather than 2:1 padding, and sub-8px intra-cell gaps. 43 items with a name, a chart and a counter in one viewport is arithmetically incompatible with 16px.", - "The height scaling is scoped to @media (min-width: 1001px): applying it to the stacked layout overflowed the fold by ~100px at 768x1024 and 1000x800, because there the title and search box sit above the grid and the viewport height is not the grid's to claim. Caught by measurement, not by inspection." + "The height scaling is scoped to @media (min-width: 1001px): applying it to the stacked layout overflowed the fold by ~100px at 768x1024 and 1000x800, because there the title and search box sit above the grid and the viewport height is not the grid's to claim. Caught by measurement, not by inspection.", + "deploy_build.sh had no lock, and it has two entry points (setup_kcd2_dice_solver.sh calls it directly; the post-commit hook starts a unit that runs it). A second run's `vite --emptyOutDir` truncating dist.staging while the first run's rsync read it made rsync exit 24, which `set -e` turned into an abort BEFORE the mount check, leaving dist/ with a fresh index.html referencing assets that never arrived. Invisible downstream: `try_files` serves index.html for the missing chunks (verified live: /assets/does-not-exist.js -> 200 text/html, not 404), and the hook's `|| true` swallows the unit failure. Reproduced with a throttled rsync, then fixed with flock around main." ], "rollback": [ "App: git revert the commit; run.sh and the dev server are untouched, and the old localStorage key/shape is unchanged so saved inventories survive a revert.", diff --git a/kcd2_dice_solver/.gitignore b/kcd2_dice_solver/.gitignore index 8106c647..4e8cce15 100644 --- a/kcd2_dice_solver/.gitignore +++ b/kcd2_dice_solver/.gitignore @@ -1,5 +1,6 @@ node_modules dist dist.staging +.deploy.lock coverage *.tsbuildinfo diff --git a/kcd2_dice_solver/deploy_build.sh b/kcd2_dice_solver/deploy_build.sh index 34dc05fb..a2db61cd 100755 --- a/kcd2_dice_solver/deploy_build.sh +++ b/kcd2_dice_solver/deploy_build.sh @@ -62,14 +62,23 @@ ensure_toolchain() { # answers the question is what the CONTAINER sees. assert_mount_live() { local running host_inode container_inode - command -v docker >/dev/null 2>&1 || return 0 + if ! command -v docker >/dev/null 2>&1; then + echo "deploy_build: docker not found — mount NOT verified." >&2 + return 0 + fi running="$(docker ps --format '{{.Names}}' 2>/dev/null || true)" - grep -qx "$CONTAINER" <<<"$running" || return 0 + if ! grep -qx "$CONTAINER" <<<"$running"; then + echo "deploy_build: ${CONTAINER} not running — mount NOT verified." >&2 + return 0 + fi host_inode="$(stat -c %i "$LIVE")" + # `docker exec` reports OCI runtime errors on STDOUT, so 2>/dev/null does not + # suppress them and a failure arrives as a long string rather than as empty + # output. Require an actual inode number instead of merely non-empty. container_inode="$(docker exec "$CONTAINER" stat -c %i /srv 2>/dev/null || true)" - if [[ -z $container_inode ]]; then - echo "deploy_build: could not read /srv inode from ${CONTAINER}; skipping mount check." >&2 + if [[ ! $container_inode =~ ^[0-9]+$ ]]; then + echo "deploy_build: could not read /srv inode from ${CONTAINER} — mount NOT verified." >&2 return 0 fi if [[ $host_inode != "$container_inode" ]]; then @@ -84,6 +93,21 @@ assert_mount_live() { } main() { + # Both entry points run this same script — setup_kcd2_dice_solver.sh calls it + # directly, and the post-commit hook starts a unit that runs it — so one lock + # closes the race between them. Without it, a second run's `vite --emptyOutDir` + # truncates dist.staging while the first run's rsync is still reading it: + # rsync exits 24, `set -e` stops the script before assert_mount_live, and dist/ + # is left holding a fresh index.html that references assets which never + # arrived. Nothing downstream notices — `try_files` serves index.html for the + # missing chunks, so the browser is handed HTML where it expects JavaScript and + # no 404 reaches any log. + # + # Wait rather than fail: a rebuild that arrives during a setup run still needs + # to happen, and the unit has no start timeout. + exec 9>"${SCRIPT_DIR}/.deploy.lock" + flock -w 600 9 || die "another deploy_build is still running after 10 minutes." + ensure_toolchain cd "$SCRIPT_DIR"