[object Object]

← back to Crazy News Channel

snapshot before shadowman-articles merge: Steve's review-queue approval of senator-s-shadow-war (18:19Z, review-actions.jsonl)

b098bf8271d42fd2c66ec8e091d6346ad730d7eb · 2026-09-25 12:03:14 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0174h5jsd9QqR6SbjXgvooua

Files touched

Diff

commit b098bf8271d42fd2c66ec8e091d6346ad730d7eb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 25 12:03:14 2026 -0700

    snapshot before shadowman-articles merge: Steve's review-queue approval of senator-s-shadow-war (18:19Z, review-actions.jsonl)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_0174h5jsd9QqR6SbjXgvooua
---
 cartoons/20260925-senator-s-shadow-war.html      |   5 +
 cartoons/manifest.js                             |  30 ++++--
 cartoons/media/20260925-senator-s-shadow-war.jpg | Bin 0 -> 284892 bytes
 daily-cartoons/review.html                       | 121 +++++++++++++++++++----
 4 files changed, 133 insertions(+), 23 deletions(-)

diff --git a/cartoons/20260925-senator-s-shadow-war.html b/cartoons/20260925-senator-s-shadow-war.html
new file mode 100644
index 0000000..e93e5f2
--- /dev/null
+++ b/cartoons/20260925-senator-s-shadow-war.html
@@ -0,0 +1,5 @@
+<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
+<meta name="viewport" content="width=device-width,initial-scale=1"><title>Senator&#x27;s Shadow War</title>
+<link rel="stylesheet" href="../assets/style.css"></head>
+<body class="cartoon-page-simple"><p><a href="index.html">← Cartoon Desk</a></p><video controls playsinline poster="media/20260925-senator-s-shadow-war.jpg" src="media/20260925-senator-s-shadow-war.mp4"></video><h1>Senator&#x27;s Shadow War</h1>
+<p class="cap">&ldquo;In shadowy negotiations, power is real.&rdquo;</p><p class="src"><a href="https://news.google.com/rss/articles/CBMi3wFBVV95cUxPaTVXUElrb1BVMzFkbjF4ajlaZVZISy1rZ1VQSEx3UnU4dUM5RkNZOUc3V2hsb25RQ25GVGtnVjNZMEpiWmZydmlLYmo4cDNMbk5nYXRDN0RBRDI3LTdxS19xbUlpOHBWajg2dDJNb1NhZHVVVGlsX2VaN3poUUc5ZkRyOGxtQVVhSTNxclpENlVPdWJOX09zOGcwR01oY3c3M2ZXWW1icVN5VTJuUlAxVFJVcVF0SzZTZHItZWUxM0xHcWdRQzVvTU91OXdjalRrOWRVWWNPc05YRE9zSmo0?oc=5" target="_blank" rel="noopener noreferrer">Source: &#x27;Survivor 51&#x27; cast: Meet the 20 castaways battling to win after last night&#x27;s elimination</a></p><p><small>AI-generated editorial cartoon in classic editorial-ink style. Invented figures only.</small></p></body></html>
\ No newline at end of file
diff --git a/cartoons/manifest.js b/cartoons/manifest.js
index 28d4003..75c49da 100644
--- a/cartoons/manifest.js
+++ b/cartoons/manifest.js
@@ -54,7 +54,7 @@ window.P24_CARTOONS = [
     "created_at": "2026-09-24T17:10:26Z",
     "category": "Political Cartoon",
     "section": "politics",
-    "blurb": "Erie's most reliable parking meter is honored, seated on the council, re-elected, and sworn in as one-day mayor — and now won't leave the office.",
+    "blurb": "Erie's most reliable parking meter is honored, seated on the council, re-elected, and sworn in as one-day mayor \u2014 and now won't leave the office.",
     "thumb": null,
     "story_id": "pw-meter-street",
     "tags": [
@@ -176,7 +176,7 @@ window.P24_CARTOONS = [
     "created_at": "2026-09-24T22:15:00Z",
     "category": "Political Cartoon",
     "section": "politics",
-    "blurb": "An invented infrastructure bill leaves committee as a single train car and picks up wildly unrelated riders — a desert lighthouse, a ferris wheel, a spoon museum — at every stop; click a car for its invented cost.",
+    "blurb": "An invented infrastructure bill leaves committee as a single train car and picks up wildly unrelated riders \u2014 a desert lighthouse, a ferris wheel, a spoon museum \u2014 at every stop; click a car for its invented cost.",
     "thumb": null,
     "story_id": "pw-pork-barrel-express",
     "tags": [
@@ -326,7 +326,7 @@ window.P24_CARTOONS = [
   },
   {
     "id": "filibuster-ink",
-    "title": "The Filibuster Marathon — Ink Edition",
+    "title": "The Filibuster Marathon \u2014 Ink Edition",
     "file": "20260924-filibuster-ink.html",
     "created_at": "2026-09-24T19:45:00Z",
     "category": "Political Cartoon",
@@ -346,7 +346,7 @@ window.P24_CARTOONS = [
   },
   {
     "id": "filibuster-qwen25",
-    "title": "The Filibuster Marathon — Editorial Ink Style (AI Model Contest)",
+    "title": "The Filibuster Marathon \u2014 Editorial Ink Style (AI Model Contest)",
     "file": "20260924-filibuster-qwen25-7b.html",
     "created_at": "2026-09-24T18:58:05Z",
     "category": "Political Cartoon",
@@ -372,7 +372,7 @@ window.P24_CARTOONS = [
     "created_at": "2026-09-24T19:45:00Z",
     "category": "Political Cartoon",
     "section": "politics",
-    "blurb": "Erie's one-day honorary mayor — a parking meter, sash on, expired flags for eyebrows — barricades itself behind an overturned desk and dares officials to take the office back. Drawn in classic editorial-ink style (the barricade device).",
+    "blurb": "Erie's one-day honorary mayor \u2014 a parking meter, sash on, expired flags for eyebrows \u2014 barricades itself behind an overturned desk and dares officials to take the office back. Drawn in classic editorial-ink style (the barricade device).",
     "thumb": null,
     "story_id": "pw-meter-street",
     "ai_generated": false,
@@ -414,7 +414,7 @@ window.P24_CARTOONS = [
     "created_at": "2026-09-24T19:49:00Z",
     "category": "Political Cartoon",
     "section": "politics",
-    "blurb": "Motorists trudge in an endless nighttime loop around a toll-booth hub — a hamster wheel of asphalt — chasing a dangling gold 'GOLD TIER' carrot as the crossing tally reads infinity. Drawn in classic editorial-ink style (the doomed-loop device).",
+    "blurb": "Motorists trudge in an endless nighttime loop around a toll-booth hub \u2014 a hamster wheel of asphalt \u2014 chasing a dangling gold 'GOLD TIER' carrot as the crossing tally reads infinity. Drawn in classic editorial-ink style (the doomed-loop device).",
     "thumb": null,
     "story_id": "bs-toll-loyalty",
     "ai_generated": false,
@@ -426,5 +426,23 @@ window.P24_CARTOONS = [
       "editorial cartoon",
       "classic editorial ink"
     ]
+  },
+  {
+    "id": "senator-s-shadow-war",
+    "title": "Senator's Shadow War",
+    "file": "20260925-senator-s-shadow-war.html",
+    "created_at": "2026-09-25T14:25:59+00:00",
+    "category": "Political Cartoon",
+    "section": "politics",
+    "blurb": "In shadowy negotiations, power is real.",
+    "thumb": "media/20260925-senator-s-shadow-war.jpg",
+    "story_id": "real-entertainment-3",
+    "ai_generated": true,
+    "style_reference": "editorial-ink",
+    "tags": [
+      "editorial cartoon",
+      "classic editorial ink",
+      "daily cartoon"
+    ]
   }
 ];
diff --git a/cartoons/media/20260925-senator-s-shadow-war.jpg b/cartoons/media/20260925-senator-s-shadow-war.jpg
new file mode 100644
index 0000000..2cd9609
Binary files /dev/null and b/cartoons/media/20260925-senator-s-shadow-war.jpg differ
diff --git a/daily-cartoons/review.html b/daily-cartoons/review.html
index 42ceeab..1b453c9 100644
--- a/daily-cartoons/review.html
+++ b/daily-cartoons/review.html
@@ -8,7 +8,7 @@
 <label>Size <input id="dens" type="range" min="220" max="640" step="20" value="340"></label></div></header>
 <main id="grid"><article class="card rq-card" data-date="2026-09-25" data-slug="infinite-scroll" data-status="queued" data-created="2026-09-25T14:29:23+00:00" data-title="Infinite Scroll" data-cost="0.103">
   <label class="rq-select"><input type="checkbox" class="rq-check" disabled aria-label="Select &ldquo;Infinite Scroll&rdquo;"> <span>Select</span></label>
-  <img src="queue/2026-09-25/infinite-scroll/poster.jpg" alt="">
+  <video controls preload="none" poster="queue/2026-09-25/infinite-scroll/poster.jpg" src="queue/2026-09-25/infinite-scroll/clip.mp4"></video>
   <div class="body">
     <h2>Infinite Scroll</h2>
     <p class="cap">&ldquo;Scrolling: where time goes to die.&rdquo;</p>
@@ -25,7 +25,7 @@
   </div>
 </article><article class="card rq-card" data-date="2026-09-25" data-slug="tech-tyrant" data-status="queued" data-created="2026-09-25T14:27:36+00:00" data-title="Tech Tyrant" data-cost="0.103">
   <label class="rq-select"><input type="checkbox" class="rq-check" disabled aria-label="Select &ldquo;Tech Tyrant&rdquo;"> <span>Select</span></label>
-  <img src="queue/2026-09-25/tech-tyrant/poster.jpg" alt="">
+  <video controls preload="none" poster="queue/2026-09-25/tech-tyrant/poster.jpg" src="queue/2026-09-25/tech-tyrant/clip.mp4"></video>
   <div class="body">
     <h2>Tech Tyrant</h2>
     <p class="cap">&ldquo;The algorithm always knows best.&rdquo;</p>
@@ -40,26 +40,26 @@
     <button type="button" class="rq-btn rq-approve" data-act="approve" disabled aria-label="Approve &ldquo;Tech Tyrant&rdquo;">Approve</button>
     <button type="button" class="rq-btn rq-delete" data-act="delete" disabled aria-label="Delete &ldquo;Tech Tyrant&rdquo;">Delete</button>
   </div>
-</article><article class="card rq-card" data-date="2026-09-25" data-slug="senator-s-shadow-war" data-status="queued" data-created="2026-09-25T14:25:59+00:00" data-title="Senator&#x27;s Shadow War" data-cost="0.103">
+</article><article class="card rq-card rq-approved" data-date="2026-09-25" data-slug="senator-s-shadow-war" data-status="approved" data-created="2026-09-25T14:25:59+00:00" data-title="Senator&#x27;s Shadow War" data-cost="0.103">
   <label class="rq-select"><input type="checkbox" class="rq-check" disabled aria-label="Select &ldquo;Senator&#x27;s Shadow War&rdquo;"> <span>Select</span></label>
-  <img src="queue/2026-09-25/senator-s-shadow-war/poster.jpg" alt="">
+  <video controls preload="none" poster="queue/2026-09-25/senator-s-shadow-war/poster.jpg" src="queue/2026-09-25/senator-s-shadow-war/clip.mp4"></video>
   <div class="body">
     <h2>Senator&#x27;s Shadow War</h2>
     <p class="cap">&ldquo;In shadowy negotiations, power is real.&rdquo;</p>
-    <p class="src no-story">no linked article — pre-TK-12237 item, pass --story-id to approve</p>
+    <p class="src"><a href="https://news.google.com/rss/articles/CBMi3wFBVV95cUxPaTVXUElrb1BVMzFkbjF4ajlaZVZISy1rZ1VQSEx3UnU4dUM5RkNZOUc3V2hsb25RQ25GVGtnVjNZMEpiWmZydmlLYmo4cDNMbk5nYXRDN0RBRDI3LTdxS19xbUlpOHBWajg2dDJNb1NhZHVVVGlsX2VaN3poUUc5ZkRyOGxtQVVhSTNxclpENlVPdWJOX09zOGcwR01oY3c3M2ZXWW1icVN5VTJuUlAxVFJVcVF0SzZTZHItZWUxM0xHcWdRQzVvTU91OXdjalRrOWRVWWNPc05YRE9zSmo0?oc=5" target="_blank" rel="noopener noreferrer">📰 &#x27;Survivor 51&#x27; cast: Meet the 20 castaways battling to win after last night&#x27;s elimination</a> <span class="chip">real-news</span></p>
     <div class="chips"><span class="chip when" title="2026-09-25T14:25:59+00:00" data-iso="2026-09-25T14:25:59+00:00">🕓 2026-09-25T14:25:59+00:00</span>
-      <span class="chip">$0.103</span><span class="chip rq-status st-queued">queued</span></div>
+      <span class="chip">$0.103</span><span class="chip rq-status st-approved">approved</span></div>
     <details><summary>prompts</summary><p><b>image:</b> Black and white single-panel editorial cartoon, classic 1970s newspaper op-ed page style. Pen-and-ink with loose, confident, slightly jagged brush contour lines and dense directional crosshatching, scratchboard-like white highlights cut out of heavy solid black. A jowly senator stands on a podium casting an enormous shadow across a dimly lit room. His shadowy figure morphs into menacing figures and abstract shapes, like swords or masks, battling each other in the background. Figures are invented anonymous caricatures, not any real person. High contrast, stark, absurd, ink-heavy, hand-drawn newspaper cartoon. No color, no gradients, no 3D render, no photorealism, no vector clip art, no text, no letters, no caption, no speech bubbles, no watermark, no signature.</p><p><b>motion:</b> Animate this black-and-white pen-and-ink editorial cartoon, keep the hand-drawn ink style throughout, static camera. The senator&#x27;s silhouette winks and gestures, causing his shadow to morph into different forms that clash violently. Slow, dry, deadpan comedy timing.</p></details>
-    <code class="cmd" title="click to copy">python3 daily-cartoons/approve.py senator-s-shadow-war --date 2026-09-25 --story-id &lt;story-id&gt;</code>
+    <code class="cmd" title="click to copy">python3 daily-cartoons/approve.py senator-s-shadow-war --date 2026-09-25 --story-id real-entertainment-3</code>
     <p class="rq-err" role="alert" hidden></p>
   </div>
   <div class="rq-actions">
-    <button type="button" class="rq-btn rq-approve" data-act="approve" disabled aria-label="Approve &ldquo;Senator&#x27;s Shadow War&rdquo;">Approve</button>
+    <button type="button" class="rq-btn rq-approve" data-act="approve" disabled aria-disabled="true">Approved ✓</button>
     <button type="button" class="rq-btn rq-delete" data-act="delete" disabled aria-label="Delete &ldquo;Senator&#x27;s Shadow War&rdquo;">Delete</button>
   </div>
 </article><article class="card rq-card" data-date="2026-09-25" data-slug="bureaucrats-and-bubbles" data-status="queued" data-created="2026-09-25T14:23:48+00:00" data-title="Bureaucrats and Bubbles" data-cost="0.103">
   <label class="rq-select"><input type="checkbox" class="rq-check" disabled aria-label="Select &ldquo;Bureaucrats and Bubbles&rdquo;"> <span>Select</span></label>
-  <img src="queue/2026-09-25/bureaucrats-and-bubbles/poster.jpg" alt="">
+  <video controls preload="none" poster="queue/2026-09-25/bureaucrats-and-bubbles/poster.jpg" src="queue/2026-09-25/bureaucrats-and-bubbles/clip.mp4"></video>
   <div class="body">
     <h2>Bureaucrats and Bubbles</h2>
     <p class="cap">&ldquo;When committees debate, they just inflate ideas.&rdquo;</p>
@@ -76,7 +76,7 @@
   </div>
 </article><article class="card rq-card" data-date="2026-09-24" data-slug="committee-of-shadows" data-status="queued" data-created="2026-09-24T22:12:18+00:00" data-title="Committee of Shadows" data-cost="0.103">
   <label class="rq-select"><input type="checkbox" class="rq-check" disabled aria-label="Select &ldquo;Committee of Shadows&rdquo;"> <span>Select</span></label>
-  <img src="queue/2026-09-24/committee-of-shadows/poster.jpg" alt="">
+  <video controls preload="none" poster="queue/2026-09-24/committee-of-shadows/poster.jpg" src="queue/2026-09-24/committee-of-shadows/clip.mp4"></video>
   <div class="body">
     <h2>Committee of Shadows</h2>
     <p class="cap">&ldquo;In darkness, they find clarity.&rdquo;</p>
@@ -110,7 +110,10 @@ if(LIVE){
   const toast=document.getElementById('rq-toast'),tText=document.getElementById('rq-toast-text'),tUndo=document.getElementById('rq-toast-undo');
   const cards=()=>[...g.querySelectorAll('.rq-card')];
   const selected=()=>cards().filter(c=>c.querySelector('.rq-check').checked);
-  const key=c=>({date:c.dataset.date,slug:c.dataset.slug});
+  const bErr=document.getElementById('rq-bulk-err');
+  const key=c=>c.dataset.storyId?{date:c.dataset.date,slug:c.dataset.slug,story_id:c.dataset.storyId}:{date:c.dataset.date,slug:c.dataset.slug};
+  const needsStory=c=>c.classList.contains('rq-needs-story')&&!c.dataset.storyId&&c.dataset.status!=='approved';
+  function bulkErr(msg){bErr.textContent=msg||'';bErr.hidden=!msg}
   const byKey=it=>g.querySelector(`.rq-card[data-date="${it.date}"][data-slug="${it.slug}"]`);
   let busy=false,toastTimer=null,lastRemoved=[];
   function refresh(){
@@ -133,18 +136,36 @@ if(LIVE){
     return j.results||[];
   }
   function markErr(c,msg){const p=c.querySelector('.rq-err');p.textContent=msg;p.hidden=!msg}
-  function markApproved(c){
+  function showSource(c,r){
+    if(!r||!r.story_url)return;
+    const p=document.createElement('p');p.className='src';
+    const a=document.createElement('a');a.href=r.story_url;a.target='_blank';a.rel='noopener noreferrer';a.textContent='📰 '+(r.story_title||'linked article');
+    const s=document.createElement('span');s.className='chip';s.textContent=r.story_source||'';
+    p.append(a,' ',s);
+    const old=c.querySelector('.body .src');if(old)old.replaceWith(p);else c.querySelector('.body .cap').after(p);
+  }
+  function markApproved(c,r){
+    const pk=c.querySelector('.rq-picker');if(pk)pk.remove();
+    c.classList.remove('rq-needs-story');showSource(c,r);
     c.dataset.status='approved';c.classList.add('rq-approved');
     const st=c.querySelector('.rq-status');st.textContent='approved';st.className='chip rq-status st-approved';
     const b=c.querySelector('.rq-approve');b.textContent='Approved ✓';b.disabled=true;b.setAttribute('aria-disabled','true');b.removeAttribute('aria-label');
     c.querySelector('.rq-check').checked=false;markErr(c,'');
   }
-  async function approve(list){
+  async function approve(list,bulk){
     list=list.filter(c=>c.dataset.status!=='approved');if(!list.length||busy)return;
-    setBusy(true);
+    // Every cartoon must link a real article (TK-12158): refuse up front, name the cards, send nothing.
+    const missing=list.filter(needsStory);
+    if(missing.length){
+      missing.forEach(c=>markErr(c,'Choose an article above before approving.'));
+      if(bulk)bulkErr(`Not approved — ${missing.length} selected card${missing.length>1?'s have':' has'} no article chosen: `+missing.map(c=>'“'+c.dataset.title+'”').join(', ')+'. Pick an article on each (or unselect it), then Approve again.');
+      else{const q=missing[0].querySelector('.rq-pk-q');if(q)q.focus()}
+      return;
+    }
+    bulkErr('');setBusy(true);
     try{
       const res=await post('/api/approve',list.map(key));let ok=0,bad=0;
-      res.forEach(r=>{const c=byKey(r);if(!c)return;if(r.ok){markApproved(c);ok++}else{markErr(c,'Approve failed: '+(r.error||'unknown error'));bad++}});
+      res.forEach(r=>{const c=byKey(r);if(!c)return;if(r.ok){markApproved(c,r);ok++}else{markErr(c,'Approve failed: '+(r.error||'unknown error'));bad++}});
       showToast(`Approved ${ok}`+(bad?` · ${bad} failed (see card)`:''));
     }catch(e){showToast('Approve failed: '+e.message)}
     finally{setBusy(false)}
@@ -182,18 +203,84 @@ if(LIVE){
     if(ev.key==='Tab'){const f=[mNo,mOk];const i=f.indexOf(document.activeElement);ev.preventDefault();f[(i+(ev.shiftKey?f.length-1:1))%f.length].focus()}
   });
   modal.addEventListener('click',ev=>{if(ev.target===modal)closeModal()});
-  g.addEventListener('change',ev=>{if(ev.target.classList.contains('rq-check'))refresh()});
+  g.addEventListener('change',ev=>{if(ev.target.classList.contains('rq-check')){bulkErr('');refresh()}});
   g.addEventListener('click',ev=>{
     const b=ev.target.closest('.rq-actions .rq-btn');if(!b||b.disabled)return;const c=b.closest('.rq-card');
     if(b.dataset.act==='approve')approve([c]);else confirmDelete([c]);
   });
-  bA.onclick=()=>approve(selected());
+  bA.onclick=()=>approve(selected(),true);
   bD.onclick=()=>confirmDelete(selected());
   const selAll=()=>{cards().forEach(c=>c.querySelector('.rq-check').checked=true);refresh()};
   document.getElementById('rq-bulk-all').onclick=selAll;
   document.getElementById('rq-select-all').onclick=selAll;
   document.getElementById('rq-bulk-clear').onclick=()=>{cards().forEach(c=>c.querySelector('.rq-check').checked=false);refresh()};
   tUndo.onclick=undo;
+  // ---- Article picker for cards with no story_id. Suggestions = plain keyword overlap between the
+  // cartoon (title+caption weighted 3, image prompt 1) and each story's headline+tags. $0, no LLM.
+  const STOP=new Set('the and for with from that this into over under after about are was were has have had its his her their they them than then what when where which who why how not but you your our out all any one two new more most very just also will can may off onto upon while says said via amid near per'.split(' '));
+  const STYLE=new Set('black white single panel editorial cartoon classic 1970s newspaper page style pen ink loose confident slightly jagged brush contour line lines dense directional crosshatching scratchboard like highlight highlights cut heavy solid figure figures invented anonymous caricature caricatures real person high contrast stark absurd drawn color colour gradient gradients render photorealism vector clip art text letter letters caption speech bubble bubbles watermark signature hand made'.split(' '));
+  const stem=w=>w.length>4&&w.endsWith('s')&&!w.endsWith('ss')?w.slice(0,-1):w;
+  const toks=(s,extra)=>new Set((String(s||'').toLowerCase().match(/[a-z0-9]+/g)||[]).filter(w=>w.length>=3&&!STOP.has(w)&&!(extra&&extra.has(w))).map(stem).filter(w=>!(extra&&extra.has(w))));
+  const srcLabel=s=>s.source==='real-news'?(s.sourceName||'Real news'):'P24';
+  let STORIES=[],SBYID=new Map();
+  function score(pk,s){
+    const A=pk._a||(pk._a=toks(pk.dataset.kwA)),B=pk._b||(pk._b=toks(pk.dataset.kwB,STYLE));
+    // Outlet names ride along in real-news tags ("the new york times" would match a caption's
+    // "time"), so drop source-name tokens; a headline hit counts double a tag-only hit.
+    const src=toks(s.sourceName),H=toks(s.headline,src),T=toks((s.tags||[]).join(' '),src);
+    let n=0;new Set([...H,...T]).forEach(t=>{const w=(A.has(t)?3:B.has(t)?1:0)*(H.has(t)?2:1);n+=w});return n;
+  }
+  function choose(c,id){
+    const s=SBYID.get(id);if(!s)return;const pk=c.querySelector('.rq-picker');if(!pk)return;
+    c.dataset.storyId=id;
+    const sel=pk.querySelector('.rq-pk-s');if([...sel.options].some(o=>o.value===id))sel.value=id;
+    pk.querySelectorAll('.rq-sugg-btn').forEach(b=>b.setAttribute('aria-pressed',String(b.dataset.id===id)));
+    const ch=pk.querySelector('.rq-pk-chosen');ch.textContent='Linked: '+s.headline+' · '+srcLabel(s);ch.classList.add('rq-pk-ok');
+    pk.classList.add('rq-picked');markErr(c,'');
+    const cmd=c.querySelector('.cmd');if(cmd)cmd.textContent=`python3 daily-cartoons/approve.py ${c.dataset.slug} --date ${c.dataset.date} --story-id ${id}`;
+    if(!bErr.hidden&&!selected().some(needsStory))bulkErr('');
+  }
+  function fill(pk){
+    const c=pk.closest('.rq-card'),sel=pk.querySelector('.rq-pk-s');
+    const terms=pk.querySelector('.rq-pk-q').value.toLowerCase().split(/\s+/).filter(Boolean);
+    const hit=s=>{const h=(s.headline+' '+(s.tags||[]).join(' ')+' '+(s.sourceName||'')+' '+s.id).toLowerCase();return terms.every(t=>h.includes(t))};
+    sel.textContent='';let n=0;
+    [['real-news','Real news'],['p24','P24 stories']].forEach(([src,label])=>{
+      const list=STORIES.filter(s=>s.source===src&&hit(s));if(!list.length)return;
+      const og=document.createElement('optgroup');og.label=`${label} (${list.length})`;
+      list.forEach(s=>{const o=document.createElement('option');o.value=s.id;o.textContent=s.headline+' — '+srcLabel(s);o.title=s.headline+' — '+srcLabel(s)+' ['+s.id+']';og.appendChild(o);n++});
+      sel.appendChild(og);
+    });
+    if(!n){const o=document.createElement('option');o.disabled=true;o.textContent='No articles match “'+terms.join(' ')+'”';sel.appendChild(o)}
+    if(c.dataset.storyId&&[...sel.options].some(o=>o.value===c.dataset.storyId))sel.value=c.dataset.storyId;
+    return n;
+  }
+  function initPicker(pk){
+    const c=pk.closest('.rq-card'),q=pk.querySelector('.rq-pk-q'),sel=pk.querySelector('.rq-pk-s'),box=pk.querySelector('.rq-sugg');
+    const top=STORIES.map(s=>({s,n:score(pk,s)})).filter(x=>x.n>0).sort((a,b)=>b.n-a.n||(b.s.publishedAt||'').localeCompare(a.s.publishedAt||'')).slice(0,3);
+    box.textContent='';
+    if(!top.length){const p=document.createElement('p');p.className='rq-sugg-none';p.textContent='No keyword matches — search below.';box.appendChild(p)}
+    top.forEach(({s})=>{
+      const b=document.createElement('button');b.type='button';b.className='rq-sugg-btn';b.dataset.id=s.id;b.setAttribute('aria-pressed','false');
+      const h=document.createElement('span');h.className='rq-sugg-t';h.textContent=s.headline;
+      const src=document.createElement('span');src.className='chip rq-sugg-src';src.textContent=srcLabel(s);
+      b.append(h,src);b.onclick=()=>choose(c,s.id);box.appendChild(b);
+    });
+    fill(pk);
+    q.addEventListener('input',()=>fill(pk));
+    q.addEventListener('keydown',ev=>{
+      if(ev.key==='ArrowDown'){ev.preventDefault();const o=[...sel.options].find(x=>!x.disabled);if(o&&!sel.value)sel.value=o.value;sel.focus()}
+      if(ev.key==='Enter'){ev.preventDefault();const o=[...sel.options].find(x=>!x.disabled);if(o)choose(c,o.value)}
+    });
+    sel.addEventListener('change',()=>{if(sel.value)choose(c,sel.value)});
+    sel.addEventListener('keydown',ev=>{if(ev.key==='Enter'&&sel.value){ev.preventDefault();choose(c,sel.value)}});
+  }
+  const pickers=[...g.querySelectorAll('.rq-picker')];
+  if(pickers.length){
+    fetch('/api/stories').then(r=>r.ok?r.json():Promise.reject(new Error('HTTP '+r.status))).then(j=>{
+      STORIES=j.stories||[];SBYID=new Map(STORIES.map(s=>[s.id,s]));pickers.forEach(initPicker);
+    }).catch(e=>pickers.forEach(pk=>{const p=pk.querySelector('.rq-sugg-none');if(p){p.textContent='Could not load articles: '+e.message;p.classList.add('rq-pk-bad')}}));
+  }
   refresh();
 }
 </script></body></html>
\ No newline at end of file

← b0563b7 p24: attach 2 Shadow Man cartoons to their articles — Money  ·  back to Crazy News Channel  ·  index.html: correct stale comment — real-news stories can no 506aa9e →