← back to Crazy News Channel Shadowman
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
M cartoons/20260924-commission-turtles.htmlM cartoons/index.htmlA cartoons/manifest.jsD cartoons/manifest.jsonM scripts/build-cartoon-batch.shM scripts/daily-cartoon-gen.mjs
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 Shadowman
·
Cartoon batch: set PATH for launchd so node/claude resolve; 30215fc →