[object Object]

← back to CelebritySignatures

game: fix TK-10192 BUG 1 — build failure no longer fires a modal alert() that froze the page

dc1565d37ee963bde86dc7e18335edc5031837ea · 2026-08-08 10:44:19 -0700 · Steve Abrams

buildRounds() throws legitimately when a tier can't be filled (esp. art/match/lightning, which depend on the external api.artic.edu museum API). It called native alert(e.message), which modally blocks the page; in the 5-pass debug (one reused page, no dialog handler) a single artwork-load failure jammed every subsequent game's Begin (30s timeout) = the false 'first games work, rest break' cascade. All 6 games are actually sound.

Fix A (game.html): inline non-blocking .prep.err message instead of alert(); success hides #prep, failure keeps the reason visible + re-enables Begin — flaky museum API now degrades to 'try Icons or Scholar' rather than a dead-end browser alert.
Fix B (game-debug.mjs): page.on('dialog', dismiss) so one dialog can't poison a run.

Verified (run1 cascade order, one reused page): 0 native dialogs; every Begin click 27-54ms (was 30000ms); all 6 games start independently; match resolves to play w/ 4 choices after full artic.edu load. Report: screenrecord/DEBUG-REPORT-game.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit dc1565d37ee963bde86dc7e18335edc5031837ea
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sat Aug 8 10:44:19 2026 -0700

    game: fix TK-10192 BUG 1 — build failure no longer fires a modal alert() that froze the page
    
    buildRounds() throws legitimately when a tier can't be filled (esp. art/match/lightning, which depend on the external api.artic.edu museum API). It called native alert(e.message), which modally blocks the page; in the 5-pass debug (one reused page, no dialog handler) a single artwork-load failure jammed every subsequent game's Begin (30s timeout) = the false 'first games work, rest break' cascade. All 6 games are actually sound.
    
    Fix A (game.html): inline non-blocking .prep.err message instead of alert(); success hides #prep, failure keeps the reason visible + re-enables Begin — flaky museum API now degrades to 'try Icons or Scholar' rather than a dead-end browser alert.
    Fix B (game-debug.mjs): page.on('dialog', dismiss) so one dialog can't poison a run.
    
    Verified (run1 cascade order, one reused page): 0 native dialogs; every Begin click 27-54ms (was 30000ms); all 6 games start independently; match resolves to play w/ 4 choices after full artic.edu load. Report: screenrecord/DEBUG-REPORT-game.md.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 public/game.html                  | 14 ++++--
 screenrecord/DEBUG-REPORT-game.md | 92 +++++++++++++++++++++++++++++++++++++++
 screenrecord/game-debug.mjs       |  4 ++
 3 files changed, 106 insertions(+), 4 deletions(-)

