← back to Cncp Failures Mockups
public/index.html
167 lines
<!doctype html><html><head>
<meta charset="utf-8"><title>CNCP Failures — 6 mockup variants</title>
<style>
:root { --bg:#0a0c12; --fg:#e6eef8; --muted:#7c8aa5; --accent:#7c3aed; --panel:#161a26; --border:#252b3d; }
body { margin:0; background:var(--bg); color:var(--fg); font-family: -apple-system,BlinkMacSystemFont,'SF Pro Text',system-ui,sans-serif; min-height:100vh; }
.hero { padding:36px 32px 18px; max-width:1280px; margin:0 auto; }
.hero h1 { font-size:24px; font-weight:700; margin:0 0 4px; letter-spacing:-0.01em; }
.hero .sub { color:var(--muted); font-size:14px; max-width:720px; }
.controls { padding:6px 32px 12px; max-width:1280px; margin:0 auto; display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
.ctrl-btn { background:var(--panel); border:1px solid var(--border); color:var(--fg); padding:6px 12px; border-radius:6px; cursor:pointer; font-size:12px; }
.ctrl-btn:hover { background:#1f2535; }
.ctrl-btn.active { background:var(--accent); border-color:var(--accent); color:#fff; }
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(440px,1fr)); gap:18px; padding:18px 32px 60px; max-width:1280px; margin:0 auto; }
.card { background:var(--panel); border:1px solid var(--border); border-radius:10px; overflow:hidden; display:flex; flex-direction:column; transition: transform 0.15s, border-color 0.15s; cursor:pointer; }
.card:hover { transform:translateY(-2px); border-color:var(--accent); }
.card:hover .preview::after { background:rgba(124,58,237,0.08); }
.card .preview::after { content:''; position:absolute; inset:0; background:transparent; transition:background 0.15s; pointer-events:none; }
.card .preview { aspect-ratio: 16/10; width:100%; background:#000; border-bottom:1px solid var(--border); overflow:hidden; position:relative; }
.card iframe { width:200%; height:200%; transform-origin: 0 0; transform: scale(0.5); border:none; pointer-events:none; }
.card .info { padding:12px 14px 14px; }
.card .info h3 { font-size:14px; font-weight:600; margin:0 0 2px; }
.card .info .desc { color:var(--muted); font-size:12px; line-height:1.45; margin-bottom:10px; }
.card .info .open { display:inline-block; background:transparent; color:var(--accent); border:1px solid var(--accent); padding:5px 12px; border-radius:5px; font-size:11.5px; text-decoration:none; }
.card .info .open:hover { background:var(--accent); color:#fff; }
.full { position:fixed; inset:0; background:#000; z-index:10; display:none; }
.full iframe { width:100%; height:100%; border:none; }
.full .close { position:absolute; top:14px; right:14px; background:var(--panel); border:1px solid var(--border); color:var(--fg); padding:8px 14px; border-radius:6px; cursor:pointer; font-size:12px; z-index:11; }
.full .nav { position:absolute; bottom:18px; left:50%; transform:translateX(-50%); display:flex; gap:6px; background:rgba(0,0,0,0.7); padding:8px 12px; border-radius:8px; z-index:11; backdrop-filter:blur(8px); }
.full .nav .ctrl-btn { font-size:11px; }
kbd { background:#1f2535; border:1px solid var(--border); padding:1px 6px; border-radius:3px; font-size:11px; font-family:ui-monospace,Menlo,monospace; }
</style>
<!-- Meta Pixel — INERT until set-fb-pixel.sh is run (does NOT load fbevents.js or hit Meta until flipped) -->
<script>
(function(){
var PIXEL_ID = '{{FACEBOOK_PIXEL_ID}}';
// Inert until flipped — the brace check prevents BOTH the loader IIFE
// AND the init call from running, so fbevents.js is never fetched.
if (PIXEL_ID.indexOf('{{') !== -1) return;
!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};if(!f._fbq)f._fbq=n;
n.push=n;n.loaded=!0;n.version='2.0';n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];s.parentNode.insertBefore(t,s)}(window,
document,'script','https://connect.facebook.net/en_US/fbevents.js');
fbq('init', PIXEL_ID);
fbq('track', 'PageView');
})();
</script>
<!-- noscript fallback is added by set-fb-pixel.sh on flip (omitted here to avoid unflipped img phone-home) -->
<!-- End Meta Pixel -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
</head><body>
<div class="hero">
<h1>CNCP Failures — 6 mockup variants</h1>
<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;">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>
<div class="full" id="full">
<button class="close" onclick="closeFull()">✕ Close (Esc)</button>
<iframe id="full-frame"></iframe>
<div class="nav">
<button class="ctrl-btn" onclick="navVariant(-1)">← Prev</button>
<span id="full-label" style="color:var(--muted); font-size:12px; align-self:center; min-width:200px; text-align:center;"></span>
<button class="ctrl-btn" onclick="navVariant(1)">Next →</button>
</div>
</div>
<script>
const variants = [
{ id:'v1-terminal', title:'1. Terminal · Log-Stream', desc:'Green-on-black CRT, monospace, severity inline. Built for the engineer who lives in tmux.' },
{ id:'v2-newspaper', title:'2. Newspaper · Wire-Service', desc:'Old-style dispatch with masthead, columns, byline. Reads failures as news, not noise.' },
{ id:'v3-heatmap', title:'3. Heatmap · Dashboard', desc:'Density-first: 24-hour severity grid + glanceable stat cells. SRE dashboard energy.' },
{ id:'v4-kanban', title:'4. Kanban · Status Board', desc:'Four lanes (triage → investigating → resolved → dismissed). Atlassian-ish, action-oriented.' },
{ id:'v5-timeline', title:'5. Timeline · APM Trace', desc:'Each process is a lane; spans show event windows. Datadog-style, time-as-axis.' },
{ id:'v6-brutalist', title:'6. Brutalist · Data-Dense', desc:'Hard borders, full table, every field visible. No chrome — just the facts.' },
{ id:'v7-age-adaptive', title:'7. Age-Adaptive · 3 → 95', desc:'Slider morphs the whole UI through 8 design bands — toddler primary colors → elementary playful → tween → teen neon → adult → mature serif → senior hi-contrast → elder essentials only.' }
];
function renderGrid(mode) {
const q = mode === 'seed' ? '?seed=1' : '';
document.getElementById('grid').innerHTML = variants.map((v, i) => `
<div class="card" onclick="openFull(${i})" role="link" tabindex="0" aria-label="Open ${v.title}" data-variant-id="${v.id}">
<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.stopPropagation(); event.preventDefault(); openFull(${i})">Open full-screen →</a>
<a href="${v.id}.html${q}" class="open" style="margin-left:8px" onclick="event.stopPropagation()" target="_blank" rel="noopener noreferrer">Open in new tab ↗</a>
</div>
</div>`).join('');
// Enter/Space on a focused card opens it
document.getElementById('grid').addEventListener('keydown', function(e) {
if (e.key !== 'Enter' && e.key !== ' ') return;
const card = e.target.closest('.card');
if (!card) return;
e.preventDefault();
const idx = Array.prototype.indexOf.call(card.parentNode.children, card);
openFull(idx);
});
}
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];
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() {
document.getElementById('full').style.display = 'none';
document.getElementById('full-frame').src = '';
}
function navVariant(delta) {
currentIdx = (currentIdx + delta + variants.length) % variants.length;
openFull(currentIdx);
}
window.addEventListener('keydown', e => {
if (document.getElementById('full').style.display !== 'block') return;
if (e.key === 'Escape') closeFull();
if (e.key === 'ArrowLeft') navVariant(-1);
if (e.key === 'ArrowRight') navVariant(1);
});
</script>
</body></html>