[object Object]

← back to Norma Sdcc Pitch

fix: live demo mode — canned realistic mock responses for /api/run-example when Norma backend unreachable; UI shows ◆ DEMO MODE vs ◆ LIVE tags + explanatory note

c03fe2c706f819d9968b7d4380ec86175dd8bfdb · 2026-05-20 09:13:41 -0700 · SteveStudio2

Files touched

Diff

commit c03fe2c706f819d9968b7d4380ec86175dd8bfdb
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 20 09:13:41 2026 -0700

    fix: live demo mode — canned realistic mock responses for /api/run-example when Norma backend unreachable; UI shows ◆ DEMO MODE vs ◆ LIVE tags + explanatory note
---
 public/app.js                   |   8 ++-
 scripts/make-explainer-video.js |  26 +++++++--
 server.js                       | 118 ++++++++++++++++++++++++++++++++++++++--
 3 files changed, 140 insertions(+), 12 deletions(-)

diff --git a/public/app.js b/public/app.js
index 6c71aba..6f8ee21 100644
--- a/public/app.js
+++ b/public/app.js
@@ -202,7 +202,9 @@
         body: JSON.stringify({ route: fullRoute, method, payload: agent.demo_payload }),
       });
       const data = await r.json();
-      outEl.textContent = `→ HTTP ${data.status} in ${data.ms}ms\n\n` + JSON.stringify(data.body, null, 2);
+      const tag = data.source === 'demo-mock' ? '◆ DEMO MODE' : data.source === 'live' ? '◆ LIVE' : '◆ ERROR';
+      const note = data._note ? `\n${data._note}\n` : '';
+      outEl.textContent = `${tag}  →  HTTP ${data.status || '?'} in ${data.ms || 0}ms${note}\n` + JSON.stringify(data.body, null, 2);
       if (!state.badges.has('ran_example')) {
         state.badges.add('ran_example');
         toast('badge', '⚡ Badge unlocked — Ran a Real Example');
@@ -430,7 +432,9 @@ ${items.map((a) => `
         body: JSON.stringify({ route: s.action, method: s.verb || 'GET', payload: s.body || null }),
       });
       const data = await r.json();
-      out.textContent = `→ HTTP ${data.status} in ${data.ms}ms\n\n` + JSON.stringify(data.body, null, 2);
+      const tag = data.source === 'demo-mock' ? '◆ DEMO MODE' : data.source === 'live' ? '◆ LIVE' : '◆ ERROR';
+      const note = data._note ? `\n${data._note}\n` : '';
+      out.textContent = `${tag}  →  HTTP ${data.status || '?'} in ${data.ms || 0}ms${note}\n` + JSON.stringify(data.body, null, 2);
       addXp(5, 'Walkthrough step');
     } catch (err) {
       out.textContent = '✗ Error: ' + err.message;
diff --git a/scripts/make-explainer-video.js b/scripts/make-explainer-video.js
index 5e62599..6d7be59 100755
--- a/scripts/make-explainer-video.js
+++ b/scripts/make-explainer-video.js
@@ -43,7 +43,9 @@ const OUT_DIR = process.env.OUT_DIR || path.join(os.homedir(), 'Videos', 'norma-
 fs.mkdirSync(OUT_DIR, { recursive: true });
 
 const stamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
-const tmpDir = path.join(OUT_DIR, '_tmp-' + stamp);
+// Reuse cached narration if present (saves ElevenLabs $$)
+const REUSE_TMP = process.env.REUSE_TMP || null;
+const tmpDir = REUSE_TMP || path.join(OUT_DIR, '_tmp-' + stamp);
 fs.mkdirSync(tmpDir, { recursive: true });
 
 // ── narration script — section, scroll target, narration text, pause-after ms
@@ -177,10 +179,17 @@ async function tts(text, outFile) {
   const audioFiles = [];
   for (let i = 0; i < SEGMENTS.length; i++) {
     const wav = path.join(tmpDir, `narr-${String(i).padStart(2, '0')}.wav`);
-    const d = await tts(SEGMENTS[i].say, wav);
+    let d;
+    if (fs.existsSync(wav) && fs.statSync(wav).size > 1000) {
+      const probe = spawnSync('ffprobe', ['-i', wav, '-show_entries', 'format=duration', '-v', 'quiet', '-of', 'csv=p=0']);
+      d = parseFloat(probe.stdout.toString().trim()) || 3;
+      console.log(`  [${i}] ${d.toFixed(1)}s (CACHED)`);
+    } else {
+      d = await tts(SEGMENTS[i].say, wav);
+      console.log(`  [${i}] ${d.toFixed(1)}s — "${SEGMENTS[i].say.slice(0, 70)}…"`);
+    }
     durations.push(d + (SEGMENTS[i].pause || 1500) / 1000);
     audioFiles.push(wav);
-    console.log(`  [${i}] ${d.toFixed(1)}s — "${SEGMENTS[i].say.slice(0, 70)}…"`);
   }
 
   // 2. concat narration into one wav
@@ -194,19 +203,24 @@ async function tts(text, outFile) {
 
   // 3. record screen with Playwright at the right pace
   console.log('[explainer] launching browser + recording…');
-  const browser = await chromium.launch({ headless: false });
+  const browser = await chromium.launch({
+    headless: true,
+    args: ['--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu'],
+  });
   const ctx = await browser.newContext({
     viewport: { width: 1440, height: 900 },
     recordVideo: { dir: tmpDir, size: { width: 1440, height: 900 } },
   });
   const page = await ctx.newPage();
 
-  // login
+  // login — submit via JS to avoid viewport-position issues
   await page.goto(TARGET + '/login');
+  await page.waitForSelector('input[name="username"]');
   await page.fill('input[name="username"]', USER);
   await page.fill('input[name="password"]', PASS);
-  await page.click('button[type="submit"]');
+  await page.evaluate(() => document.querySelector('form').submit());
   await page.waitForLoadState('networkidle');
+  await page.waitForTimeout(1500);
 
   // walk segments
   for (let i = 0; i < SEGMENTS.length; i++) {
diff --git a/server.js b/server.js
index 1b55b3b..2023d42 100644
--- a/server.js
+++ b/server.js
@@ -1,4 +1,15 @@
 #!/usr/bin/env node
+// Inline .env loader (no dotenv dependency)
+(() => {
+  try {
+    const f = require('fs').readFileSync(require('path').join(__dirname, '.env'), 'utf-8');
+    f.split(/\r?\n/).forEach((line) => {
+      const m = line.match(/^([A-Z0-9_]+)=(.*)$/);
+      if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2].replace(/^['"]|['"]$/g, '');
+    });
+  } catch { /* .env missing — that's fine in dev */ }
+})();
+
 /**
  * norma-sdcc-pitch — lecture-style viewer pitching SDCC on adopting Norma.
  *
@@ -171,7 +182,99 @@ app.post('/api/pitch/:role', (req, res) => {
   res.json({ saved: true, state });
 });
 
-// ── proxy: run an agent example against the live Norma at :7400 ────────────
+// ── canned mock responses for when Norma isn't reachable (pitch demo mode) ──
+const MOCK_RESPONSES = {
+  '/api/petitions/topics': () => ({
+    topics: [
+      { id: '3d49b69c-cd54-4085-8ee4-4f17faba9608', title: 'Federal Loan Forgiveness', subtitle: 'critical priority', category: 'policy', urgency: 'critical', source: 'headline', description: 'Updates on federal student loan forgiveness programs', date: new Date().toISOString() },
+      { id: '619b8f6c-92da-4f1c-b26f-8e6b7e0bd522', title: 'Racial Debt Disparities', subtitle: 'high priority', category: 'equity', urgency: 'high', source: 'headline', description: 'Black borrowers carry 30% more debt 12 years after graduation.', date: new Date().toISOString() },
+      { id: 'a7e1c3f0-3220-49a3-9b6e-92c8d6e5a0e1', title: 'SAVE Plan Litigation Update', subtitle: 'breaking', category: 'policy', urgency: 'critical', source: 'headline', description: '8th Circuit ruling impacts 8M borrowers in SAVE plan.', date: new Date().toISOString() },
+      { id: 'b4d3a1c8-1f9e-4b22-9a05-7e8d6f9c3a21', title: 'PSLF Processing Backlog', subtitle: 'urgent', category: 'operations', urgency: 'high', source: 'headline', description: '47,000 PSLF applications stuck in MOHELA queue.', date: new Date().toISOString() },
+      { id: 'c8f2e7b9-2a3d-4c5f-8d6e-1b9a8c7d6e5f', title: 'Parent PLUS Reform Bill', subtitle: 'medium priority', category: 'legislation', urgency: 'medium', source: 'headline', description: 'New bipartisan bill caps Parent PLUS interest at 5%.', date: new Date().toISOString() },
+    ],
+  }),
+  '/api/agents/advocacy/generate': () => ({
+    statement: "The Department of Education's announcement today that 1 million borrowers have been approved for Public Service Loan Forgiveness is a significant step forward — and a long-overdue one. Public servants — teachers, nurses, social workers, firefighters — have spent a decade waiting for the relief they were promised. We applaud this milestone, AND we will not stop pressing until every eligible borrower receives their forgiveness without delay. The processing backlog must be cleared. The communication from MOHELA must improve. And Congress must finally make permanent the reforms that allowed this progress. — Natalia Abrams, Executive Director, Student Debt Crisis Center",
+    citations: [
+      { source: 'ED press release', url: 'https://ed.gov/press-releases/2026-05-20-pslf-discharge', date: '2026-05-20' },
+      { source: 'SDCC 2024 PSLF position', url: 'https://studentdebtcrisis.org/positions/pslf', date: '2024-08-15' },
+    ],
+    confidence: 0.87,
+    generated_at: new Date().toISOString(),
+    review_required: true,
+    voice_match_score: 0.92,
+  }),
+  '/api/leads/discover': () => ({
+    stories: [
+      { id: 'bs-2026-1834', borrower_name_anonymized: 'Sarah K.', state: 'Texas', debt_type: 'federal', program: 'pslf', urgency: 'medium', press_ready: true, story_summary: 'Public school teacher 11 years, paid every month, denied PSLF 3x for paperwork errors. Now approved as of May 20.' },
+      { id: 'bs-2026-1801', borrower_name_anonymized: 'James M.', state: 'Texas', debt_type: 'federal', program: 'pslf', urgency: 'high', press_ready: true, story_summary: 'Nonprofit director, recent PSLF approval after 12 years.' },
+      { id: 'bs-2026-1745', borrower_name_anonymized: 'Maria T.', state: 'Texas', debt_type: 'federal', program: 'pslf', urgency: 'medium', press_ready: false, story_summary: 'Public health nurse — consented for press but anonymously only.' },
+    ],
+    total_matching: 3,
+    query_time_ms: 47,
+  }),
+  '/api/ai-chat': () => ({
+    response: '"This milestone is welcome but overdue — public servants should never have to wait 12 years for the forgiveness they were promised. SDCC will keep pushing until every eligible borrower gets relief without the bureaucratic gauntlet."',
+    cited_sources: [
+      { type: 'prior_statement', date: '2024-11-12', excerpt: '"public servants should never have to wait for the relief they were promised"' },
+      { type: 'prior_statement', date: '2025-03-08', excerpt: '"the bureaucratic gauntlet that turns 10-year promises into 12-year waits"' },
+    ],
+    confidence: 0.89,
+  }),
+  '/api/contacts': () => ({
+    contacts: [
+      { id: 'c-1', name: 'Danielle Douglas-Gabriel', org: 'Washington Post', beat: 'higher education', last_contact: '2026-04-12', notes: 'Wrote 3 stories citing SDCC in 2025.' },
+      { id: 'c-2', name: 'Michael Stratford', org: 'POLITICO', beat: 'education policy', last_contact: '2026-05-14', notes: 'Currently working on a PSLF processing piece.' },
+      { id: 'c-3', name: 'Cory Turner', org: 'NPR', beat: 'education', last_contact: '2026-03-22', notes: 'Used 2 SDCC borrower stories in March segment.' },
+      { id: 'c-4', name: 'Anya Kamenetz', org: 'freelance', beat: 'education', last_contact: '2026-02-18', notes: 'Substack newsletter on student debt.' },
+      { id: 'c-5', name: 'Adam Harris', org: 'The Atlantic', beat: 'higher education', last_contact: '2026-01-30', notes: 'Long-form, slow turnaround.' },
+      { id: 'c-6', name: 'Stacy Cowley', org: 'NY Times', beat: 'consumer finance / debt', last_contact: '2026-04-02', notes: '' },
+      { id: 'c-7', name: 'Emily Wilkins', org: 'CNBC', beat: 'Washington/policy', last_contact: '2026-05-05', notes: '' },
+      { id: 'c-8', name: 'Kate Magill', org: 'Higher Ed Dive', beat: 'higher education', last_contact: '2026-03-18', notes: '' },
+      { id: 'c-9', name: 'Andrew Kreighbaum', org: 'Bloomberg', beat: 'higher ed / policy', last_contact: '2026-02-22', notes: '' },
+      { id: 'c-10', name: 'Hannah Knowles', org: 'Washington Post', beat: 'policy', last_contact: '2026-04-29', notes: '' },
+    ],
+    total: 10,
+  }),
+  '/api/agents/petition/generate': () => ({
+    title: 'Block the Rollback of SAVE Plan Protections',
+    body: "Millions of student-loan borrowers in the SAVE plan are facing an unprecedented threat. The Administration's proposal to roll back income-driven protections would push 8 million borrowers into payments they cannot afford — overnight.\n\nWe've been here before. We won SAVE because borrowers, advocates, and policymakers built coalition pressure that the previous administration couldn't ignore. We will do it again.\n\nThis petition demands three things: (1) keep the SAVE plan's $0 monthly payment for borrowers earning under $32,800; (2) prevent retroactive interest accrual for the millions in administrative forbearance; (3) hold servicers accountable for accurate payment counts.\n\nSign now. Share widely. Call your senators. The deadline for public comment is in 30 days.",
+    share_text: { x: 'The Admin wants to roll back SAVE plan protections — pushing 8M borrowers into payments they can\'t afford. We won SAVE once. We can do it again. Sign now → [link]', bluesky: 'Block the SAVE rollback. 8M borrowers depend on it. Sign + share.', threads: 'SAVE plan rollback = 8M borrowers in crisis. Sign the petition. We won this fight once. Time to win it again.' },
+    follow_up_sequence: ['Day 1 — thank you + share ask', 'Day 3 — story from a SAVE borrower', 'Day 7 — call-your-senator scripts', 'Day 14 — coalition partners + escalation'],
+    hero_image_prompt: 'Photorealistic borrower mid-30s at kitchen table, paperwork spread out, soft natural light, looking determined toward camera. SDCC brand royal blue accent.',
+    confidence: 0.91,
+  }),
+  '/api/agents/grants/match': () => ({
+    matches: [
+      { funder: 'Lumina Foundation', program: 'Equitable Pathways Initiative', deadline: '2026-07-15', range: '$100,000-$250,000', fit_score: 0.93, why: 'SDCC mission aligns with Lumina\'s equitable-completion focus + prior funding to similar advocacy orgs.' },
+      { funder: 'Open Society Foundations', program: 'Economic Justice', deadline: '2026-09-30', range: '$50,000-$500,000', fit_score: 0.88, why: 'OSF funded SDCC in 2022; renewal cycle approaches.' },
+      { funder: 'JEHT Foundation Successor (Funders Collaborative)', program: 'Borrower Defense Advocacy', deadline: '2026-06-30', range: '$75,000-$150,000', fit_score: 0.84, why: 'Direct match — borrower defense is SDCC\'s wheelhouse.' },
+      { funder: 'Sloan Foundation', program: 'Higher Ed Public Policy', deadline: '2026-08-12', range: '$50,000-$200,000', fit_score: 0.71, why: 'Lighter fit — Sloan prefers data/research-heavy proposals; SDCC could partner with TICAS.' },
+    ],
+    total_scanned: 12847,
+    matching_count: 4,
+    last_updated: new Date().toISOString(),
+  }),
+  '/api/agents/congress-by-zip': () => ({
+    zip: '90404',
+    state: 'CA',
+    district: 'CA-36',
+    senators: [
+      { name: 'Alex Padilla', party: 'D', phone: '202-224-3553', email: 'senator@padilla.senate.gov', committees: ['Judiciary', 'HSGAC'], recent_debt_votes: { 'SAVE-protections': 'support', 'PSLF-reform': 'support' } },
+      { name: 'Adam Schiff', party: 'D', phone: '202-224-3841', email: 'senator@schiff.senate.gov', committees: ['Judiciary', 'Intelligence'], recent_debt_votes: { 'SAVE-protections': 'support', 'PSLF-reform': 'support' } },
+    ],
+    house: { name: 'Ted Lieu', party: 'D', district: 'CA-36', phone: '202-225-3976', email: 'rep@lieu.house.gov', committees: ['Judiciary', 'Foreign Affairs'], recent_debt_votes: { 'SAVE-protections': 'support', 'PSLF-reform': 'support' } },
+    state_legislature: { state_senator: 'Ben Allen (D-SD-24)', state_assembly: 'Rick Chavez Zbur (D-AD-51)' },
+  }),
+};
+
+function maybeMockResponse(route, method) {
+  const path = route.split('?')[0];
+  if (MOCK_RESPONSES[path]) return MOCK_RESPONSES[path]();
+  return null;
+}
+
+// ── proxy: run an agent example against the live Norma at :7400, or canned mock if Norma unreachable ──
 app.post('/api/run-example', async (req, res) => {
   const { route, payload, method } = req.body || {};
   if (!route || typeof route !== 'string' || !route.startsWith('/api/')) {
@@ -211,7 +314,7 @@ app.post('/api/run-example', async (req, res) => {
     const url = NORMA_BASE + route;
     const headers = { 'Content-Type': 'application/json' };
     if (cookie) headers.Cookie = cookie;
-    const init = { method: m, headers };
+    const init = { method: m, headers, signal: AbortSignal.timeout(3000) };
     if (m !== 'GET' && payload) init.body = JSON.stringify(payload);
     const t0 = Date.now();
     const r = await fetch(url, init);
@@ -219,9 +322,16 @@ app.post('/api/run-example', async (req, res) => {
     const text = await r.text();
     let body;
     try { body = JSON.parse(text); } catch { body = { _raw: text.slice(0, 4000) }; }
-    res.json({ ok: r.ok, status: r.status, ms, body });
+    res.json({ ok: r.ok, status: r.status, ms, body, source: 'live' });
   } catch (err) {
-    res.status(502).json({ error: 'proxy error', detail: err.message });
+    // Norma not reachable — serve canned mock if available so the demo still works
+    const mock = maybeMockResponse(route, m);
+    if (mock) {
+      // Add small artificial delay to feel realistic
+      await new Promise((r) => setTimeout(r, 250 + Math.random() * 400));
+      return res.json({ ok: true, status: 200, ms: 320 + Math.floor(Math.random() * 200), body: mock, source: 'demo-mock', _note: 'Live Norma backend not deployed on this host — showing realistic demo data from the schema. Identical to production response shape.' });
+    }
+    res.status(502).json({ ok: false, status: 502, ms: 0, body: { error: 'Norma backend unreachable + no mock for ' + route, detail: err.message }, source: 'error' });
   }
 });
 

← 9383603 add: sdcc + steve login gate, ElevenLabs voice cloning scrip  ·  back to Norma Sdcc Pitch  ·  fix: demo mode short-circuit — when NORMA_BASE=demo, /api/ru 76506bb →