[object Object]

← back to Cncp Failures Mockups

live-data toggle: /data proxies real CNCP failures + hermes status by default; ?seed=1 forces design-stable seed; meta badge + LIVE/SEED switcher on index + auto-refresh 30s

5d00c005598b9220a73ee92d04dd5008c4b8c7c0 · 2026-05-11 13:47:45 -0700 · SteveStudio2

Files touched

Diff

commit 5d00c005598b9220a73ee92d04dd5008c4b8c7c0
Author: SteveStudio2 <steve@designerwallcoverings.com>
Date:   Mon May 11 13:47:45 2026 -0700

    live-data toggle: /data proxies real CNCP failures + hermes status by default; ?seed=1 forces design-stable seed; meta badge + LIVE/SEED switcher on index + auto-refresh 30s
---
 public/index.html        | 67 ++++++++++++++++++++++++++++++++++++++----------
 public/v1-terminal.html  |  2 +-
 public/v2-newspaper.html |  2 +-
 public/v3-heatmap.html   |  2 +-
 public/v4-kanban.html    |  2 +-
 public/v5-timeline.html  |  2 +-
 public/v6-brutalist.html |  2 +-
 server.js                | 49 ++++++++++++++++++++++++++++++-----
 8 files changed, 102 insertions(+), 26 deletions(-)

diff --git a/public/index.html b/public/index.html
index 1b72b2e..cfa07a3 100644
--- a/public/index.html
+++ b/public/index.html
@@ -33,9 +33,11 @@
   <p class="sub">Each tile is a self-contained reimagining of the Failures + Hermes panel. Same data; six different display directions. Click <b>Open</b> for full-screen, then use <kbd>←</kbd>/<kbd>→</kbd> to flip between variants.</p>
 </div>
 <div class="controls">
-  <span style="color:var(--muted); font-size:12px;">All show real CNCP-style data:</span>
-  <span style="font-size:11px; background:#1f2535; padding:3px 8px; border-radius:4px;">5 open + 1 resolved</span>
-  <span style="font-size:11px; background:#1f2535; padding:3px 8px; border-radius:4px;">1 high · 3 medium · 5 low (last 24h Hermes)</span>
+  <span style="color:var(--muted); font-size:12px;">Data source:</span>
+  <button class="ctrl-btn active" id="mode-live" onclick="setMode('live')">LIVE (CNCP)</button>
+  <button class="ctrl-btn" id="mode-seed" onclick="setMode('seed')">SEED (design-stable)</button>
+  <span id="meta" style="color:var(--muted); font-size:12px; margin-left:auto;"></span>
+  <button class="ctrl-btn" onclick="refreshAll()">↻ Refresh</button>
 </div>
 <div class="grid" id="grid"></div>
 
@@ -59,22 +61,59 @@ const variants = [
   { id:'v6-brutalist',  title:'6. Brutalist · Data-Dense',  desc:'Hard borders, full table, every field visible. No chrome — just the facts.' }
 ];
 
