[object Object]

← back to Crazy News Channel

Fix Cody's 4 verified findings: XSS in cartoon gallery, file:// manifest fetch failure, cover-label desync, dead tab stops on inert panels

7518fc5b798d5052a9e4e97bece915b19a5dcbba · 2026-09-24 09:42:31 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqUe1Hvqsnrkxpe4R6jrk1

Files touched

Diff

commit 7518fc5b798d5052a9e4e97bece915b19a5dcbba
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 24 09:42:31 2026 -0700

    Fix Cody's 4 verified findings: XSS in cartoon gallery, file:// manifest fetch failure, cover-label desync, dead tab stops on inert panels
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01DqUe1Hvqsnrkxpe4R6jrk1
---
 cartoons/20260924-commission-turtles.html | 12 ++++++++++--
 cartoons/index.html                       | 29 ++++++++++++++++++-----------
 cartoons/manifest.js                      | 17 +++++++++++++++++
 cartoons/manifest.json                    | 13 -------------
 scripts/build-cartoon-batch.sh            |  4 ++--
 scripts/daily-cartoon-gen.mjs             | 11 +++++++++--
 6 files changed, 56 insertions(+), 30 deletions(-)

diff --git a/cartoons/20260924-commission-turtles.html b/cartoons/20260924-commission-turtles.html
index 3ede9e8..c4fa735 100644
--- a/cartoons/20260924-commission-turtles.html
+++ b/cartoons/20260924-commission-turtles.html
@@ -213,7 +213,13 @@ footer{max-width:900px;margin:20px auto 0;padding:0 20px;font-family:-apple-syst
     if (i === state.revealed) { cover.hidden = false; cover.disabled = false; cover.textContent = i === 0 ? 'Click, tap, or press N to begin' : 'Click, tap, or press N to process'; }
   }
 
