← back to Cncp Failures Mockups

public/v4-kanban.html

91 lines

<!doctype html><html><head>
<meta charset="utf-8"><title>v4 — Kanban board</title>
<style>
  :root { --bg:#f4f5f7; --ink:#172b4d; --muted:#5e6c84; --col-bg:#ebecf0; --card:#fff; --hi:#de350b; --med:#ff8b00; --lo:#0052cc; --ok:#36b37e; --shadow: 0 1px 0 rgba(9,30,66,0.25); }
  body { margin:0; background:var(--bg); color:var(--ink); font-family: -apple-system,BlinkMacSystemFont,'SF Pro Text',system-ui,sans-serif; padding:0; min-height:100vh; }
  header { padding:14px 24px; border-bottom:1px solid #dfe1e6; background:#fff; display:flex; justify-content:space-between; align-items:center; }
  header h1 { font-size:18px; font-weight:600; margin:0; }
  header .meta { font-size:12px; color:var(--muted); }
  .board { display:grid; grid-template-columns: repeat(4, 1fr); gap:14px; padding:18px 24px; align-items:start; }
  .col { background:var(--col-bg); border-radius:6px; padding:10px; min-height: 65vh; }
  .col h2 { font-size:12px; font-weight:600; text-transform:uppercase; letter-spacing:1px; color:var(--muted); margin:0 0 10px; display:flex; justify-content:space-between; }
  .col h2 .count { background:#dfe1e6; padding:2px 8px; border-radius:10px; font-size:11px; }
  .card { background:var(--card); border-radius:4px; padding:10px 12px; box-shadow:var(--shadow); margin-bottom:8px; font-size:13px; cursor:grab; transition: box-shadow 0.15s; }
  .card:hover { box-shadow: 0 2px 8px rgba(9,30,66,0.16); }
  .card .src { display:inline-block; font-size:10px; text-transform:uppercase; letter-spacing:1px; padding:2px 6px; border-radius:3px; color:#fff; }
  .card .src.pm2{ background:#0052cc; }
  .card .src.launchd{ background:#6554c0; }
  .card .src.agent{ background:#0747a6; }
  .card .src.build{ background:#974f0c; }
  .card .name { font-family:ui-monospace,Menlo,monospace; font-weight:600; margin-top:6px; font-size:12.5px; word-break:break-all; }
  .card .last { color:var(--muted); font-size:11px; margin-top:6px; line-height:1.4; max-height:42px; overflow:hidden; }
  .card .action { color:var(--lo); font-size:11px; font-style:italic; margin-top:6px; }
  .card .footer { display:flex; justify-content:space-between; align-items:center; margin-top:8px; }
  .sev { width:6px; height:6px; border-radius:50%; display:inline-block; vertical-align:middle; }
  .sev.h { background:var(--hi); }
  .sev.m { background:var(--med); }
  .sev.l { background:var(--lo); }
  .sev.u { background:#a5adba; }
  .pill { background:#dfe1e6; padding:2px 7px; border-radius:10px; font-size:11px; color:var(--muted); }
  .hermes-bar { background:#fff; border:1px solid #dfe1e6; border-radius:6px; padding:12px 16px; margin: 0 24px 12px; display:grid; grid-template-columns: auto 1fr auto; gap:18px; align-items:center; }
  .hermes-bar .pulse { width:8px; height:8px; border-radius:50%; background:var(--ok); display:inline-block; animation: pulse 2s infinite; }
  @keyframes pulse { 50% { opacity:0.3; } }
  .hermes-bar .stat-line { font-size:12px; color:var(--muted); }
  .hermes-bar .stat-line b { color:var(--ink); font-weight:600; }
  .hermes-bar .recent { font-size:11px; color:var(--muted); }
</style>
</head><body>
<header>
  <h1>CNCP Failures · Board</h1>
  <div class="meta">drag cards between lanes · auto-classify on triage</div>
</header>

<div class="hermes-bar">
  <div><span class="pulse"></span> <b>Hermes 3</b> · 8B · in mem</div>
  <div class="stat-line" id="h-stats"></div>
  <div class="recent" id="h-recent"></div>
</div>

<div class="board" id="board"></div>

<script>
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' },
    { id:'resolved', title:'Resolved', sub:'fix verified' },
    { id:'dismissed', title:'Dismissed', sub:'transient · auto-resolved' }
  ];
  const sevClass = s => s==='high'?'h':s==='medium'?'m':s==='low'?'l':'u';
  const byLane = Object.fromEntries(lanes.map(l => [l.id, []]));
  for (const f of d.failures) (byLane[f.status] || (byLane[f.status]=[])).push(f);

  document.getElementById('board').innerHTML = lanes.map(l => `
    <div class="col">
      <h2>${l.title}<span class="count">${(byLane[l.id]||[]).length}</span></h2>
      ${(byLane[l.id]||[]).map(f => `
        <div class="card">
          <div><span class="src ${f.source}">${f.source}</span> <span class="sev ${sevClass(f.severity)}"></span> <span class="pill">×${f.eventCount}</span></div>
          <div class="name">${f.processName}</div>
          <div class="last">${(f.lastLines||'').split('\\n').filter(Boolean)[0] || ''}</div>
          ${f.suggestedAction ? `<div class="action">→ ${f.suggestedAction}</div>` : ''}
          <div class="footer">
            <span style="font-size:10px; color:var(--muted);">${new Date(f.lastSeen).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit'})}</span>
            <span style="font-size:10px; color:var(--muted);">#${f.id.slice(0,7)}</span>
          </div>
        </div>
      `).join('')}
    </div>`).join('');

  const h = d.hermes.last24h;
  const sev = h.bySev;
  document.getElementById('h-stats').innerHTML = `
    <b>${h.total}</b> calls · <b>${h.byType.classify}</b> cls · <b>${h.byType.chat}</b> chat · <b>${h.byType['idea-chat']}</b> idea ·
    sev <b style="color:var(--hi)">${sev.high}H</b> <b style="color:var(--med)">${sev.medium}M</b> <b style="color:var(--lo)">${sev.low}L</b> ·
    p50 <b>${(h.avgLatencyMs/1000).toFixed(1)}s</b>`;
  document.getElementById('h-recent').innerHTML = d.hermes.recent.slice(0,3).map(c => `
    <div>${c.ok?'✓':'✗'} ${c.type}: ${c.target.slice(0,28)} (${(c.latencyMs/1000).toFixed(1)}s)</div>`).join('');
});
</script>
</body></html>