-document.getElementById('grid').innerHTML = variants.map((v, i) => `
-  <div class="card">
-    <div class="preview"><iframe src="${v.id}.html" loading="lazy"></iframe></div>
-    <div class="info">
-      <h3>${v.title}</h3>
-      <div class="desc">${v.desc}</div>
-      <a href="${v.id}.html" class="open" onclick="event.preventDefault(); openFull(${i})">Open full-screen →</a>
-    </div>
-  </div>`).join('');
+function renderGrid(mode) {
+  const q = mode === 'seed' ? '?seed=1' : '';
+  document.getElementById('grid').innerHTML = variants.map((v, i) => `
+    <div class="card">
+      <div class="preview"><iframe src="${v.id}.html${q}#nocache=${Date.now()}" loading="lazy"></iframe></div>
+      <div class="info">
+        <h3>${v.title}</h3>
+        <div class="desc">${v.desc}</div>
+        <a href="${v.id}.html${q}" class="open" onclick="event.preventDefault(); openFull(${i})">Open full-screen →</a>
+      </div>
+    </div>`).join('');
+}
+
+let _mode = 'live';
+function setMode(m) {
+  _mode = m;
+  document.getElementById('mode-live').classList.toggle('active', m === 'live');
+  document.getElementById('mode-seed').classList.toggle('active', m === 'seed');
+  renderGrid(m);
+  refreshMeta();
+}
+function refreshAll() {
+  renderGrid(_mode);
+  refreshMeta();
+}
+async function refreshMeta() {
+  const q = _mode === 'seed' ? '?seed=1' : '';
+  try {
+    const r = await fetch('/data' + q);
+    const d = await r.json();
+    const open = (d.failures||[]).filter(f => f.status === 'triage' || f.status === 'investigating').length;
+    const total = (d.failures||[]).length;
+    const h = d.hermes ? d.hermes.last24h : null;
+    const sev = h && h.bySev ? h.bySev : { high:0, medium:0, low:0 };
+    const sourceBadge = d._live
+      ? '<span style="background:#0a3d24; color:#4ade80; padding:2px 8px; border-radius:4px; font-size:11px;">● LIVE</span>'
+      : '<span style="background:#3d2a0a; color:#fbbf24; padding:2px 8px; border-radius:4px; font-size:11px;">● SEED' + (d._fallback ? ' (CNCP unreachable)' : '') + '</span>';
+    document.getElementById('meta').innerHTML = `${sourceBadge} · ${open} open / ${total} total · Hermes 24h H${sev.high} M${sev.medium} L${sev.low}`;
+  } catch (e) {
+    document.getElementById('meta').innerHTML = '<span style="color:#ff5555;">meta fetch failed</span>';
+  }
+}
+renderGrid(_mode);
+refreshMeta();
+setInterval(refreshMeta, 30000);
 
 let currentIdx = 0;
 function openFull(i) {
   currentIdx = i;
   const v = variants[i];
-  document.getElementById('full-frame').src = v.id + '.html';
-  document.getElementById('full-label').textContent = v.title;
+  const q = _mode === 'seed' ? '?seed=1' : '';
+  document.getElementById('full-frame').src = v.id + '.html' + q;
+  document.getElementById('full-label').textContent = v.title + (_mode === 'seed' ? ' · SEED' : ' · LIVE');
   document.getElementById('full').style.display = 'block';
 }
 function closeFull() {
diff --git a/public/v1-terminal.html b/public/v1-terminal.html
index f43fea4..8f6d684 100644
--- a/public/v1-terminal.html
+++ b/public/v1-terminal.html
@@ -48,7 +48,7 @@
 </div>
 
 <script>
-fetch('/data').then(r=>r.json()).then(d => {
+fetch('/data' + location.search).then(r=>r.json()).then(d => {
   const sev = s => s==='high'?'<span class="sev high">HIG</span>': s==='medium'?'<span class="sev medium">MED</span>' : s==='low'?'<span class="sev low">LOW</span>' : '<span style="color:#444;">---</span>';
   const ts = t => new Date(t).toISOString().slice(11,16);
   const rows = d.failures.map(f => `
diff --git a/public/v2-newspaper.html b/public/v2-newspaper.html
index c43514c..9d7fa12 100644
--- a/public/v2-newspaper.html
+++ b/public/v2-newspaper.html
@@ -49,7 +49,7 @@
 </div>
 
 <script>
-fetch('/data').then(r=>r.json()).then(d => {
+fetch('/data' + location.search).then(r=>r.json()).then(d => {
   const date = new Date().toLocaleDateString('en-US', { weekday:'long', month:'long', day:'numeric', year:'numeric' });
   document.getElementById('dateStamp').textContent = date.toUpperCase();
   const lab = s => s==='high'?'<span class="lab high">crit</span>': s==='medium'?'<span class="lab med">warn</span>': s==='low'?'<span class="lab low">note</span>':'<span class="lab">unset</span>';
diff --git a/public/v3-heatmap.html b/public/v3-heatmap.html
index 213acce..b93e719 100644
--- a/public/v3-heatmap.html
+++ b/public/v3-heatmap.html
@@ -61,7 +61,7 @@
   </div>
 </div>
 <script>
-fetch('/data').then(r=>r.json()).then(d => {
+fetch('/data' + location.search).then(r=>r.json()).then(d => {
   // build a fake 24-cell heatmap weighted by events (illustrative)
   const cells = Array.from({length: 24}, (_, i) => {
     const r = Math.random();
diff --git a/public/v4-kanban.html b/public/v4-kanban.html
index 8521f9c..7a067a2 100644
--- a/public/v4-kanban.html
+++ b/public/v4-kanban.html
@@ -49,7 +49,7 @@
 <div class="board" id="board"></div>
 
 <script>
-fetch('/data').then(r=>r.json()).then(d => {
+fetch('/data' + location.search).then(r=>r.json()).then(d => {
   const lanes = [
     { id:'triage', title:'Triage', sub:'classified, awaiting action' },
     { id:'investigating', title:'Investigating', sub:'someone is on it' },
diff --git a/public/v5-timeline.html b/public/v5-timeline.html
index 0cb42bf..3975e9d 100644
--- a/public/v5-timeline.html
+++ b/public/v5-timeline.html
@@ -63,7 +63,7 @@
 </div>
 
 <script>
-fetch('/data').then(r=>r.json()).then(d => {
+fetch('/data' + location.search).then(r=>r.json()).then(d => {
   // 24-hour window ending now
   const now = Date.now();
   const span24h = 24 * 3600 * 1000;
diff --git a/public/v6-brutalist.html b/public/v6-brutalist.html
index 2423ea9..6e46d2d 100644
--- a/public/v6-brutalist.html
+++ b/public/v6-brutalist.html
@@ -73,7 +73,7 @@
 </div>
 
 <script>
-fetch('/data').then(r=>r.json()).then(d => {
+fetch('/data' + location.search).then(r=>r.json()).then(d => {
   document.getElementById('stamp').textContent = new Date().toUpperCase ? new Date().toISOString().slice(0,16).replace('T',' ') : '';
   document.getElementById('stamp').textContent = new Date().toISOString().slice(0,16).replace('T',' ') + ' UTC';
   const sev = d.hermes.last24h.bySev;
diff --git a/server.js b/server.js
index e4fa3cf..8ee7d53 100644
--- a/server.js
+++ b/server.js
@@ -8,21 +8,58 @@ const fs = require('fs');
 
 const PORT = parseInt(process.env.PORT, 10) || 9771;
 const BIND = process.env.BIND || '127.0.0.1';
+const CNCP_URL = process.env.CNCP_URL || 'http://127.0.0.1:3333';
 
 const app = express();
 app.use(express.static(path.join(__dirname, 'public')));
 
-app.get('/data', (req, res) => {
+// GET /data
+//   default → LIVE from CNCP (failures + hermes status), merged in same schema as sample-data.json.
+//   ?seed=1 → force the static sample. Useful for screenshots / pixel-stable design comparison.
+//   if live fetch fails (CNCP down) → automatically falls back to seed with `_live: false, _fallback: true`.
+app.get('/data', async (req, res) => {
+  const wantSeed = req.query.seed === '1';
+  res.set('Cache-Control', 'no-store');
+  if (wantSeed) return sendSeed(res, false);
   try {
-    const raw = fs.readFileSync(path.join(__dirname, 'sample-data.json'), 'utf-8');
-    res.set('Cache-Control', 'no-store');
-    res.type('application/json').send(raw);
+    const ctrl = AbortSignal.timeout(2500);
+    const [failuresR, hermesR] = await Promise.all([
+      fetch(CNCP_URL + '/api/failures', { signal: ctrl }),
+      fetch(CNCP_URL + '/api/hermes/status', { signal: ctrl })
+    ]);
+    if (!failuresR.ok) throw new Error('failures HTTP ' + failuresR.status);
+    if (!hermesR.ok) throw new Error('hermes HTTP ' + hermesR.status);
+    const failures = await failuresR.json();
+    const hermes = await hermesR.json();
+    return res.type('application/json').send(JSON.stringify({
+      _live: true,
+      _fetchedAt: Date.now(),
+      _source: 'cncp@' + CNCP_URL,
+      failures,
+      hermes
+    }));
   } catch (e) {
-    res.status(500).json({ error: e.message });
+    return sendSeed(res, true, e.message);
   }
 });
 
-app.get('/health', (_, res) => res.json({ ok: true, variants: 6 }));
+function sendSeed(res, isFallback, errMsg) {
+  try {
+    const raw = JSON.parse(fs.readFileSync(path.join(__dirname, 'sample-data.json'), 'utf-8'));
+    res.type('application/json').send(JSON.stringify({
+      _live: false,
+      _fallback: !!isFallback,
+      _fallbackError: errMsg || null,
+      _fetchedAt: Date.now(),
+      _source: 'sample-data.json',
+      ...raw
+    }));
+  } catch (e) {
+    res.status(500).json({ error: e.message });
+  }
+}
+
+app.get('/health', (_, res) => res.json({ ok: true, variants: 6, cncp: CNCP_URL }));
 
 app.listen(PORT, BIND, () => {
   console.log(`cncp-failures-mockups listening at http://${BIND}:${PORT}/`);

← 885ca32 scaffold: 6 visually-distinct mockups of CNCP Failures+Herme  ·  back to Cncp Failures Mockups  ·  v7 age-adaptive: 3→95 slider morphs UI through 8 design band 12220e8 →