-  function paintAll(){ for (let i=0;i<3;i++) renderPanel(i); syncButtons(); }
+  function paintAll(){
+    for (let i=0;i<3;i++) {
+      renderPanel(i);
+      document.getElementById('art'+i).setAttribute('tabindex', i === state.revealed ? '0' : '-1');
+    }
+    syncButtons();
+  }
 
   function syncButtons(){
     const nextBtn = document.getElementById('nextBtn');
@@ -233,8 +239,10 @@ footer{max-width:900px;margin:20px auto 0;padding:0 20px;font-family:-apple-syst
       const cover = document.getElementById('cover'+i);
       cover.hidden = i < state.revealed;
       cover.disabled = i > state.revealed;
+      document.getElementById('art'+i).setAttribute('tabindex', i === state.revealed ? '0' : '-1');
     }
-    renderPanel(state.revealed - 1);
+    renderPanel(state.revealed - 1); // the panel that just got processed
+    if (state.revealed < 3) renderPanel(state.revealed); // the newly-active cover's label
     say('ok', `Panel ${state.revealed} processed.`);
     syncButtons();
     if (state.revealed >= 3) say('ok', 'All 3 panels processed. Press Plot Twist! to nest it deeper.');
diff --git a/cartoons/index.html b/cartoons/index.html
index 0a9e68a..1028d7c 100644
--- a/cartoons/index.html
+++ b/cartoons/index.html
@@ -78,6 +78,7 @@ footer{max-width:1200px; margin:30px auto; padding:0 20px 30px; color:var(--text
   </div>
 </main>
 <footer>All cartoons are invented satire. No actual bureaucrats were inconvenienced. Part of PANDEMONIUM-24.</footer>
+<script src="manifest.js"></script>
 <script>
 (function(){
   const grid = document.getElementById('grid');
@@ -112,13 +113,21 @@ footer{max-width:1200px; margin:30px auto; padding:0 20px 30px; color:var(--text
     for (const c of sorted) {
       const a = document.createElement('a');
       a.className = 'card'; a.href = c.file; a.role = 'listitem';
-      a.innerHTML = `
-        <div class="thumb" style="${c.thumb ? `background-image:url('${c.thumb}')` : ''}" aria-hidden="true">${c.thumb ? '' : '🖋️'}</div>
-        <div class="body">
-          <div class="title">${c.title}</div>
-          <div class="meta" title="${c.created_at||''}">${c.created_at ? fmtDate(c.created_at) : ''}${c.category ? ' · ' + c.category : ''}</div>
-          <div class="blurb">${c.blurb || ''}</div>
-        </div>`;
+
+      const thumb = document.createElement('div');
+      thumb.className = 'thumb'; thumb.setAttribute('aria-hidden', 'true');
+      if (c.thumb) thumb.style.backgroundImage = `url('${c.thumb.replace(/['\\]/g, '')}')`;
+      else thumb.textContent = '\u{1F58B}\uFE0F';
+
+      const body = document.createElement('div'); body.className = 'body';
+      const title = document.createElement('div'); title.className = 'title'; title.textContent = c.title || '';
+      const meta = document.createElement('div'); meta.className = 'meta';
+      meta.title = c.created_at || '';
+      meta.textContent = (c.created_at ? fmtDate(c.created_at) : '') + (c.category ? ' \u00b7 ' + c.category : '');
+      const blurb = document.createElement('div'); blurb.className = 'blurb'; blurb.textContent = c.blurb || '';
+
+      body.append(title, meta, blurb);
+      a.append(thumb, body);
       grid.appendChild(a);
     }
   }
@@ -129,10 +138,8 @@ footer{max-width:1200px; margin:30px auto; padding:0 20px 30px; color:var(--text
     safeSet(LS_DENSITY, density.value);
   });
 
-  fetch('manifest.json').then(r => r.json()).then(data => {
-    cartoons = data.cartoons || [];
-    render();
-  }).catch(() => { empty.hidden = false; });
+  cartoons = window.P24_CARTOONS || [];
+  render();
 })();
 </script>
 </body>
diff --git a/cartoons/manifest.js b/cartoons/manifest.js
new file mode 100644
index 0000000..1cb78d6
--- /dev/null
+++ b/cartoons/manifest.js
@@ -0,0 +1,17 @@
+// cartoons/manifest.js — the P24 Political Cartoon Desk archive data.
+// Loaded as a plain <script src> (matching the stories-data.js pattern used by the
+// main P24 page) rather than fetched as JSON, because this project's whole design
+// contract is offline-capable file:// use with no server (see ../README.md) — a
+// fetch() of a sibling JSON file is blocked under file:// in Chrome/Safari and
+// silently mis-renders the empty state instead of erroring (TK-12133 finding #2).
+window.P24_CARTOONS = [
+  {
+    "id": "commission-turtles",
+    "title": "Commissions All the Way Down",
+    "file": "20260924-commission-turtles.html",
+    "created_at": "2026-09-24T17:05:00Z",
+    "category": "Political Cartoon",
+    "blurb": "A blue-ribbon commission recommends forming a commission to study its own recommendation. It keeps going.",
+    "thumb": null
+  }
+];
diff --git a/cartoons/manifest.json b/cartoons/manifest.json
deleted file mode 100644
index bb0624f..0000000
--- a/cartoons/manifest.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "cartoons": [
-    {
-      "id": "commission-turtles",
-      "title": "Commissions All the Way Down",
-      "file": "20260924-commission-turtles.html",
-      "created_at": "2026-09-24T17:05:00Z",
-      "category": "Political Cartoon",
-      "blurb": "A blue-ribbon commission recommends forming a commission to study its own recommendation. It keeps going.",
-      "thumb": null
-    }
-  ]
-}
\ No newline at end of file
diff --git a/scripts/build-cartoon-batch.sh b/scripts/build-cartoon-batch.sh
index 0915324..a9cb825 100644
--- a/scripts/build-cartoon-batch.sh
+++ b/scripts/build-cartoon-batch.sh
@@ -15,7 +15,7 @@
 #     (3 panels, click-to-advance, Plot Twist button, non-partisan invented
 #     satire, 390-1440px responsive, keyboard accessible, reduced-motion safe,
 #     zero console errors — same acceptance bar as 20260924-commission-turtles.html),
-#     e2e-test it with Playwright, add it to cartons/manifest.json, and commit.
+#     e2e-test it with Playwright, add it to cartoons/manifest.js, and commit.
 #  3. Logs the whole run to yolo/cartoon-batch-log.jsonl.
 #
 # COST: each `claude -p` build is a real paid Anthropic API session. Today's single
@@ -69,7 +69,7 @@ for b in q['briefs']:
         f"~/Projects/crazy-news-channel/cartoons/{b['slug']}.html. Playwright-test at 1440/1024/390 "
         f"+ reduced-motion + dark (require playwright via ~/Projects/animals/node_modules/playwright, "
         f"launch chromium with channel:'chrome'). Fix any real defects found, then add an entry to "
-        f"~/Projects/crazy-news-channel/cartoons/manifest.json (id, title, file, created_at ISO now, "
+        f"~/Projects/crazy-news-channel/cartoons/manifest.js (as a new object appended to the window.P24_CARTOONS array: id, title, file, created_at ISO now, "
         f"category 'Political Cartoon', blurb, thumb:null) and commit with author "
         f"steve@designerwallcoverings.com. Do not deploy, push, or touch anything outside this repo."
     )
diff --git a/scripts/daily-cartoon-gen.mjs b/scripts/daily-cartoon-gen.mjs
index caa3e03..c58fe3b 100644
--- a/scripts/daily-cartoon-gen.mjs
+++ b/scripts/daily-cartoon-gen.mjs
@@ -16,7 +16,7 @@ import { CARTOON_BANK } from './cartoon-prompt-bank.mjs';
 
 const __dirname = path.dirname(fileURLToPath(import.meta.url));
 const ROOT = path.join(__dirname, '..');
-const MANIFEST = path.join(ROOT, 'cartoons', 'manifest.json');
+const MANIFEST = path.join(ROOT, 'cartoons', 'manifest.js');
 const QUEUE_DIR = path.join(ROOT, 'cartoons', 'queue');
 
 const args = Object.fromEntries(process.argv.slice(2).map(a => {
@@ -27,7 +27,14 @@ const COUNT = parseInt(args.count || '6', 10);
 const DATE = args.date || new Date().toISOString().slice(0, 10);
 
 function loadManifest() {
-  try { return JSON.parse(fs.readFileSync(MANIFEST, 'utf8')); } catch { return { cartoons: [] }; }
+  // manifest.js is `window.P24_CARTOONS = [ ...json... ];` — extract just the array
+  // literal rather than fetch()/require()-ing it (this file has no DOM `window`, and
+  // the array itself is plain JSON so a regex+JSON.parse is safe and dependency-free).
+  try {
+    const src = fs.readFileSync(MANIFEST, 'utf8');
+    const m = src.match(/window\.P24_CARTOONS\s*=\s*(\[[\s\S]*\])\s*;\s*$/);
+    return { cartoons: m ? JSON.parse(m[1]) : [] };
+  } catch { return { cartoons: [] }; }
 }
 
 function seededShuffle(arr, seed) {

← 5cd57ad P24 Political Cartoon Desk: gallery archive, daily prompt ge  ·  back to Crazy News Channel  ·  Cartoon batch: set PATH for launchd so node/claude resolve; 30215fc →