← 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
M public/index.htmlM public/v1-terminal.htmlM public/v2-newspaper.htmlM public/v3-heatmap.htmlM public/v4-kanban.htmlM public/v5-timeline.htmlM public/v6-brutalist.htmlM server.js
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 →