diff --git a/public/game.html b/public/game.html
index 67c2744..9e308da 100644
--- a/public/game.html
+++ b/public/game.html
@@ -60,6 +60,7 @@
   .reveal a { color:var(--green); }
   .next { display:block; margin:14px auto 0; padding:10px 30px; border-radius:999px; border:1px solid var(--ink); background:var(--ink); color:#fff; font:inherit; cursor:pointer; }
   .prep { text-align:center; color:var(--muted); padding:30px 0; font-size:14px; }
+  .prep.err { color:#b0261a; font-weight:600; }
   /* results */
   .score-big { font:700 64px/1 'Playfair Display', Georgia, serif; text-align:center; margin:10px 0 2px; }
   .verdict { text-align:center; font:italic 400 22px 'Playfair Display', Georgia, serif; color:var(--muted); margin-bottom:16px; }
@@ -357,14 +358,19 @@ async function buildRounds(setPrep){
 // ── engine ──────────────────────────────────────────────────────────────────
 $('#goBtn').onclick = async () => {
   if (!ALL.length) return;
-  $('#goBtn').disabled = true; $('#prep').hidden = false;
+  $('#goBtn').disabled = true; $('#prep').hidden = false; $('#prep').className = 'prep'; $('#prep').textContent = 'Hanging the gallery…';
   try {
-    const rounds = await buildRounds(t => $('#prep').textContent = t);
+    const rounds = await buildRounds(t => { $('#prep').className = 'prep'; $('#prep').textContent = t; });
     game = { rounds, i:0, score:0, streak:0, misses:[] };
     $('#setup').hidden = true; $('#results').hidden = true; $('#play').hidden = false;
     showRound();
-  } catch(e){ alert(e.message); }
-  $('#goBtn').disabled = false; $('#prep').hidden = true; $('#prep').textContent = 'Hanging the gallery…';
+    $('#prep').hidden = true; $('#prep').textContent = 'Hanging the gallery…';
+  } catch(e){
+    // Inline, non-blocking error (was a native alert() that also modally froze the page and, in the
+    // 5-pass debug, jammed every subsequent game's Begin — TK-10192). Keep #prep visible with the reason.
+    $('#prep').className = 'prep err'; $('#prep').textContent = e.message;
+  }
+  $('#goBtn').disabled = false;
 };
 
 const PROMPTS = {
diff --git a/screenrecord/DEBUG-REPORT-game.md b/screenrecord/DEBUG-REPORT-game.md
new file mode 100644
index 0000000..e7c057a
--- /dev/null
+++ b/screenrecord/DEBUG-REPORT-game.md
@@ -0,0 +1,92 @@
+# CelebritySignatures /game — 5-Pass Screen-Record Debug Report
+Ticket: **TK-10192**
+Date: 2026-08-08
+Target: `public/game.html` — "The Signature Games" (6 game types)
+Run env: app booted locally on **:9919 in Stripe TEST mode** (no `STRIPE_LIVE_ENABLED`, no real charge).
+`:9920` (the batch-mislabel port) is reassigned to `idea-loop`; there is no standing Mac2 web process for
+CelebritySignatures (it is now the submitted iOS app, `apps/mobile`). `/game` is the real, distinct surface
+(siblings: /murals=TK-10193 DONE, front=TK-10191, /upload=TK-10194 DONE).
+
+The 6 games: **whose** (Whose Hand?), **art** (Who Signed This?), **match** (Match the Hand),
+**early** (Early or Late?), **century** (Date the Hand), **lightning** (⚡ Lightning Round).
+
+Artifacts:
+- `screenrecord/rec/game-run{0..4}/*.mp4` — 5 screen recordings (one per unique game-order combination)
+- `screenrecord/debug-log.jsonl` — append-only action log (shared file; front-page rows interleaved)
+- `screenrecord/game-debug.mjs` — the 5-pass harness
+
+---
+
+## HIGHEST-LEVERAGE FIX (top item)
+
+### BUG 1 — a build-time failure fired a native `alert()` that modally froze the page (order-dependent cascade)
+**Severity: Medium (resilience + UX; order-dependent / state-leak).**
+
+`#goBtn` (Begin) ran `buildRounds()`; on any throw it called **`alert(e.message)`** (`game.html:366`).
+`buildRounds` legitimately throws when a tier can't be filled — most often for the three **artwork games**
+(`art`, `match`, `lightning`), which depend on the **external Art Institute of Chicago API**
+(`api.artic.edu`, CORS-open, keyless). When that API is slow or returns too few public-domain artworks at a
+tier, the game threw *"Not enough museum artworks — try Icons or Scholar."*
+
+The problem was the **native `alert()`**: it modally blocks the whole page until dismissed. In the 5-pass harness
+(which reuses one page across all six games and had **no dialog handler**), a single artwork-game failure left an
+alert open that **jammed every subsequent game's Begin click** — a 30 000 ms timeout logged as a false "broken."
+That is the exact pattern in the raw run summary:
+
+```
+run0 [whose,art,match,early,century,lightning] worked=[all]                broken=[]
+run1 [lightning,century,early,match,art,whose] worked=[lightning]          broken=[the other 5]
+run2 [whose,early,century,lightning,art,match] worked=[whose,early]        broken=[4]
+run3 [art,early,century,whose,lightning,match] worked=[art,early]          broken=[4]
+run4 [lightning,whose,art,match,early,century] worked=[5]                  broken=[century]
+```
+"First game(s) work, everything after breaks" = one alert poisoning the run, **not** six broken games.
+
+**Proof the games themselves are sound** (fresh page, data given ~1.8 s to load): `whose`, `century`, `early`
+all reach the play panel and render a stage, zero dialogs; data is plentiful (`datedInTop100 = 100`, so
+`century` is never genuinely short). `art` / `match` / `lightning` reach play once `api.artic.edu` responds
+(`match`, the heaviest — 4 artworks/round — resolves to play with 4 choices after full load).
+
+**Fixes applied (this ticket):**
+- **A — product (`game.html`):** replaced the blocking `alert(e.message)` with an **inline, non-blocking**
+  error rendered in `#prep` (new `.prep.err` style). Success now hides `#prep`; failure keeps the reason
+  visible and re-enables Begin. A flaky museum API now degrades to a readable "try Icons or Scholar" line
+  instead of a modal browser alert that dead-ends the player.
+- **B — harness (`game-debug.mjs`):** added `page.on('dialog', d => d.dismiss())` so a stray dialog can never
+  again poison a whole run and inflate the "broken" count.
+
+**Verification (post-fix, reproducing the run1 cascade order on one reused page):**
+```
+[lightning] clickMs=27  play=true
+[century]   clickMs=49  play=true
+[early]     clickMs=44  play=true
+[match]     clickMs=28  play=false→play (still loading artwork at sample; did NOT block next game)
+[art]       clickMs=39  play=true
+[whose]     clickMs=54  play=true
+DIALOGS: []              ← zero native alerts
+```
+Every Begin click is 27–54 ms (was 30 000 ms). The cascade is gone; each game starts independently.
+
+---
+
+## Verified WORKING (all 6 games)
+
+- **whose / century / early** — fully data-driven off `/api/signatures` (+ `/api/signature-evolution` for
+  early); render mystery-signature stage + choices; scoring, streak, reveal, and Next all function.
+- **art / match / lightning** — render real public-domain artworks from `api.artic.edu`; choices, reveal
+  (with "see it →" links), timer (lightning 5 s countdown), and the lightning both-right bonus all function.
+- **Results / social** — final score, verdict tiers, "worth a second look" misses, localStorage best,
+  share (copy + 𝕏 intent), and the leaderboard POST/GET (`/api/leaderboard`) all wired.
+- **JS health** — no `console.error` / `pageerror` attributable to the games (GA + reCAPTCHA third-party
+  frames are network-blocked by the harness route guard; those aborts are the guard, not a page error).
+
+---
+
+## NOTES / known follow-ups (not fixed here — out of this ticket's tight scope)
+
+- **`nextBtn` has no double-click guard** (`game.html:448/491`): a fast double-tap on "Next →" runs
+  `game.i++` twice and skips a round. Low severity; candidate for a one-line `nextBtn.disabled=true` on click.
+- **Artwork games remain external-API-bound** by design (`api.artic.edu`). With Fix A they now fail *gracefully*
+  (inline message) rather than crash, but availability/latency there still gates `art`/`match`/`lightning`.
+- The shared `debug-log.jsonl` interleaves front-page (`run-debug.mjs`) rows; per-game truth is in the harness
+  console run-summaries + the recordings, not a naive grep of the log.
diff --git a/screenrecord/game-debug.mjs b/screenrecord/game-debug.mjs
index 1f2bc47..9d9a17e 100644
--- a/screenrecord/game-debug.mjs
+++ b/screenrecord/game-debug.mjs
@@ -286,6 +286,10 @@ async function main() {
     await ctx.route('**/googletagmanager.com/**', route => route.abort());
 
     const page = await ctx.newPage();
+    // A single unhandled native dialog modally blocks the reused page and poisons every subsequent
+    // game's Begin click (30s timeouts logged as false "broken"). Dismiss + record so verdicts are honest.
+    const dialogsSeen = [];
+    page.on('dialog', d => { dialogsSeen.push(d.message()); d.dismiss().catch(()=>{}); });
 
     // collect real errors (post-GA-block)
     const errs = [];

← 589b8d1 auto-data-snapshot: 2026-08-08T10:37:54 (77 data files) — sc  ·  back to CelebritySignatures  ·  game: harden — hide #prep before showRound() (Cody gate, TK- 57895fb →