@i4ctime/q-ring 0.9.6 → 0.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-WG4ZKN7Q.js → chunk-JV4JFWV6.js} +94 -45
- package/dist/chunk-JV4JFWV6.js.map +1 -0
- package/dist/{chunk-5JBU7TWN.js → chunk-ZV7LRKBA.js} +94 -45
- package/dist/chunk-ZV7LRKBA.js.map +1 -0
- package/dist/{dashboard-JT5ZNLT5.js → dashboard-4H474M2N.js} +3 -3
- package/dist/{dashboard-Q5OQRQCX.js.map → dashboard-4H474M2N.js.map} +1 -1
- package/dist/{dashboard-Q5OQRQCX.js → dashboard-5L4ILERJ.js} +3 -3
- package/dist/{dashboard-JT5ZNLT5.js.map → dashboard-5L4ILERJ.js.map} +1 -1
- package/dist/index.js +44 -8
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +46 -12
- package/dist/mcp.js.map +1 -1
- package/package.json +3 -2
- package/dist/chunk-5JBU7TWN.js.map +0 -1
- package/dist/chunk-WG4ZKN7Q.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/dashboard.ts","../src/core/dashboard-html.ts"],"sourcesContent":["/**\n * Quantum Status Dashboard: local HTTP server with SSE live updates.\n *\n * Collects a full snapshot of all quantum state every few seconds and\n * pushes it to connected browsers. Never exposes secret values.\n */\n\nimport { createServer, type IncomingMessage, type ServerResponse, type Server } from \"node:http\";\nimport { listSecrets } from \"./keyring.js\";\nimport { checkDecay, type DecayStatus, type QuantumEnvelope } from \"./envelope.js\";\nimport { listEntanglements, type EntanglementPair } from \"./entanglement.js\";\nimport { tunnelList } from \"./tunnel.js\";\nimport { queryAudit, detectAnomalies, type AuditEvent, type AccessAnomaly } from \"./observer.js\";\nimport { collapseEnvironment, type CollapseResult } from \"./collapse.js\";\nimport { getDashboardHtml } from \"./dashboard-html.js\";\n\nexport interface SecretSnapshot {\n key: string;\n scope: string;\n type: \"collapsed\" | \"superposition\";\n environments?: string[];\n defaultEnv?: string;\n decay: DecayStatus;\n accessCount: number;\n lastAccessedAt?: string;\n createdAt: string;\n updatedAt: string;\n description?: string;\n tags?: string[];\n entangled?: { service: string; key: string }[];\n}\n\nexport interface TunnelSnapshot {\n id: string;\n createdAt: number;\n expiresAt?: number;\n accessCount: number;\n maxReads?: number;\n}\n\nexport interface DashboardSnapshot {\n timestamp: string;\n secrets: SecretSnapshot[];\n health: { healthy: number; stale: number; expired: number; noDecay: number; total: number };\n entanglements: EntanglementPair[];\n tunnels: TunnelSnapshot[];\n audit: AuditEvent[];\n anomalies: AccessAnomaly[];\n environment: CollapseResult | null;\n}\n\nfunction toSecretSnapshot(entry: {\n key: string;\n scope: string;\n envelope?: QuantumEnvelope;\n decay?: DecayStatus;\n}): SecretSnapshot {\n const envelope = entry.envelope;\n const decay = envelope ? checkDecay(envelope) : {\n isExpired: false,\n isStale: false,\n lifetimePercent: 0,\n secondsRemaining: null,\n timeRemaining: null,\n };\n\n return {\n key: entry.key,\n scope: entry.scope,\n type: envelope?.states ? \"superposition\" : \"collapsed\",\n environments: envelope?.states ? Object.keys(envelope.states) : undefined,\n defaultEnv: envelope?.defaultEnv,\n decay,\n accessCount: envelope?.meta.accessCount ?? 0,\n lastAccessedAt: envelope?.meta.lastAccessedAt,\n createdAt: envelope?.meta.createdAt ?? \"\",\n updatedAt: envelope?.meta.updatedAt ?? \"\",\n description: envelope?.meta.description,\n tags: envelope?.meta.tags,\n entangled: envelope?.meta.entangled,\n };\n}\n\nexport function collectSnapshot(): DashboardSnapshot {\n const entries = listSecrets({ source: \"api\", silent: true });\n\n const secrets = entries.map(toSecretSnapshot);\n\n let healthy = 0;\n let stale = 0;\n let expired = 0;\n let noDecay = 0;\n\n for (const s of secrets) {\n if (!s.decay.timeRemaining) {\n noDecay++;\n } else if (s.decay.isExpired) {\n expired++;\n } else if (s.decay.isStale) {\n stale++;\n } else {\n healthy++;\n }\n }\n\n return {\n timestamp: new Date().toISOString(),\n secrets,\n health: { healthy, stale, expired, noDecay, total: secrets.length },\n entanglements: listEntanglements(),\n tunnels: tunnelList(),\n audit: queryAudit({ limit: 50 }).filter(e => e.action !== \"list\"),\n anomalies: detectAnomalies(),\n environment: collapseEnvironment(),\n };\n}\n\nexport interface DashboardServerOptions {\n port?: number;\n}\n\nexport function startDashboardServer(\n options: DashboardServerOptions = {},\n): { port: number; close: () => void; server: Server } {\n const port = options.port ?? 9876;\n const clients = new Set<ServerResponse>();\n let intervalHandle: ReturnType<typeof setInterval> | null = null;\n\n const html = getDashboardHtml();\n\n function broadcast() {\n const snapshot = collectSnapshot();\n const data = `data: ${JSON.stringify(snapshot)}\\n\\n`;\n for (const res of clients) {\n if (res.writableEnded || res.destroyed) {\n clients.delete(res);\n continue;\n }\n try {\n const ok = res.write(data);\n if (!ok) {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n } catch {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n }\n }\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n let pathname: string;\n try {\n ({ pathname } = new URL(req.url ?? \"/\", \"http://127.0.0.1\"));\n } catch {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Bad Request: invalid URL\");\n return;\n }\n\n if (pathname === \"/events\") {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n\n const snapshot = collectSnapshot();\n res.write(`data: ${JSON.stringify(snapshot)}\\n\\n`);\n\n clients.add(res);\n req.on(\"close\", () => clients.delete(res));\n return;\n }\n\n if (pathname === \"/api/status\") {\n const snapshot = collectSnapshot();\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n });\n res.end(JSON.stringify(snapshot, null, 2));\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(html);\n });\n\n server.listen(port, \"127.0.0.1\", () => {\n intervalHandle = setInterval(broadcast, 5000);\n if (intervalHandle && typeof intervalHandle === \"object\" && \"unref\" in intervalHandle) {\n intervalHandle.unref();\n }\n });\n\n return {\n port,\n close: () => {\n if (intervalHandle) clearInterval(intervalHandle);\n for (const res of clients) {\n try { res.end(); } catch { /* noop */ }\n }\n clients.clear();\n server.close();\n },\n server,\n };\n}\n","/**\n * Self-contained HTML dashboard for q-ring quantum status.\n * Matches the gh-pages site design system: Outfit + JetBrains Mono fonts,\n * deep navy palette, neon cyan→violet SVG icons, glassmorphism cards.\n *\n * Zero dependencies — inline CSS + vanilla JS with EventSource for SSE.\n */\n\nexport function getDashboardHtml(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\"/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n<title>q-ring — quantum status</title>\n<style>\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg-deep:#04080f;\n --bg-section:#080e18;\n --bg-card:#0f1a2e;\n --bg-card-hover:#142240;\n --border:#1a2d4a;\n --border-glow:#0ea5e9;\n\n --text-primary:#f8f8ff;\n --text-secondary:#c8cfe0;\n --text-dim:#8899b4;\n\n --accent:#0ea5e9;\n --accent-bright:#38bdf8;\n --accent-dim:rgba(14,165,233,0.15);\n --accent-glow:rgba(14,165,233,0.4);\n\n --danger:#ff5e5b;\n --warning:#fbbf24;\n --green:#22c55e;\n --violet:#a855f7;\n --pink:#ff0055;\n\n --font-display:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;\n --font-mono:'SF Mono','Cascadia Code','Fira Code',Consolas,'Liberation Mono',monospace;\n\n --radius:12px;\n --radius-sm:8px;\n --radius-lg:20px;\n\n --glass-bg:rgba(15,26,46,0.65);\n --glass-border:rgba(26,45,74,0.8);\n --shadow-card:0 4px 24px -1px rgba(0,0,0,0.4),0 0 1px rgba(255,255,255,0.04);\n --shadow-glow:0 0 20px rgba(14,165,233,0.12),0 0 40px rgba(168,85,247,0.08);\n}\nhtml{background:var(--bg-deep);color:var(--text-primary);font-family:var(--font-display);font-size:16px;line-height:1.5}\nbody{min-height:100vh;overflow-x:hidden;position:relative}\n\n/* Hidden SVG defs for gradient icons */\n.svg-defs{position:absolute;width:0;height:0;overflow:hidden}\n\n/* Mesh blobs */\n.blob{position:fixed;border-radius:50%;filter:blur(100px);opacity:.12;pointer-events:none;z-index:0}\n.blob-1{width:600px;height:600px;top:-120px;left:-100px;background:radial-gradient(circle,var(--accent),transparent 70%);animation:drift1 22s ease-in-out infinite}\n.blob-2{width:500px;height:500px;bottom:-80px;right:-60px;background:radial-gradient(circle,var(--violet),transparent 70%);animation:drift2 26s ease-in-out infinite}\n.blob-3{width:350px;height:350px;top:40%;left:50%;background:radial-gradient(circle,var(--green),transparent 70%);animation:drift3 30s ease-in-out infinite;opacity:.06}\n@keyframes drift1{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(60px,-40px) scale(1.08)}66%{transform:translate(-30px,50px) scale(.95)}}\n@keyframes drift2{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(-50px,30px) scale(1.05)}66%{transform:translate(40px,-60px) scale(.92)}}\n@keyframes drift3{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-40%,-60%) scale(1.1)}}\n\n.container{position:relative;z-index:1;max-width:1280px;margin:0 auto;padding:24px 20px 48px}\n\n/* Header */\n.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;padding:16px 20px;background:rgba(4,8,15,0.75);backdrop-filter:blur(16px) saturate(1.2);-webkit-backdrop-filter:blur(16px) saturate(1.2);border:1px solid var(--border);border-radius:var(--radius)}\n.header h1{font-family:var(--font-display);font-size:1.65rem;font-weight:700;letter-spacing:-.02em;display:flex;align-items:center;gap:10px}\n.header h1 .q-icon{display:flex;filter:drop-shadow(0 0 6px rgba(14,165,233,0.6))}\n.header h1 .brand{background:linear-gradient(135deg,#00D1FF,var(--violet));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}\n.header h1 .sub{color:var(--text-dim);font-weight:400;font-size:1.1rem;-webkit-text-fill-color:var(--text-dim)}\n.status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);display:inline-block;animation:pulse 2s ease-in-out infinite}\n.status-dot.disconnected{background:var(--danger);box-shadow:0 0 8px var(--danger)}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}\n.conn-label{font-size:.85rem;color:var(--text-dim);display:flex;align-items:center;gap:6px;font-family:var(--font-mono)}\n\n/* Grid */\n.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:16px}\n.grid-wide{grid-column:1/-1}\n\n/* Cards — reveal animation */\n.card{background:var(--glass-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--glass-border);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow-card);transition:border-color .3s,box-shadow .3s,transform .3s;opacity:0;transform:translateY(16px);animation:cardReveal .5s cubic-bezier(0.16,1,0.3,1) forwards}\n.card:hover{border-color:var(--border-glow);box-shadow:var(--shadow-card),var(--shadow-glow);transform:translateY(-2px)}\n@keyframes cardReveal{to{opacity:1;transform:translateY(0)}}\n\n.card-title{font-family:var(--font-display);font-size:.8rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);margin-bottom:14px;display:flex;align-items:center;gap:8px;font-weight:600}\n.card-title svg{width:16px;height:16px;flex-shrink:0;filter:drop-shadow(0 0 4px rgba(14,165,233,0.4))}\n\n/* Health donut */\n.health-row{display:flex;align-items:center;gap:24px}\n.donut-wrap{position:relative;width:100px;height:100px;flex-shrink:0}\n.donut-wrap svg{transform:rotate(-90deg)}\n.donut-wrap .donut-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;font-size:1.5rem;font-weight:700;line-height:1;font-family:var(--font-display)}\n.donut-wrap .donut-label small{font-size:.7rem;color:var(--text-dim);font-weight:400;margin-top:2px;letter-spacing:.04em}\n.health-legend{display:flex;flex-direction:column;gap:6px}\n.legend-item{display:flex;align-items:center;gap:8px;font-size:.88rem}\n.legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n\n/* Decay bars */\n.decay-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.decay-item{display:flex;align-items:center;gap:10px}\n.decay-key{font-family:var(--font-mono);font-size:.85rem;min-width:120px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.decay-bar{flex:1;height:6px;border-radius:3px;background:rgba(255,255,255,0.06);overflow:hidden;position:relative}\n.decay-fill{height:100%;border-radius:3px;transition:width .6s ease}\n.decay-time{font-size:.8rem;color:var(--text-dim);min-width:56px;text-align:right;font-family:var(--font-mono)}\n\n/* Superposition pills */\n.super-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.super-item{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.super-key{font-family:var(--font-mono);font-size:.85rem;min-width:100px}\n.env-pill{font-size:.75rem;padding:2px 8px;border-radius:99px;font-weight:600;letter-spacing:.03em;font-family:var(--font-mono)}\n.env-prod{background:rgba(255,0,85,0.2);color:var(--pink);border:1px solid rgba(255,0,85,0.3)}\n.env-staging{background:rgba(251,191,36,0.15);color:var(--warning);border:1px solid rgba(251,191,36,0.25)}\n.env-dev{background:rgba(34,197,94,0.15);color:var(--green);border:1px solid rgba(34,197,94,0.25)}\n.env-default{background:rgba(168,85,247,0.15);color:var(--violet);border:1px solid rgba(168,85,247,0.25)}\n\n/* Entanglement */\n.entangle-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow-y:auto}\n.entangle-pair{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.85rem}\n.entangle-arrow{color:var(--accent-bright)}\n\n/* Tunnels */\n.tunnel-list{display:flex;flex-direction:column;gap:8px;max-height:240px;overflow-y:auto}\n.tunnel-card{background:rgba(168,85,247,0.06);border:1px solid rgba(168,85,247,0.15);border-radius:var(--radius-sm);padding:10px 12px;font-size:.85rem;font-family:var(--font-mono)}\n.tunnel-meta{display:flex;gap:12px;margin-top:4px;font-size:.8rem;color:var(--text-dim)}\n\n/* Audit feed */\n.audit-feed{display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto;font-size:.85rem;font-family:var(--font-mono)}\n.audit-row{display:flex;gap:8px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.03)}\n.audit-ts{color:var(--text-dim);min-width:70px;flex-shrink:0}\n.audit-action{min-width:64px;font-weight:600}\n.audit-action.read{color:var(--accent)}.audit-action.write{color:var(--green)}.audit-action.delete{color:var(--danger)}\n.audit-action.entangle{color:var(--violet)}.audit-action.tunnel{color:var(--violet)}.audit-action.teleport{color:var(--warning)}\n.audit-action.generate{color:var(--warning)}.audit-action.list{color:var(--text-dim)}.audit-action.export{color:var(--text-dim)}\n.audit-action.collapse{color:var(--accent)}\n\n/* Anomalies */\n.anomaly-card{background:rgba(255,94,91,0.06);border:1px solid rgba(255,94,91,0.15);border-radius:var(--radius-sm);padding:10px 14px;animation:anomaly-pulse 3s ease-in-out infinite}\n@keyframes anomaly-pulse{0%,100%{border-color:rgba(255,94,91,0.15)}50%{border-color:rgba(255,94,91,0.4)}}\n.anomaly-type{font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--danger);font-weight:700;margin-bottom:2px;font-family:var(--font-display)}\n.anomaly-desc{font-size:.88rem;color:var(--text-primary)}\n\n/* Environment badge */\n.env-status{display:flex;align-items:center;gap:12px}\n.env-big{font-size:1.1rem;font-weight:700;padding:4px 14px;border-radius:var(--radius-sm)}\n.env-source{font-size:.85rem;color:var(--text-dim)}\n\n/* Empty states */\n.empty{color:var(--text-dim);font-size:.88rem;font-style:italic;padding:8px 0}\n\n/* Scrollbar */\n::-webkit-scrollbar{width:4px;height:4px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);border-radius:2px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.15)}\n</style>\n</head>\n<body>\n\n<!-- Shared SVG gradient definition -->\n<svg class=\"svg-defs\" aria-hidden=\"true\" focusable=\"false\">\n <defs>\n <linearGradient id=\"neon-grad\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\n <stop offset=\"0%\" stop-color=\"#00D1FF\"/>\n <stop offset=\"100%\" stop-color=\"#a855f7\"/>\n </linearGradient>\n </defs>\n</svg>\n\n<div class=\"blob blob-1\"></div>\n<div class=\"blob blob-2\"></div>\n<div class=\"blob blob-3\"></div>\n<div class=\"container\">\n <div class=\"header\">\n <h1>\n <span class=\"q-icon\"><svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg></span>\n <span class=\"brand\">q-ring</span>\n <span class=\"sub\">quantum status</span>\n </h1>\n <div class=\"conn-label\"><span class=\"status-dot\" id=\"connDot\"></span><span id=\"connText\">connecting\\u2026</span></div>\n </div>\n <div class=\"grid\" id=\"dashboard\">\n <div class=\"card\"><div class=\"empty\">Connecting to q-ring\\u2026</div></div>\n </div>\n</div>\n\n<script>\n(function(){\n const $ = (id) => document.getElementById(id);\n const dash = $('dashboard');\n const dot = $('connDot');\n const connText = $('connText');\n\n /* --- SVG icon library (matching gh-pages neon gradient) --- */\n const icons = {\n health: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/></svg>',\n environment: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/></svg>',\n decay: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 22h14\"/><path d=\"M5 2h14\"/><path d=\"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22\"/><path d=\"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2\"/></svg>',\n superposition: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 2 7 12 12 22 7 12 2\"/><polyline points=\"2 17 12 22 22 17\"/><polyline points=\"2 12 12 17 22 12\"/></svg>',\n entangle: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>',\n tunnel: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 10h.01\"/><path d=\"M15 10h.01\"/><path d=\"M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z\"/></svg>',\n anomaly: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/></svg>',\n audit: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg>'\n };\n\n function esc(s){ const d=document.createElement('div');d.textContent=s;return d.innerHTML; }\n\n function envClass(e){\n if(e==='prod'||e==='production') return 'env-prod';\n if(e==='staging'||e==='stage') return 'env-staging';\n if(e==='dev'||e==='development') return 'env-dev';\n return 'env-default';\n }\n\n function decayColor(pct,expired){\n if(expired) return 'var(--danger)';\n if(pct>=90) return 'var(--danger)';\n if(pct>=75) return 'var(--warning)';\n return 'var(--accent)';\n }\n\n function fmtTime(ts){\n const d=new Date(ts);\n return d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')+':'+d.getSeconds().toString().padStart(2,'0');\n }\n\n function renderHealth(h){\n const total=h.total||1;\n const r=42, circ=2*Math.PI*r;\n const slices=[\n {v:h.healthy,c:'var(--accent)'},\n {v:h.stale,c:'var(--warning)'},\n {v:h.expired,c:'var(--danger)'},\n {v:h.noDecay,c:'var(--text-dim)'}\n ];\n let offset=0;\n let rings='';\n for(const sl of slices){\n const pct=sl.v/total;\n const len=pct*circ;\n rings+=\\`<circle cx=\"50\" cy=\"50\" r=\"\\${r}\" fill=\"none\" stroke=\"\\${sl.c}\" stroke-width=\"12\" stroke-dasharray=\"\\${len} \\${circ-len}\" stroke-dashoffset=\"-\\${offset}\" stroke-linecap=\"round\" opacity=\"0.85\"/>\\`;\n offset+=len;\n }\n return \\`<div class=\"health-row\">\n <div class=\"donut-wrap\">\n <svg viewBox=\"0 0 100 100\" width=\"100\" height=\"100\">\\${rings}</svg>\n <div class=\"donut-label\">\\${h.total}<small>secrets</small></div>\n </div>\n <div class=\"health-legend\">\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--accent)\"></span>Healthy \\${h.healthy}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--warning)\"></span>Stale \\${h.stale}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--danger)\"></span>Expired \\${h.expired}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--text-dim)\"></span>No decay \\${h.noDecay}</div>\n </div>\n </div>\\`;\n }\n\n function renderDecay(secrets){\n const withDecay=secrets.filter(s=>s.decay&&s.decay.timeRemaining);\n if(!withDecay.length) return '<div class=\"empty\">No secrets with decay configured</div>';\n return '<div class=\"decay-list\">'+withDecay.map(s=>{\n const pct=Math.min(s.decay.lifetimePercent,100);\n const col=decayColor(pct,s.decay.isExpired);\n const label=s.decay.isExpired?'expired':s.decay.timeRemaining||'';\n return \\`<div class=\"decay-item\">\n <span class=\"decay-key\" title=\"\\${esc(s.key)}\">\\${esc(s.key)}</span>\n <div class=\"decay-bar\"><div class=\"decay-fill\" style=\"width:\\${pct}%;background:\\${col}\"></div></div>\n <span class=\"decay-time\" style=\"color:\\${col}\">\\${esc(label)}</span>\n </div>\\`;\n }).join('')+'</div>';\n }\n\n function renderSuperposition(secrets){\n const sup=secrets.filter(s=>s.type==='superposition'&&s.environments);\n if(!sup.length) return '<div class=\"empty\">No secrets in superposition</div>';\n return '<div class=\"super-list\">'+sup.map(s=>{\n const pills=(s.environments||[]).map(e=>{\n const isDefault=e===s.defaultEnv;\n return \\`<span class=\"env-pill \\${envClass(e)}\">\\${esc(e)}\\${isDefault?' \\u2713':''}</span>\\`;\n }).join('');\n return \\`<div class=\"super-item\"><span class=\"super-key\">\\${esc(s.key)}</span>\\${pills}</div>\\`;\n }).join('')+'</div>';\n }\n\n function renderEntanglements(pairs){\n if(!pairs.length) return '<div class=\"empty\">No entangled secrets</div>';\n const seen=new Set();\n const unique=pairs.filter(p=>{\n const id=[p.source.service,p.source.key,p.target.service,p.target.key].sort().join('|');\n if(seen.has(id)) return false;\n seen.add(id);return true;\n });\n return '<div class=\"entangle-list\">'+unique.map(p=>\n \\`<div class=\"entangle-pair\"><span>\\${esc(p.source.key)}</span><span class=\"entangle-arrow\">\\u2194</span><span>\\${esc(p.target.key)}</span></div>\\`\n ).join('')+'</div>';\n }\n\n function renderTunnels(tunnels){\n if(!tunnels.length) return '<div class=\"empty\">No active tunnels</div>';\n return '<div class=\"tunnel-list\">'+tunnels.map(t=>{\n const rem=t.expiresAt?Math.max(0,Math.floor((t.expiresAt-Date.now())/1000)):null;\n return \\`<div class=\"tunnel-card\">\\${esc(t.id)}<div class=\"tunnel-meta\">\n <span>reads: \\${t.accessCount}\\${t.maxReads?'/'+t.maxReads:''}</span>\n \\${rem!==null?'<span>expires: '+rem+'s</span>':'<span>no expiry</span>'}\n </div></div>\\`;\n }).join('')+'</div>';\n }\n\n function renderAudit(events){\n if(!events.length) return '<div class=\"empty\">No audit events</div>';\n return '<div class=\"audit-feed\">'+events.slice(0,40).map(e=>\n \\`<div class=\"audit-row\">\n <span class=\"audit-ts\">\\${fmtTime(e.timestamp)}</span>\n <span class=\"audit-action \\${e.action}\">\\${e.action}</span>\n <span class=\"audit-key\">\\${e.key?esc(e.key):''}</span>\n <span class=\"audit-detail\">\\${e.detail?esc(e.detail):''}</span>\n </div>\\`\n ).join('')+'</div>';\n }\n\n function renderAnomalies(anomalies){\n if(!anomalies.length) return '<div class=\"empty\">No anomalies detected \\u2014 all clear</div>';\n return anomalies.map(a=>\n \\`<div class=\"anomaly-card\"><div class=\"anomaly-type\">\\${esc(a.type)}</div><div class=\"anomaly-desc\">\\${esc(a.description)}</div></div>\\`\n ).join('');\n }\n\n function renderEnvironment(env){\n if(!env) return '<div class=\"empty\">No environment detected</div>';\n return \\`<div class=\"env-status\">\n <span class=\"env-big env-pill \\${envClass(env.env)}\">\\${esc(env.env)}</span>\n <span class=\"env-source\">detected via \\${esc(env.source)}</span>\n </div>\\`;\n }\n\n const panels = [\n { id:'p-health', icon:icons.health, label:'Health Summary', wide:false, render:s=>renderHealth(s.health) },\n { id:'p-env', icon:icons.environment, label:'Environment', wide:false, render:s=>renderEnvironment(s.environment) },\n { id:'p-decay', icon:icons.decay, label:'Decay Timers', wide:false, render:s=>renderDecay(s.secrets) },\n { id:'p-super', icon:icons.superposition, label:'Superposition States', wide:false, render:s=>renderSuperposition(s.secrets) },\n { id:'p-ent', icon:icons.entangle, label:'Entanglement', wide:false, render:s=>renderEntanglements(s.entanglements) },\n { id:'p-tunnel', icon:icons.tunnel, label:'Quantum Tunnels', wide:false, render:s=>renderTunnels(s.tunnels) },\n { id:'p-anomaly', icon:icons.anomaly, label:'Anomaly Alerts', wide:true, render:s=>renderAnomalies(s.anomalies) },\n { id:'p-audit', icon:icons.audit, label:'Audit Log', wide:true, render:s=>renderAudit(s.audit) }\n ];\n\n let initialised = false;\n\n function render(snap){\n if(!initialised){\n initialised = true;\n dash.innerHTML = panels.map((p,i) =>\n \\`<div class=\"card\\${p.wide?' grid-wide':''}\" id=\"\\${p.id}\" style=\"animation-delay:\\${i*60}ms\">\n <div class=\"card-title\">\\${p.icon} \\${p.label}</div>\n <div class=\"card-body\" id=\"\\${p.id}-body\"></div>\n </div>\\`\n ).join('');\n }\n for(const p of panels){\n const body = $(\\`\\${p.id}-body\\`);\n if(body){\n const html = p.render(snap);\n if(body._prev !== html){ body.innerHTML = html; body._prev = html; }\n }\n }\n }\n\n function connect(){\n const es=new EventSource('/events');\n es.onopen=()=>{\n dot.classList.remove('disconnected');\n connText.textContent='live';\n };\n es.onmessage=(e)=>{\n try{ render(JSON.parse(e.data)); }catch(err){ console.error('render error',err); }\n };\n es.onerror=()=>{\n dot.classList.add('disconnected');\n connText.textContent='reconnecting\\u2026';\n };\n }\n\n connect();\n})();\n</script>\n</body>\n</html>`;\n}\n"],"mappings":";;;;;;;;;;;;AAOA,SAAS,oBAA4E;;;ACC9E,SAAS,mBAA2B;AACzC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core/dashboard.ts","../src/core/dashboard-html.ts"],"sourcesContent":["/**\n * Quantum Status Dashboard: local HTTP server with SSE live updates.\n *\n * Collects a full snapshot of all quantum state every few seconds and\n * pushes it to connected browsers. Never exposes secret values.\n */\n\nimport { createServer, type IncomingMessage, type ServerResponse, type Server } from \"node:http\";\nimport { listSecrets } from \"./keyring.js\";\nimport { checkDecay, type DecayStatus, type QuantumEnvelope } from \"./envelope.js\";\nimport { listEntanglements, type EntanglementPair } from \"./entanglement.js\";\nimport { tunnelList } from \"./tunnel.js\";\nimport { queryAudit, detectAnomalies, type AuditEvent, type AccessAnomaly } from \"./observer.js\";\nimport { collapseEnvironment, type CollapseResult } from \"./collapse.js\";\nimport { getDashboardHtml } from \"./dashboard-html.js\";\n\nexport interface SecretSnapshot {\n key: string;\n scope: string;\n type: \"collapsed\" | \"superposition\";\n environments?: string[];\n defaultEnv?: string;\n decay: DecayStatus;\n accessCount: number;\n lastAccessedAt?: string;\n createdAt: string;\n updatedAt: string;\n description?: string;\n tags?: string[];\n entangled?: { service: string; key: string }[];\n}\n\nexport interface TunnelSnapshot {\n id: string;\n createdAt: number;\n expiresAt?: number;\n accessCount: number;\n maxReads?: number;\n}\n\nexport interface DashboardSnapshot {\n timestamp: string;\n secrets: SecretSnapshot[];\n health: { healthy: number; stale: number; expired: number; noDecay: number; total: number };\n entanglements: EntanglementPair[];\n tunnels: TunnelSnapshot[];\n audit: AuditEvent[];\n anomalies: AccessAnomaly[];\n environment: CollapseResult | null;\n}\n\nfunction toSecretSnapshot(entry: {\n key: string;\n scope: string;\n envelope?: QuantumEnvelope;\n decay?: DecayStatus;\n}): SecretSnapshot {\n const envelope = entry.envelope;\n const decay = envelope ? checkDecay(envelope) : {\n isExpired: false,\n isStale: false,\n lifetimePercent: 0,\n secondsRemaining: null,\n timeRemaining: null,\n };\n\n return {\n key: entry.key,\n scope: entry.scope,\n type: envelope?.states ? \"superposition\" : \"collapsed\",\n environments: envelope?.states ? Object.keys(envelope.states) : undefined,\n defaultEnv: envelope?.defaultEnv,\n decay,\n accessCount: envelope?.meta.accessCount ?? 0,\n lastAccessedAt: envelope?.meta.lastAccessedAt,\n createdAt: envelope?.meta.createdAt ?? \"\",\n updatedAt: envelope?.meta.updatedAt ?? \"\",\n description: envelope?.meta.description,\n tags: envelope?.meta.tags,\n entangled: envelope?.meta.entangled,\n };\n}\n\nexport function collectSnapshot(): DashboardSnapshot {\n const entries = listSecrets({ source: \"api\", silent: true });\n\n const secrets = entries.map(toSecretSnapshot);\n\n let healthy = 0;\n let stale = 0;\n let expired = 0;\n let noDecay = 0;\n\n for (const s of secrets) {\n if (!s.decay.timeRemaining) {\n noDecay++;\n } else if (s.decay.isExpired) {\n expired++;\n } else if (s.decay.isStale) {\n stale++;\n } else {\n healthy++;\n }\n }\n\n return {\n timestamp: new Date().toISOString(),\n secrets,\n health: { healthy, stale, expired, noDecay, total: secrets.length },\n entanglements: listEntanglements(),\n tunnels: tunnelList(),\n audit: queryAudit({ limit: 50 }).filter(e => e.action !== \"list\"),\n anomalies: detectAnomalies(),\n environment: collapseEnvironment(),\n };\n}\n\nexport interface DashboardServerOptions {\n port?: number;\n}\n\nexport function startDashboardServer(\n options: DashboardServerOptions = {},\n): { port: number; close: () => void; server: Server } {\n const port = options.port ?? 9876;\n const clients = new Set<ServerResponse>();\n let intervalHandle: ReturnType<typeof setInterval> | null = null;\n\n const html = getDashboardHtml();\n\n function broadcast() {\n const snapshot = collectSnapshot();\n const data = `data: ${JSON.stringify(snapshot)}\\n\\n`;\n for (const res of clients) {\n if (res.writableEnded || res.destroyed) {\n clients.delete(res);\n continue;\n }\n try {\n const ok = res.write(data);\n if (!ok) {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n } catch {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n }\n }\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n let pathname: string;\n try {\n ({ pathname } = new URL(req.url ?? \"/\", \"http://127.0.0.1\"));\n } catch {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Bad Request: invalid URL\");\n return;\n }\n\n if (pathname === \"/events\") {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n\n const snapshot = collectSnapshot();\n res.write(`data: ${JSON.stringify(snapshot)}\\n\\n`);\n\n clients.add(res);\n req.on(\"close\", () => clients.delete(res));\n return;\n }\n\n if (pathname === \"/api/status\") {\n const snapshot = collectSnapshot();\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n });\n res.end(JSON.stringify(snapshot, null, 2));\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(html);\n });\n\n server.listen(port, \"127.0.0.1\", () => {\n intervalHandle = setInterval(broadcast, 5000);\n if (intervalHandle && typeof intervalHandle === \"object\" && \"unref\" in intervalHandle) {\n intervalHandle.unref();\n }\n });\n\n return {\n port,\n close: () => {\n if (intervalHandle) clearInterval(intervalHandle);\n for (const res of clients) {\n try { res.end(); } catch { /* noop */ }\n }\n clients.clear();\n server.close();\n },\n server,\n };\n}\n","/**\n * Self-contained HTML dashboard for q-ring quantum status.\n * Matches the gh-pages site design system: Outfit + JetBrains Mono fonts,\n * deep navy palette, neon cyan→violet SVG icons, glassmorphism cards.\n *\n * Zero dependencies — inline CSS + vanilla JS with EventSource for SSE.\n */\n\nexport function getDashboardHtml(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\"/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n<title>q-ring — quantum status</title>\n<style>\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg-deep:#04080f;\n --bg-section:#080e18;\n --bg-card:#0f1a2e;\n --bg-card-hover:#142240;\n --border:#1a2d4a;\n --border-glow:#0ea5e9;\n\n --text-primary:#f8f8ff;\n --text-secondary:#c8cfe0;\n --text-dim:#8899b4;\n\n --accent:#0ea5e9;\n --accent-bright:#38bdf8;\n --accent-dim:rgba(14,165,233,0.15);\n --accent-glow:rgba(14,165,233,0.4);\n\n --danger:#ff5e5b;\n --warning:#fbbf24;\n --green:#22c55e;\n --violet:#a855f7;\n --pink:#ff0055;\n\n --font-display:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;\n --font-mono:'SF Mono','Cascadia Code','Fira Code',Consolas,'Liberation Mono',monospace;\n\n --radius:12px;\n --radius-sm:8px;\n --radius-lg:20px;\n\n --glass-bg:rgba(15,26,46,0.65);\n --glass-border:rgba(26,45,74,0.8);\n --shadow-card:0 4px 24px -1px rgba(0,0,0,0.4),0 0 1px rgba(255,255,255,0.04);\n --shadow-glow:0 0 20px rgba(14,165,233,0.12),0 0 40px rgba(168,85,247,0.08);\n}\nhtml{background:var(--bg-deep);color:var(--text-primary);font-family:var(--font-display);font-size:16px;line-height:1.5}\nbody{min-height:100vh;overflow-x:hidden;position:relative}\n\n/* Hidden SVG defs for gradient icons */\n.svg-defs{position:absolute;width:0;height:0;overflow:hidden}\n\n/* Mesh blobs */\n.blob{position:fixed;border-radius:50%;filter:blur(100px);opacity:.12;pointer-events:none;z-index:0}\n.blob-1{width:600px;height:600px;top:-120px;left:-100px;background:radial-gradient(circle,var(--accent),transparent 70%);animation:drift1 22s ease-in-out infinite}\n.blob-2{width:500px;height:500px;bottom:-80px;right:-60px;background:radial-gradient(circle,var(--violet),transparent 70%);animation:drift2 26s ease-in-out infinite}\n.blob-3{width:350px;height:350px;top:40%;left:50%;background:radial-gradient(circle,var(--green),transparent 70%);animation:drift3 30s ease-in-out infinite;opacity:.06}\n@keyframes drift1{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(60px,-40px) scale(1.08)}66%{transform:translate(-30px,50px) scale(.95)}}\n@keyframes drift2{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(-50px,30px) scale(1.05)}66%{transform:translate(40px,-60px) scale(.92)}}\n@keyframes drift3{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-40%,-60%) scale(1.1)}}\n\n.container{position:relative;z-index:1;max-width:1280px;margin:0 auto;padding:24px 20px 48px}\n\n/* Header */\n.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;padding:16px 20px;background:rgba(4,8,15,0.75);backdrop-filter:blur(16px) saturate(1.2);-webkit-backdrop-filter:blur(16px) saturate(1.2);border:1px solid var(--border);border-radius:var(--radius)}\n.header h1{font-family:var(--font-display);font-size:1.65rem;font-weight:700;letter-spacing:-.02em;display:flex;align-items:center;gap:10px}\n.header h1 .q-icon{display:flex;filter:drop-shadow(0 0 6px rgba(14,165,233,0.6))}\n.header h1 .brand{background:linear-gradient(135deg,#00D1FF,var(--violet));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}\n.header h1 .sub{color:var(--text-dim);font-weight:400;font-size:1.1rem;-webkit-text-fill-color:var(--text-dim)}\n.status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);display:inline-block;animation:pulse 2s ease-in-out infinite}\n.status-dot.disconnected{background:var(--danger);box-shadow:0 0 8px var(--danger)}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}\n.conn-label{font-size:.85rem;color:var(--text-dim);display:flex;align-items:center;gap:6px;font-family:var(--font-mono)}\n\n/* Grid */\n.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:16px}\n.grid-wide{grid-column:1/-1}\n\n/* Cards — reveal animation */\n.card{background:var(--glass-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--glass-border);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow-card);transition:border-color .3s,box-shadow .3s,transform .3s;opacity:0;transform:translateY(16px);animation:cardReveal .5s cubic-bezier(0.16,1,0.3,1) forwards}\n.card:hover{border-color:var(--border-glow);box-shadow:var(--shadow-card),var(--shadow-glow);transform:translateY(-2px)}\n@keyframes cardReveal{to{opacity:1;transform:translateY(0)}}\n\n.card-title{font-family:var(--font-display);font-size:.8rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);margin-bottom:14px;display:flex;align-items:center;gap:8px;font-weight:600}\n.card-title svg{width:16px;height:16px;flex-shrink:0;filter:drop-shadow(0 0 4px rgba(14,165,233,0.4))}\n\n/* Health donut */\n.health-row{display:flex;align-items:center;gap:24px}\n.donut-wrap{position:relative;width:100px;height:100px;flex-shrink:0}\n.donut-wrap svg{transform:rotate(-90deg)}\n.donut-wrap .donut-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;font-size:1.5rem;font-weight:700;line-height:1;font-family:var(--font-display)}\n.donut-wrap .donut-label small{font-size:.7rem;color:var(--text-dim);font-weight:400;margin-top:2px;letter-spacing:.04em}\n.health-legend{display:flex;flex-direction:column;gap:6px}\n.legend-item{display:flex;align-items:center;gap:8px;font-size:.88rem}\n.legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n\n/* Decay bars */\n.decay-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.decay-item{display:flex;align-items:center;gap:10px}\n.decay-key{font-family:var(--font-mono);font-size:.85rem;min-width:120px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.decay-bar{flex:1;height:6px;border-radius:3px;background:rgba(255,255,255,0.06);overflow:hidden;position:relative}\n.decay-fill{height:100%;border-radius:3px;transition:width .6s ease}\n.decay-time{font-size:.8rem;color:var(--text-dim);min-width:56px;text-align:right;font-family:var(--font-mono)}\n\n/* Superposition pills */\n.super-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.super-item{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.super-key{font-family:var(--font-mono);font-size:.85rem;min-width:100px}\n.env-pill{font-size:.75rem;padding:2px 8px;border-radius:99px;font-weight:600;letter-spacing:.03em;font-family:var(--font-mono)}\n.env-prod{background:rgba(255,0,85,0.2);color:var(--pink);border:1px solid rgba(255,0,85,0.3)}\n.env-staging{background:rgba(251,191,36,0.15);color:var(--warning);border:1px solid rgba(251,191,36,0.25)}\n.env-dev{background:rgba(34,197,94,0.15);color:var(--green);border:1px solid rgba(34,197,94,0.25)}\n.env-default{background:rgba(168,85,247,0.15);color:var(--violet);border:1px solid rgba(168,85,247,0.25)}\n\n/* Entanglement */\n.entangle-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow-y:auto}\n.entangle-pair{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.85rem}\n.entangle-arrow{color:var(--accent-bright)}\n\n/* Tunnels */\n.tunnel-list{display:flex;flex-direction:column;gap:8px;max-height:240px;overflow-y:auto}\n.tunnel-card{background:rgba(168,85,247,0.06);border:1px solid rgba(168,85,247,0.15);border-radius:var(--radius-sm);padding:10px 12px;font-size:.85rem;font-family:var(--font-mono)}\n.tunnel-meta{display:flex;gap:12px;margin-top:4px;font-size:.8rem;color:var(--text-dim)}\n\n/* Audit feed */\n.audit-feed{display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto;font-size:.85rem;font-family:var(--font-mono)}\n.audit-row{display:flex;gap:8px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.03)}\n.audit-ts{color:var(--text-dim);min-width:70px;flex-shrink:0}\n.audit-action{min-width:64px;font-weight:600}\n.audit-action.read{color:var(--accent)}.audit-action.write{color:var(--green)}.audit-action.delete{color:var(--danger)}\n.audit-action.entangle{color:var(--violet)}.audit-action.tunnel{color:var(--violet)}.audit-action.teleport{color:var(--warning)}\n.audit-action.generate{color:var(--warning)}.audit-action.list{color:var(--text-dim)}.audit-action.export{color:var(--text-dim)}\n.audit-action.collapse{color:var(--accent)}\n\n/* Anomalies */\n.anomaly-card{background:rgba(255,94,91,0.06);border:1px solid rgba(255,94,91,0.15);border-radius:var(--radius-sm);padding:10px 14px;animation:anomaly-pulse 3s ease-in-out infinite}\n@keyframes anomaly-pulse{0%,100%{border-color:rgba(255,94,91,0.15)}50%{border-color:rgba(255,94,91,0.4)}}\n.anomaly-type{font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--danger);font-weight:700;margin-bottom:2px;font-family:var(--font-display)}\n.anomaly-desc{font-size:.88rem;color:var(--text-primary)}\n\n/* Environment badge */\n.env-status{display:flex;align-items:center;gap:12px}\n.env-big{font-size:1.1rem;font-weight:700;padding:4px 14px;border-radius:var(--radius-sm)}\n.env-source{font-size:.85rem;color:var(--text-dim)}\n\n/* Empty states */\n.empty{color:var(--text-dim);font-size:.88rem;font-style:italic;padding:8px 0}\n\n/* Scrollbar */\n::-webkit-scrollbar{width:4px;height:4px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);border-radius:2px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.15)}\n</style>\n</head>\n<body>\n\n<!-- Shared SVG gradient definition -->\n<svg class=\"svg-defs\" aria-hidden=\"true\" focusable=\"false\">\n <defs>\n <linearGradient id=\"neon-grad\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\n <stop offset=\"0%\" stop-color=\"#00D1FF\"/>\n <stop offset=\"100%\" stop-color=\"#a855f7\"/>\n </linearGradient>\n </defs>\n</svg>\n\n<div class=\"blob blob-1\"></div>\n<div class=\"blob blob-2\"></div>\n<div class=\"blob blob-3\"></div>\n<div class=\"container\">\n <div class=\"header\">\n <h1>\n <span class=\"q-icon\"><svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg></span>\n <span class=\"brand\">q-ring</span>\n <span class=\"sub\">quantum status</span>\n </h1>\n <div class=\"conn-label\"><span class=\"status-dot\" id=\"connDot\"></span><span id=\"connText\">connecting\\u2026</span></div>\n </div>\n <div class=\"grid\" id=\"dashboard\">\n <div class=\"card\"><div class=\"empty\">Connecting to q-ring\\u2026</div></div>\n </div>\n</div>\n\n<script>\n(function(){\n const $ = (id) => document.getElementById(id);\n const dash = $('dashboard');\n const dot = $('connDot');\n const connText = $('connText');\n\n /* --- SVG icon library (matching gh-pages neon gradient) --- */\n const icons = {\n health: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/></svg>',\n environment: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/></svg>',\n decay: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 22h14\"/><path d=\"M5 2h14\"/><path d=\"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22\"/><path d=\"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2\"/></svg>',\n superposition: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 2 7 12 12 22 7 12 2\"/><polyline points=\"2 17 12 22 22 17\"/><polyline points=\"2 12 12 17 22 12\"/></svg>',\n entangle: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>',\n tunnel: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 10h.01\"/><path d=\"M15 10h.01\"/><path d=\"M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z\"/></svg>',\n anomaly: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/></svg>',\n audit: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg>'\n };\n\n function esc(s){ const d=document.createElement('div');d.textContent=s;return d.innerHTML; }\n\n function envClass(e){\n if(e==='prod'||e==='production') return 'env-prod';\n if(e==='staging'||e==='stage') return 'env-staging';\n if(e==='dev'||e==='development') return 'env-dev';\n return 'env-default';\n }\n\n function decayColor(pct,expired){\n if(expired) return 'var(--danger)';\n if(pct>=90) return 'var(--danger)';\n if(pct>=75) return 'var(--warning)';\n return 'var(--accent)';\n }\n\n function fmtTime(ts){\n const d=new Date(ts);\n return d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')+':'+d.getSeconds().toString().padStart(2,'0');\n }\n\n function renderHealth(h){\n const total=h.total||1;\n const r=42, circ=2*Math.PI*r;\n const slices=[\n {v:h.healthy,c:'var(--accent)'},\n {v:h.stale,c:'var(--warning)'},\n {v:h.expired,c:'var(--danger)'},\n {v:h.noDecay,c:'var(--text-dim)'}\n ];\n let offset=0;\n let rings='';\n for(const sl of slices){\n const pct=sl.v/total;\n const len=pct*circ;\n rings+=\\`<circle cx=\"50\" cy=\"50\" r=\"\\${r}\" fill=\"none\" stroke=\"\\${sl.c}\" stroke-width=\"12\" stroke-dasharray=\"\\${len} \\${circ-len}\" stroke-dashoffset=\"-\\${offset}\" stroke-linecap=\"round\" opacity=\"0.85\"/>\\`;\n offset+=len;\n }\n return \\`<div class=\"health-row\">\n <div class=\"donut-wrap\">\n <svg viewBox=\"0 0 100 100\" width=\"100\" height=\"100\">\\${rings}</svg>\n <div class=\"donut-label\">\\${h.total}<small>secrets</small></div>\n </div>\n <div class=\"health-legend\">\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--accent)\"></span>Healthy \\${h.healthy}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--warning)\"></span>Stale \\${h.stale}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--danger)\"></span>Expired \\${h.expired}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--text-dim)\"></span>No decay \\${h.noDecay}</div>\n </div>\n </div>\\`;\n }\n\n function renderDecay(secrets){\n const withDecay=secrets.filter(s=>s.decay&&s.decay.timeRemaining);\n if(!withDecay.length) return '<div class=\"empty\">No secrets with decay configured</div>';\n return '<div class=\"decay-list\">'+withDecay.map(s=>{\n const pct=Math.min(s.decay.lifetimePercent,100);\n const col=decayColor(pct,s.decay.isExpired);\n const label=s.decay.isExpired?'expired':s.decay.timeRemaining||'';\n return \\`<div class=\"decay-item\">\n <span class=\"decay-key\" title=\"\\${esc(s.key)}\">\\${esc(s.key)}</span>\n <div class=\"decay-bar\"><div class=\"decay-fill\" style=\"width:\\${pct}%;background:\\${col}\"></div></div>\n <span class=\"decay-time\" style=\"color:\\${col}\">\\${esc(label)}</span>\n </div>\\`;\n }).join('')+'</div>';\n }\n\n function renderSuperposition(secrets){\n const sup=secrets.filter(s=>s.type==='superposition'&&s.environments);\n if(!sup.length) return '<div class=\"empty\">No secrets in superposition</div>';\n return '<div class=\"super-list\">'+sup.map(s=>{\n const pills=(s.environments||[]).map(e=>{\n const isDefault=e===s.defaultEnv;\n return \\`<span class=\"env-pill \\${envClass(e)}\">\\${esc(e)}\\${isDefault?' \\u2713':''}</span>\\`;\n }).join('');\n return \\`<div class=\"super-item\"><span class=\"super-key\">\\${esc(s.key)}</span>\\${pills}</div>\\`;\n }).join('')+'</div>';\n }\n\n function renderEntanglements(pairs){\n if(!pairs.length) return '<div class=\"empty\">No entangled secrets</div>';\n const seen=new Set();\n const unique=pairs.filter(p=>{\n const id=[p.source.service,p.source.key,p.target.service,p.target.key].sort().join('|');\n if(seen.has(id)) return false;\n seen.add(id);return true;\n });\n return '<div class=\"entangle-list\">'+unique.map(p=>\n \\`<div class=\"entangle-pair\"><span>\\${esc(p.source.key)}</span><span class=\"entangle-arrow\">\\u2194</span><span>\\${esc(p.target.key)}</span></div>\\`\n ).join('')+'</div>';\n }\n\n function renderTunnels(tunnels){\n if(!tunnels.length) return '<div class=\"empty\">No active tunnels</div>';\n return '<div class=\"tunnel-list\">'+tunnels.map(t=>{\n const rem=t.expiresAt?Math.max(0,Math.floor((t.expiresAt-Date.now())/1000)):null;\n return \\`<div class=\"tunnel-card\">\\${esc(t.id)}<div class=\"tunnel-meta\">\n <span>reads: \\${t.accessCount}\\${t.maxReads?'/'+t.maxReads:''}</span>\n \\${rem!==null?'<span>expires: '+rem+'s</span>':'<span>no expiry</span>'}\n </div></div>\\`;\n }).join('')+'</div>';\n }\n\n function renderAudit(events){\n if(!events.length) return '<div class=\"empty\">No audit events</div>';\n return '<div class=\"audit-feed\">'+events.slice(0,40).map(e=>\n \\`<div class=\"audit-row\">\n <span class=\"audit-ts\">\\${fmtTime(e.timestamp)}</span>\n <span class=\"audit-action \\${esc(e.action)}\">\\${esc(e.action)}</span>\n <span class=\"audit-key\">\\${e.key?esc(e.key):''}</span>\n <span class=\"audit-detail\">\\${e.detail?esc(e.detail):''}</span>\n </div>\\`\n ).join('')+'</div>';\n }\n\n function renderAnomalies(anomalies){\n if(!anomalies.length) return '<div class=\"empty\">No anomalies detected \\u2014 all clear</div>';\n return anomalies.map(a=>\n \\`<div class=\"anomaly-card\"><div class=\"anomaly-type\">\\${esc(a.type)}</div><div class=\"anomaly-desc\">\\${esc(a.description)}</div></div>\\`\n ).join('');\n }\n\n function renderEnvironment(env){\n if(!env) return '<div class=\"empty\">No environment detected</div>';\n return \\`<div class=\"env-status\">\n <span class=\"env-big env-pill \\${envClass(env.env)}\">\\${esc(env.env)}</span>\n <span class=\"env-source\">detected via \\${esc(env.source)}</span>\n </div>\\`;\n }\n\n const panels = [\n { id:'p-health', icon:icons.health, label:'Health Summary', wide:false, render:s=>renderHealth(s.health) },\n { id:'p-env', icon:icons.environment, label:'Environment', wide:false, render:s=>renderEnvironment(s.environment) },\n { id:'p-decay', icon:icons.decay, label:'Decay Timers', wide:false, render:s=>renderDecay(s.secrets) },\n { id:'p-super', icon:icons.superposition, label:'Superposition States', wide:false, render:s=>renderSuperposition(s.secrets) },\n { id:'p-ent', icon:icons.entangle, label:'Entanglement', wide:false, render:s=>renderEntanglements(s.entanglements) },\n { id:'p-tunnel', icon:icons.tunnel, label:'Quantum Tunnels', wide:false, render:s=>renderTunnels(s.tunnels) },\n { id:'p-anomaly', icon:icons.anomaly, label:'Anomaly Alerts', wide:true, render:s=>renderAnomalies(s.anomalies) },\n { id:'p-audit', icon:icons.audit, label:'Audit Log', wide:true, render:s=>renderAudit(s.audit) }\n ];\n\n let initialised = false;\n\n function render(snap){\n if(!initialised){\n initialised = true;\n dash.innerHTML = panels.map((p,i) =>\n \\`<div class=\"card\\${p.wide?' grid-wide':''}\" id=\"\\${p.id}\" style=\"animation-delay:\\${i*60}ms\">\n <div class=\"card-title\">\\${p.icon} \\${p.label}</div>\n <div class=\"card-body\" id=\"\\${p.id}-body\"></div>\n </div>\\`\n ).join('');\n }\n for(const p of panels){\n const body = $(\\`\\${p.id}-body\\`);\n if(body){\n const html = p.render(snap);\n if(body._prev !== html){ body.innerHTML = html; body._prev = html; }\n }\n }\n }\n\n function connect(){\n const es=new EventSource('/events');\n es.onopen=()=>{\n dot.classList.remove('disconnected');\n connText.textContent='live';\n };\n es.onmessage=(e)=>{\n try{ render(JSON.parse(e.data)); }catch(err){ console.error('render error',err); }\n };\n es.onerror=()=>{\n dot.classList.add('disconnected');\n connText.textContent='reconnecting\\u2026';\n };\n }\n\n connect();\n})();\n</script>\n</body>\n</html>`;\n}\n"],"mappings":";;;;;;;;;;;;AAOA,SAAS,oBAA4E;;;ACC9E,SAAS,mBAA2B;AACzC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","names":[]}
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
listSecrets,
|
|
8
8
|
queryAudit,
|
|
9
9
|
tunnelList
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-JV4JFWV6.js";
|
|
11
11
|
|
|
12
12
|
// src/core/dashboard.ts
|
|
13
13
|
import { createServer } from "http";
|
|
@@ -322,7 +322,7 @@ body{min-height:100vh;overflow-x:hidden;position:relative}
|
|
|
322
322
|
return '<div class="audit-feed">'+events.slice(0,40).map(e=>
|
|
323
323
|
\`<div class="audit-row">
|
|
324
324
|
<span class="audit-ts">\${fmtTime(e.timestamp)}</span>
|
|
325
|
-
<span class="audit-action \${e.action}">\${e.action}</span>
|
|
325
|
+
<span class="audit-action \${esc(e.action)}">\${esc(e.action)}</span>
|
|
326
326
|
<span class="audit-key">\${e.key?esc(e.key):''}</span>
|
|
327
327
|
<span class="audit-detail">\${e.detail?esc(e.detail):''}</span>
|
|
328
328
|
</div>\`
|
|
@@ -554,4 +554,4 @@ export {
|
|
|
554
554
|
collectSnapshot,
|
|
555
555
|
startDashboardServer
|
|
556
556
|
};
|
|
557
|
-
//# sourceMappingURL=dashboard-
|
|
557
|
+
//# sourceMappingURL=dashboard-5L4ILERJ.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/dashboard.ts","../src/core/dashboard-html.ts"],"sourcesContent":["/**\n * Quantum Status Dashboard: local HTTP server with SSE live updates.\n *\n * Collects a full snapshot of all quantum state every few seconds and\n * pushes it to connected browsers. Never exposes secret values.\n */\n\nimport { createServer, type IncomingMessage, type ServerResponse, type Server } from \"node:http\";\nimport { listSecrets } from \"./keyring.js\";\nimport { checkDecay, type DecayStatus, type QuantumEnvelope } from \"./envelope.js\";\nimport { listEntanglements, type EntanglementPair } from \"./entanglement.js\";\nimport { tunnelList } from \"./tunnel.js\";\nimport { queryAudit, detectAnomalies, type AuditEvent, type AccessAnomaly } from \"./observer.js\";\nimport { collapseEnvironment, type CollapseResult } from \"./collapse.js\";\nimport { getDashboardHtml } from \"./dashboard-html.js\";\n\nexport interface SecretSnapshot {\n key: string;\n scope: string;\n type: \"collapsed\" | \"superposition\";\n environments?: string[];\n defaultEnv?: string;\n decay: DecayStatus;\n accessCount: number;\n lastAccessedAt?: string;\n createdAt: string;\n updatedAt: string;\n description?: string;\n tags?: string[];\n entangled?: { service: string; key: string }[];\n}\n\nexport interface TunnelSnapshot {\n id: string;\n createdAt: number;\n expiresAt?: number;\n accessCount: number;\n maxReads?: number;\n}\n\nexport interface DashboardSnapshot {\n timestamp: string;\n secrets: SecretSnapshot[];\n health: { healthy: number; stale: number; expired: number; noDecay: number; total: number };\n entanglements: EntanglementPair[];\n tunnels: TunnelSnapshot[];\n audit: AuditEvent[];\n anomalies: AccessAnomaly[];\n environment: CollapseResult | null;\n}\n\nfunction toSecretSnapshot(entry: {\n key: string;\n scope: string;\n envelope?: QuantumEnvelope;\n decay?: DecayStatus;\n}): SecretSnapshot {\n const envelope = entry.envelope;\n const decay = envelope ? checkDecay(envelope) : {\n isExpired: false,\n isStale: false,\n lifetimePercent: 0,\n secondsRemaining: null,\n timeRemaining: null,\n };\n\n return {\n key: entry.key,\n scope: entry.scope,\n type: envelope?.states ? \"superposition\" : \"collapsed\",\n environments: envelope?.states ? Object.keys(envelope.states) : undefined,\n defaultEnv: envelope?.defaultEnv,\n decay,\n accessCount: envelope?.meta.accessCount ?? 0,\n lastAccessedAt: envelope?.meta.lastAccessedAt,\n createdAt: envelope?.meta.createdAt ?? \"\",\n updatedAt: envelope?.meta.updatedAt ?? \"\",\n description: envelope?.meta.description,\n tags: envelope?.meta.tags,\n entangled: envelope?.meta.entangled,\n };\n}\n\nexport function collectSnapshot(): DashboardSnapshot {\n const entries = listSecrets({ source: \"api\", silent: true });\n\n const secrets = entries.map(toSecretSnapshot);\n\n let healthy = 0;\n let stale = 0;\n let expired = 0;\n let noDecay = 0;\n\n for (const s of secrets) {\n if (!s.decay.timeRemaining) {\n noDecay++;\n } else if (s.decay.isExpired) {\n expired++;\n } else if (s.decay.isStale) {\n stale++;\n } else {\n healthy++;\n }\n }\n\n return {\n timestamp: new Date().toISOString(),\n secrets,\n health: { healthy, stale, expired, noDecay, total: secrets.length },\n entanglements: listEntanglements(),\n tunnels: tunnelList(),\n audit: queryAudit({ limit: 50 }).filter(e => e.action !== \"list\"),\n anomalies: detectAnomalies(),\n environment: collapseEnvironment(),\n };\n}\n\nexport interface DashboardServerOptions {\n port?: number;\n}\n\nexport function startDashboardServer(\n options: DashboardServerOptions = {},\n): { port: number; close: () => void; server: Server } {\n const port = options.port ?? 9876;\n const clients = new Set<ServerResponse>();\n let intervalHandle: ReturnType<typeof setInterval> | null = null;\n\n const html = getDashboardHtml();\n\n function broadcast() {\n const snapshot = collectSnapshot();\n const data = `data: ${JSON.stringify(snapshot)}\\n\\n`;\n for (const res of clients) {\n if (res.writableEnded || res.destroyed) {\n clients.delete(res);\n continue;\n }\n try {\n const ok = res.write(data);\n if (!ok) {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n } catch {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n }\n }\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n let pathname: string;\n try {\n ({ pathname } = new URL(req.url ?? \"/\", \"http://127.0.0.1\"));\n } catch {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Bad Request: invalid URL\");\n return;\n }\n\n if (pathname === \"/events\") {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n\n const snapshot = collectSnapshot();\n res.write(`data: ${JSON.stringify(snapshot)}\\n\\n`);\n\n clients.add(res);\n req.on(\"close\", () => clients.delete(res));\n return;\n }\n\n if (pathname === \"/api/status\") {\n const snapshot = collectSnapshot();\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n });\n res.end(JSON.stringify(snapshot, null, 2));\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(html);\n });\n\n server.listen(port, \"127.0.0.1\", () => {\n intervalHandle = setInterval(broadcast, 5000);\n if (intervalHandle && typeof intervalHandle === \"object\" && \"unref\" in intervalHandle) {\n intervalHandle.unref();\n }\n });\n\n return {\n port,\n close: () => {\n if (intervalHandle) clearInterval(intervalHandle);\n for (const res of clients) {\n try { res.end(); } catch { /* noop */ }\n }\n clients.clear();\n server.close();\n },\n server,\n };\n}\n","/**\n * Self-contained HTML dashboard for q-ring quantum status.\n * Matches the gh-pages site design system: Outfit + JetBrains Mono fonts,\n * deep navy palette, neon cyan→violet SVG icons, glassmorphism cards.\n *\n * Zero dependencies — inline CSS + vanilla JS with EventSource for SSE.\n */\n\nexport function getDashboardHtml(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\"/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n<title>q-ring — quantum status</title>\n<style>\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg-deep:#04080f;\n --bg-section:#080e18;\n --bg-card:#0f1a2e;\n --bg-card-hover:#142240;\n --border:#1a2d4a;\n --border-glow:#0ea5e9;\n\n --text-primary:#f8f8ff;\n --text-secondary:#c8cfe0;\n --text-dim:#8899b4;\n\n --accent:#0ea5e9;\n --accent-bright:#38bdf8;\n --accent-dim:rgba(14,165,233,0.15);\n --accent-glow:rgba(14,165,233,0.4);\n\n --danger:#ff5e5b;\n --warning:#fbbf24;\n --green:#22c55e;\n --violet:#a855f7;\n --pink:#ff0055;\n\n --font-display:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;\n --font-mono:'SF Mono','Cascadia Code','Fira Code',Consolas,'Liberation Mono',monospace;\n\n --radius:12px;\n --radius-sm:8px;\n --radius-lg:20px;\n\n --glass-bg:rgba(15,26,46,0.65);\n --glass-border:rgba(26,45,74,0.8);\n --shadow-card:0 4px 24px -1px rgba(0,0,0,0.4),0 0 1px rgba(255,255,255,0.04);\n --shadow-glow:0 0 20px rgba(14,165,233,0.12),0 0 40px rgba(168,85,247,0.08);\n}\nhtml{background:var(--bg-deep);color:var(--text-primary);font-family:var(--font-display);font-size:16px;line-height:1.5}\nbody{min-height:100vh;overflow-x:hidden;position:relative}\n\n/* Hidden SVG defs for gradient icons */\n.svg-defs{position:absolute;width:0;height:0;overflow:hidden}\n\n/* Mesh blobs */\n.blob{position:fixed;border-radius:50%;filter:blur(100px);opacity:.12;pointer-events:none;z-index:0}\n.blob-1{width:600px;height:600px;top:-120px;left:-100px;background:radial-gradient(circle,var(--accent),transparent 70%);animation:drift1 22s ease-in-out infinite}\n.blob-2{width:500px;height:500px;bottom:-80px;right:-60px;background:radial-gradient(circle,var(--violet),transparent 70%);animation:drift2 26s ease-in-out infinite}\n.blob-3{width:350px;height:350px;top:40%;left:50%;background:radial-gradient(circle,var(--green),transparent 70%);animation:drift3 30s ease-in-out infinite;opacity:.06}\n@keyframes drift1{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(60px,-40px) scale(1.08)}66%{transform:translate(-30px,50px) scale(.95)}}\n@keyframes drift2{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(-50px,30px) scale(1.05)}66%{transform:translate(40px,-60px) scale(.92)}}\n@keyframes drift3{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-40%,-60%) scale(1.1)}}\n\n.container{position:relative;z-index:1;max-width:1280px;margin:0 auto;padding:24px 20px 48px}\n\n/* Header */\n.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;padding:16px 20px;background:rgba(4,8,15,0.75);backdrop-filter:blur(16px) saturate(1.2);-webkit-backdrop-filter:blur(16px) saturate(1.2);border:1px solid var(--border);border-radius:var(--radius)}\n.header h1{font-family:var(--font-display);font-size:1.65rem;font-weight:700;letter-spacing:-.02em;display:flex;align-items:center;gap:10px}\n.header h1 .q-icon{display:flex;filter:drop-shadow(0 0 6px rgba(14,165,233,0.6))}\n.header h1 .brand{background:linear-gradient(135deg,#00D1FF,var(--violet));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}\n.header h1 .sub{color:var(--text-dim);font-weight:400;font-size:1.1rem;-webkit-text-fill-color:var(--text-dim)}\n.status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);display:inline-block;animation:pulse 2s ease-in-out infinite}\n.status-dot.disconnected{background:var(--danger);box-shadow:0 0 8px var(--danger)}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}\n.conn-label{font-size:.85rem;color:var(--text-dim);display:flex;align-items:center;gap:6px;font-family:var(--font-mono)}\n\n/* Grid */\n.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:16px}\n.grid-wide{grid-column:1/-1}\n\n/* Cards — reveal animation */\n.card{background:var(--glass-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--glass-border);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow-card);transition:border-color .3s,box-shadow .3s,transform .3s;opacity:0;transform:translateY(16px);animation:cardReveal .5s cubic-bezier(0.16,1,0.3,1) forwards}\n.card:hover{border-color:var(--border-glow);box-shadow:var(--shadow-card),var(--shadow-glow);transform:translateY(-2px)}\n@keyframes cardReveal{to{opacity:1;transform:translateY(0)}}\n\n.card-title{font-family:var(--font-display);font-size:.8rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);margin-bottom:14px;display:flex;align-items:center;gap:8px;font-weight:600}\n.card-title svg{width:16px;height:16px;flex-shrink:0;filter:drop-shadow(0 0 4px rgba(14,165,233,0.4))}\n\n/* Health donut */\n.health-row{display:flex;align-items:center;gap:24px}\n.donut-wrap{position:relative;width:100px;height:100px;flex-shrink:0}\n.donut-wrap svg{transform:rotate(-90deg)}\n.donut-wrap .donut-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;font-size:1.5rem;font-weight:700;line-height:1;font-family:var(--font-display)}\n.donut-wrap .donut-label small{font-size:.7rem;color:var(--text-dim);font-weight:400;margin-top:2px;letter-spacing:.04em}\n.health-legend{display:flex;flex-direction:column;gap:6px}\n.legend-item{display:flex;align-items:center;gap:8px;font-size:.88rem}\n.legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n\n/* Decay bars */\n.decay-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.decay-item{display:flex;align-items:center;gap:10px}\n.decay-key{font-family:var(--font-mono);font-size:.85rem;min-width:120px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.decay-bar{flex:1;height:6px;border-radius:3px;background:rgba(255,255,255,0.06);overflow:hidden;position:relative}\n.decay-fill{height:100%;border-radius:3px;transition:width .6s ease}\n.decay-time{font-size:.8rem;color:var(--text-dim);min-width:56px;text-align:right;font-family:var(--font-mono)}\n\n/* Superposition pills */\n.super-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.super-item{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.super-key{font-family:var(--font-mono);font-size:.85rem;min-width:100px}\n.env-pill{font-size:.75rem;padding:2px 8px;border-radius:99px;font-weight:600;letter-spacing:.03em;font-family:var(--font-mono)}\n.env-prod{background:rgba(255,0,85,0.2);color:var(--pink);border:1px solid rgba(255,0,85,0.3)}\n.env-staging{background:rgba(251,191,36,0.15);color:var(--warning);border:1px solid rgba(251,191,36,0.25)}\n.env-dev{background:rgba(34,197,94,0.15);color:var(--green);border:1px solid rgba(34,197,94,0.25)}\n.env-default{background:rgba(168,85,247,0.15);color:var(--violet);border:1px solid rgba(168,85,247,0.25)}\n\n/* Entanglement */\n.entangle-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow-y:auto}\n.entangle-pair{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.85rem}\n.entangle-arrow{color:var(--accent-bright)}\n\n/* Tunnels */\n.tunnel-list{display:flex;flex-direction:column;gap:8px;max-height:240px;overflow-y:auto}\n.tunnel-card{background:rgba(168,85,247,0.06);border:1px solid rgba(168,85,247,0.15);border-radius:var(--radius-sm);padding:10px 12px;font-size:.85rem;font-family:var(--font-mono)}\n.tunnel-meta{display:flex;gap:12px;margin-top:4px;font-size:.8rem;color:var(--text-dim)}\n\n/* Audit feed */\n.audit-feed{display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto;font-size:.85rem;font-family:var(--font-mono)}\n.audit-row{display:flex;gap:8px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.03)}\n.audit-ts{color:var(--text-dim);min-width:70px;flex-shrink:0}\n.audit-action{min-width:64px;font-weight:600}\n.audit-action.read{color:var(--accent)}.audit-action.write{color:var(--green)}.audit-action.delete{color:var(--danger)}\n.audit-action.entangle{color:var(--violet)}.audit-action.tunnel{color:var(--violet)}.audit-action.teleport{color:var(--warning)}\n.audit-action.generate{color:var(--warning)}.audit-action.list{color:var(--text-dim)}.audit-action.export{color:var(--text-dim)}\n.audit-action.collapse{color:var(--accent)}\n\n/* Anomalies */\n.anomaly-card{background:rgba(255,94,91,0.06);border:1px solid rgba(255,94,91,0.15);border-radius:var(--radius-sm);padding:10px 14px;animation:anomaly-pulse 3s ease-in-out infinite}\n@keyframes anomaly-pulse{0%,100%{border-color:rgba(255,94,91,0.15)}50%{border-color:rgba(255,94,91,0.4)}}\n.anomaly-type{font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--danger);font-weight:700;margin-bottom:2px;font-family:var(--font-display)}\n.anomaly-desc{font-size:.88rem;color:var(--text-primary)}\n\n/* Environment badge */\n.env-status{display:flex;align-items:center;gap:12px}\n.env-big{font-size:1.1rem;font-weight:700;padding:4px 14px;border-radius:var(--radius-sm)}\n.env-source{font-size:.85rem;color:var(--text-dim)}\n\n/* Empty states */\n.empty{color:var(--text-dim);font-size:.88rem;font-style:italic;padding:8px 0}\n\n/* Scrollbar */\n::-webkit-scrollbar{width:4px;height:4px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);border-radius:2px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.15)}\n</style>\n</head>\n<body>\n\n<!-- Shared SVG gradient definition -->\n<svg class=\"svg-defs\" aria-hidden=\"true\" focusable=\"false\">\n <defs>\n <linearGradient id=\"neon-grad\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\n <stop offset=\"0%\" stop-color=\"#00D1FF\"/>\n <stop offset=\"100%\" stop-color=\"#a855f7\"/>\n </linearGradient>\n </defs>\n</svg>\n\n<div class=\"blob blob-1\"></div>\n<div class=\"blob blob-2\"></div>\n<div class=\"blob blob-3\"></div>\n<div class=\"container\">\n <div class=\"header\">\n <h1>\n <span class=\"q-icon\"><svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg></span>\n <span class=\"brand\">q-ring</span>\n <span class=\"sub\">quantum status</span>\n </h1>\n <div class=\"conn-label\"><span class=\"status-dot\" id=\"connDot\"></span><span id=\"connText\">connecting\\u2026</span></div>\n </div>\n <div class=\"grid\" id=\"dashboard\">\n <div class=\"card\"><div class=\"empty\">Connecting to q-ring\\u2026</div></div>\n </div>\n</div>\n\n<script>\n(function(){\n const $ = (id) => document.getElementById(id);\n const dash = $('dashboard');\n const dot = $('connDot');\n const connText = $('connText');\n\n /* --- SVG icon library (matching gh-pages neon gradient) --- */\n const icons = {\n health: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/></svg>',\n environment: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/></svg>',\n decay: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 22h14\"/><path d=\"M5 2h14\"/><path d=\"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22\"/><path d=\"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2\"/></svg>',\n superposition: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 2 7 12 12 22 7 12 2\"/><polyline points=\"2 17 12 22 22 17\"/><polyline points=\"2 12 12 17 22 12\"/></svg>',\n entangle: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>',\n tunnel: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 10h.01\"/><path d=\"M15 10h.01\"/><path d=\"M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z\"/></svg>',\n anomaly: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/></svg>',\n audit: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg>'\n };\n\n function esc(s){ const d=document.createElement('div');d.textContent=s;return d.innerHTML; }\n\n function envClass(e){\n if(e==='prod'||e==='production') return 'env-prod';\n if(e==='staging'||e==='stage') return 'env-staging';\n if(e==='dev'||e==='development') return 'env-dev';\n return 'env-default';\n }\n\n function decayColor(pct,expired){\n if(expired) return 'var(--danger)';\n if(pct>=90) return 'var(--danger)';\n if(pct>=75) return 'var(--warning)';\n return 'var(--accent)';\n }\n\n function fmtTime(ts){\n const d=new Date(ts);\n return d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')+':'+d.getSeconds().toString().padStart(2,'0');\n }\n\n function renderHealth(h){\n const total=h.total||1;\n const r=42, circ=2*Math.PI*r;\n const slices=[\n {v:h.healthy,c:'var(--accent)'},\n {v:h.stale,c:'var(--warning)'},\n {v:h.expired,c:'var(--danger)'},\n {v:h.noDecay,c:'var(--text-dim)'}\n ];\n let offset=0;\n let rings='';\n for(const sl of slices){\n const pct=sl.v/total;\n const len=pct*circ;\n rings+=\\`<circle cx=\"50\" cy=\"50\" r=\"\\${r}\" fill=\"none\" stroke=\"\\${sl.c}\" stroke-width=\"12\" stroke-dasharray=\"\\${len} \\${circ-len}\" stroke-dashoffset=\"-\\${offset}\" stroke-linecap=\"round\" opacity=\"0.85\"/>\\`;\n offset+=len;\n }\n return \\`<div class=\"health-row\">\n <div class=\"donut-wrap\">\n <svg viewBox=\"0 0 100 100\" width=\"100\" height=\"100\">\\${rings}</svg>\n <div class=\"donut-label\">\\${h.total}<small>secrets</small></div>\n </div>\n <div class=\"health-legend\">\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--accent)\"></span>Healthy \\${h.healthy}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--warning)\"></span>Stale \\${h.stale}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--danger)\"></span>Expired \\${h.expired}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--text-dim)\"></span>No decay \\${h.noDecay}</div>\n </div>\n </div>\\`;\n }\n\n function renderDecay(secrets){\n const withDecay=secrets.filter(s=>s.decay&&s.decay.timeRemaining);\n if(!withDecay.length) return '<div class=\"empty\">No secrets with decay configured</div>';\n return '<div class=\"decay-list\">'+withDecay.map(s=>{\n const pct=Math.min(s.decay.lifetimePercent,100);\n const col=decayColor(pct,s.decay.isExpired);\n const label=s.decay.isExpired?'expired':s.decay.timeRemaining||'';\n return \\`<div class=\"decay-item\">\n <span class=\"decay-key\" title=\"\\${esc(s.key)}\">\\${esc(s.key)}</span>\n <div class=\"decay-bar\"><div class=\"decay-fill\" style=\"width:\\${pct}%;background:\\${col}\"></div></div>\n <span class=\"decay-time\" style=\"color:\\${col}\">\\${esc(label)}</span>\n </div>\\`;\n }).join('')+'</div>';\n }\n\n function renderSuperposition(secrets){\n const sup=secrets.filter(s=>s.type==='superposition'&&s.environments);\n if(!sup.length) return '<div class=\"empty\">No secrets in superposition</div>';\n return '<div class=\"super-list\">'+sup.map(s=>{\n const pills=(s.environments||[]).map(e=>{\n const isDefault=e===s.defaultEnv;\n return \\`<span class=\"env-pill \\${envClass(e)}\">\\${esc(e)}\\${isDefault?' \\u2713':''}</span>\\`;\n }).join('');\n return \\`<div class=\"super-item\"><span class=\"super-key\">\\${esc(s.key)}</span>\\${pills}</div>\\`;\n }).join('')+'</div>';\n }\n\n function renderEntanglements(pairs){\n if(!pairs.length) return '<div class=\"empty\">No entangled secrets</div>';\n const seen=new Set();\n const unique=pairs.filter(p=>{\n const id=[p.source.service,p.source.key,p.target.service,p.target.key].sort().join('|');\n if(seen.has(id)) return false;\n seen.add(id);return true;\n });\n return '<div class=\"entangle-list\">'+unique.map(p=>\n \\`<div class=\"entangle-pair\"><span>\\${esc(p.source.key)}</span><span class=\"entangle-arrow\">\\u2194</span><span>\\${esc(p.target.key)}</span></div>\\`\n ).join('')+'</div>';\n }\n\n function renderTunnels(tunnels){\n if(!tunnels.length) return '<div class=\"empty\">No active tunnels</div>';\n return '<div class=\"tunnel-list\">'+tunnels.map(t=>{\n const rem=t.expiresAt?Math.max(0,Math.floor((t.expiresAt-Date.now())/1000)):null;\n return \\`<div class=\"tunnel-card\">\\${esc(t.id)}<div class=\"tunnel-meta\">\n <span>reads: \\${t.accessCount}\\${t.maxReads?'/'+t.maxReads:''}</span>\n \\${rem!==null?'<span>expires: '+rem+'s</span>':'<span>no expiry</span>'}\n </div></div>\\`;\n }).join('')+'</div>';\n }\n\n function renderAudit(events){\n if(!events.length) return '<div class=\"empty\">No audit events</div>';\n return '<div class=\"audit-feed\">'+events.slice(0,40).map(e=>\n \\`<div class=\"audit-row\">\n <span class=\"audit-ts\">\\${fmtTime(e.timestamp)}</span>\n <span class=\"audit-action \\${e.action}\">\\${e.action}</span>\n <span class=\"audit-key\">\\${e.key?esc(e.key):''}</span>\n <span class=\"audit-detail\">\\${e.detail?esc(e.detail):''}</span>\n </div>\\`\n ).join('')+'</div>';\n }\n\n function renderAnomalies(anomalies){\n if(!anomalies.length) return '<div class=\"empty\">No anomalies detected \\u2014 all clear</div>';\n return anomalies.map(a=>\n \\`<div class=\"anomaly-card\"><div class=\"anomaly-type\">\\${esc(a.type)}</div><div class=\"anomaly-desc\">\\${esc(a.description)}</div></div>\\`\n ).join('');\n }\n\n function renderEnvironment(env){\n if(!env) return '<div class=\"empty\">No environment detected</div>';\n return \\`<div class=\"env-status\">\n <span class=\"env-big env-pill \\${envClass(env.env)}\">\\${esc(env.env)}</span>\n <span class=\"env-source\">detected via \\${esc(env.source)}</span>\n </div>\\`;\n }\n\n const panels = [\n { id:'p-health', icon:icons.health, label:'Health Summary', wide:false, render:s=>renderHealth(s.health) },\n { id:'p-env', icon:icons.environment, label:'Environment', wide:false, render:s=>renderEnvironment(s.environment) },\n { id:'p-decay', icon:icons.decay, label:'Decay Timers', wide:false, render:s=>renderDecay(s.secrets) },\n { id:'p-super', icon:icons.superposition, label:'Superposition States', wide:false, render:s=>renderSuperposition(s.secrets) },\n { id:'p-ent', icon:icons.entangle, label:'Entanglement', wide:false, render:s=>renderEntanglements(s.entanglements) },\n { id:'p-tunnel', icon:icons.tunnel, label:'Quantum Tunnels', wide:false, render:s=>renderTunnels(s.tunnels) },\n { id:'p-anomaly', icon:icons.anomaly, label:'Anomaly Alerts', wide:true, render:s=>renderAnomalies(s.anomalies) },\n { id:'p-audit', icon:icons.audit, label:'Audit Log', wide:true, render:s=>renderAudit(s.audit) }\n ];\n\n let initialised = false;\n\n function render(snap){\n if(!initialised){\n initialised = true;\n dash.innerHTML = panels.map((p,i) =>\n \\`<div class=\"card\\${p.wide?' grid-wide':''}\" id=\"\\${p.id}\" style=\"animation-delay:\\${i*60}ms\">\n <div class=\"card-title\">\\${p.icon} \\${p.label}</div>\n <div class=\"card-body\" id=\"\\${p.id}-body\"></div>\n </div>\\`\n ).join('');\n }\n for(const p of panels){\n const body = $(\\`\\${p.id}-body\\`);\n if(body){\n const html = p.render(snap);\n if(body._prev !== html){ body.innerHTML = html; body._prev = html; }\n }\n }\n }\n\n function connect(){\n const es=new EventSource('/events');\n es.onopen=()=>{\n dot.classList.remove('disconnected');\n connText.textContent='live';\n };\n es.onmessage=(e)=>{\n try{ render(JSON.parse(e.data)); }catch(err){ console.error('render error',err); }\n };\n es.onerror=()=>{\n dot.classList.add('disconnected');\n connText.textContent='reconnecting\\u2026';\n };\n }\n\n connect();\n})();\n</script>\n</body>\n</html>`;\n}\n"],"mappings":";;;;;;;;;;;;AAOA,SAAS,oBAA4E;;;ACC9E,SAAS,mBAA2B;AACzC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA8XT;;;ADpVA,SAAS,iBAAiB,OAKP;AACjB,QAAM,WAAW,MAAM;AACvB,QAAM,QAAQ,WAAW,WAAW,QAAQ,IAAI;AAAA,IAC9C,WAAW;AAAA,IACX,SAAS;AAAA,IACT,iBAAiB;AAAA,IACjB,kBAAkB;AAAA,IAClB,eAAe;AAAA,EACjB;AAEA,SAAO;AAAA,IACL,KAAK,MAAM;AAAA,IACX,OAAO,MAAM;AAAA,IACb,MAAM,UAAU,SAAS,kBAAkB;AAAA,IAC3C,cAAc,UAAU,SAAS,OAAO,KAAK,SAAS,MAAM,IAAI;AAAA,IAChE,YAAY,UAAU;AAAA,IACtB;AAAA,IACA,aAAa,UAAU,KAAK,eAAe;AAAA,IAC3C,gBAAgB,UAAU,KAAK;AAAA,IAC/B,WAAW,UAAU,KAAK,aAAa;AAAA,IACvC,WAAW,UAAU,KAAK,aAAa;AAAA,IACvC,aAAa,UAAU,KAAK;AAAA,IAC5B,MAAM,UAAU,KAAK;AAAA,IACrB,WAAW,UAAU,KAAK;AAAA,EAC5B;AACF;AAEO,SAAS,kBAAqC;AACnD,QAAM,UAAU,YAAY,EAAE,QAAQ,OAAO,QAAQ,KAAK,CAAC;AAE3D,QAAM,UAAU,QAAQ,IAAI,gBAAgB;AAE5C,MAAI,UAAU;AACd,MAAI,QAAQ;AACZ,MAAI,UAAU;AACd,MAAI,UAAU;AAEd,aAAW,KAAK,SAAS;AACvB,QAAI,CAAC,EAAE,MAAM,eAAe;AAC1B;AAAA,IACF,WAAW,EAAE,MAAM,WAAW;AAC5B;AAAA,IACF,WAAW,EAAE,MAAM,SAAS;AAC1B;AAAA,IACF,OAAO;AACL;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC;AAAA,IACA,QAAQ,EAAE,SAAS,OAAO,SAAS,SAAS,OAAO,QAAQ,OAAO;AAAA,IAClE,eAAe,kBAAkB;AAAA,IACjC,SAAS,WAAW;AAAA,IACpB,OAAO,WAAW,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,OAAK,EAAE,WAAW,MAAM;AAAA,IAChE,WAAW,gBAAgB;AAAA,IAC3B,aAAa,oBAAoB;AAAA,EACnC;AACF;AAMO,SAAS,qBACd,UAAkC,CAAC,GACkB;AACrD,QAAM,OAAO,QAAQ,QAAQ;AAC7B,QAAM,UAAU,oBAAI,IAAoB;AACxC,MAAI,iBAAwD;AAE5D,QAAM,OAAO,iBAAiB;AAE9B,WAAS,YAAY;AACnB,UAAM,WAAW,gBAAgB;AACjC,UAAM,OAAO,SAAS,KAAK,UAAU,QAAQ,CAAC;AAAA;AAAA;AAC9C,eAAW,OAAO,SAAS;AACzB,UAAI,IAAI,iBAAiB,IAAI,WAAW;AACtC,gBAAQ,OAAO,GAAG;AAClB;AAAA,MACF;AACA,UAAI;AACF,cAAM,KAAK,IAAI,MAAM,IAAI;AACzB,YAAI,CAAC,IAAI;AACP,kBAAQ,OAAO,GAAG;AAClB,cAAI;AAAE,gBAAI,IAAI;AAAA,UAAG,QAAQ;AAAE,gBAAI;AAAE,kBAAI,QAAQ;AAAA,YAAG,QAAQ;AAAA,YAAa;AAAA,UAAE;AAAA,QACzE;AAAA,MACF,QAAQ;AACN,gBAAQ,OAAO,GAAG;AAClB,YAAI;AAAE,cAAI,IAAI;AAAA,QAAG,QAAQ;AAAE,cAAI;AAAE,gBAAI,QAAQ;AAAA,UAAG,QAAQ;AAAA,UAAa;AAAA,QAAE;AAAA,MACzE;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,aAAa,CAAC,KAAsB,QAAwB;AACzE,QAAI;AACJ,QAAI;AACF,OAAC,EAAE,SAAS,IAAI,IAAI,IAAI,IAAI,OAAO,KAAK,kBAAkB;AAAA,IAC5D,QAAQ;AACN,UAAI,UAAU,KAAK,EAAE,gBAAgB,4BAA4B,CAAC;AAClE,UAAI,IAAI,0BAA0B;AAClC;AAAA,IACF;AAEA,QAAI,aAAa,WAAW;AAC1B,UAAI,UAAU,KAAK;AAAA,QACjB,gBAAgB;AAAA,QAChB,iBAAiB;AAAA,QACjB,YAAY;AAAA,MACd,CAAC;AAED,YAAM,WAAW,gBAAgB;AACjC,UAAI,MAAM,SAAS,KAAK,UAAU,QAAQ,CAAC;AAAA;AAAA,CAAM;AAEjD,cAAQ,IAAI,GAAG;AACf,UAAI,GAAG,SAAS,MAAM,QAAQ,OAAO,GAAG,CAAC;AACzC;AAAA,IACF;AAEA,QAAI,aAAa,eAAe;AAC9B,YAAM,WAAW,gBAAgB;AACjC,UAAI,UAAU,KAAK;AAAA,QACjB,gBAAgB;AAAA,MAClB,CAAC;AACD,UAAI,IAAI,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AACzC;AAAA,IACF;AAEA,QAAI,UAAU,KAAK,EAAE,gBAAgB,2BAA2B,CAAC;AACjE,QAAI,IAAI,IAAI;AAAA,EACd,CAAC;AAED,SAAO,OAAO,MAAM,aAAa,MAAM;AACrC,qBAAiB,YAAY,WAAW,GAAI;AAC5C,QAAI,kBAAkB,OAAO,mBAAmB,YAAY,WAAW,gBAAgB;AACrF,qBAAe,MAAM;AAAA,IACvB;AAAA,EACF,CAAC;AAED,SAAO;AAAA,IACL;AAAA,IACA,OAAO,MAAM;AACX,UAAI,eAAgB,eAAc,cAAc;AAChD,iBAAW,OAAO,SAAS;AACzB,YAAI;AAAE,cAAI,IAAI;AAAA,QAAG,QAAQ;AAAA,QAAa;AAAA,MACxC;AACA,cAAQ,MAAM;AACd,aAAO,MAAM;AAAA,IACf;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core/dashboard.ts","../src/core/dashboard-html.ts"],"sourcesContent":["/**\n * Quantum Status Dashboard: local HTTP server with SSE live updates.\n *\n * Collects a full snapshot of all quantum state every few seconds and\n * pushes it to connected browsers. Never exposes secret values.\n */\n\nimport { createServer, type IncomingMessage, type ServerResponse, type Server } from \"node:http\";\nimport { listSecrets } from \"./keyring.js\";\nimport { checkDecay, type DecayStatus, type QuantumEnvelope } from \"./envelope.js\";\nimport { listEntanglements, type EntanglementPair } from \"./entanglement.js\";\nimport { tunnelList } from \"./tunnel.js\";\nimport { queryAudit, detectAnomalies, type AuditEvent, type AccessAnomaly } from \"./observer.js\";\nimport { collapseEnvironment, type CollapseResult } from \"./collapse.js\";\nimport { getDashboardHtml } from \"./dashboard-html.js\";\n\nexport interface SecretSnapshot {\n key: string;\n scope: string;\n type: \"collapsed\" | \"superposition\";\n environments?: string[];\n defaultEnv?: string;\n decay: DecayStatus;\n accessCount: number;\n lastAccessedAt?: string;\n createdAt: string;\n updatedAt: string;\n description?: string;\n tags?: string[];\n entangled?: { service: string; key: string }[];\n}\n\nexport interface TunnelSnapshot {\n id: string;\n createdAt: number;\n expiresAt?: number;\n accessCount: number;\n maxReads?: number;\n}\n\nexport interface DashboardSnapshot {\n timestamp: string;\n secrets: SecretSnapshot[];\n health: { healthy: number; stale: number; expired: number; noDecay: number; total: number };\n entanglements: EntanglementPair[];\n tunnels: TunnelSnapshot[];\n audit: AuditEvent[];\n anomalies: AccessAnomaly[];\n environment: CollapseResult | null;\n}\n\nfunction toSecretSnapshot(entry: {\n key: string;\n scope: string;\n envelope?: QuantumEnvelope;\n decay?: DecayStatus;\n}): SecretSnapshot {\n const envelope = entry.envelope;\n const decay = envelope ? checkDecay(envelope) : {\n isExpired: false,\n isStale: false,\n lifetimePercent: 0,\n secondsRemaining: null,\n timeRemaining: null,\n };\n\n return {\n key: entry.key,\n scope: entry.scope,\n type: envelope?.states ? \"superposition\" : \"collapsed\",\n environments: envelope?.states ? Object.keys(envelope.states) : undefined,\n defaultEnv: envelope?.defaultEnv,\n decay,\n accessCount: envelope?.meta.accessCount ?? 0,\n lastAccessedAt: envelope?.meta.lastAccessedAt,\n createdAt: envelope?.meta.createdAt ?? \"\",\n updatedAt: envelope?.meta.updatedAt ?? \"\",\n description: envelope?.meta.description,\n tags: envelope?.meta.tags,\n entangled: envelope?.meta.entangled,\n };\n}\n\nexport function collectSnapshot(): DashboardSnapshot {\n const entries = listSecrets({ source: \"api\", silent: true });\n\n const secrets = entries.map(toSecretSnapshot);\n\n let healthy = 0;\n let stale = 0;\n let expired = 0;\n let noDecay = 0;\n\n for (const s of secrets) {\n if (!s.decay.timeRemaining) {\n noDecay++;\n } else if (s.decay.isExpired) {\n expired++;\n } else if (s.decay.isStale) {\n stale++;\n } else {\n healthy++;\n }\n }\n\n return {\n timestamp: new Date().toISOString(),\n secrets,\n health: { healthy, stale, expired, noDecay, total: secrets.length },\n entanglements: listEntanglements(),\n tunnels: tunnelList(),\n audit: queryAudit({ limit: 50 }).filter(e => e.action !== \"list\"),\n anomalies: detectAnomalies(),\n environment: collapseEnvironment(),\n };\n}\n\nexport interface DashboardServerOptions {\n port?: number;\n}\n\nexport function startDashboardServer(\n options: DashboardServerOptions = {},\n): { port: number; close: () => void; server: Server } {\n const port = options.port ?? 9876;\n const clients = new Set<ServerResponse>();\n let intervalHandle: ReturnType<typeof setInterval> | null = null;\n\n const html = getDashboardHtml();\n\n function broadcast() {\n const snapshot = collectSnapshot();\n const data = `data: ${JSON.stringify(snapshot)}\\n\\n`;\n for (const res of clients) {\n if (res.writableEnded || res.destroyed) {\n clients.delete(res);\n continue;\n }\n try {\n const ok = res.write(data);\n if (!ok) {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n } catch {\n clients.delete(res);\n try { res.end(); } catch { try { res.destroy(); } catch { /* noop */ } }\n }\n }\n }\n\n const server = createServer((req: IncomingMessage, res: ServerResponse) => {\n let pathname: string;\n try {\n ({ pathname } = new URL(req.url ?? \"/\", \"http://127.0.0.1\"));\n } catch {\n res.writeHead(400, { \"Content-Type\": \"text/plain; charset=utf-8\" });\n res.end(\"Bad Request: invalid URL\");\n return;\n }\n\n if (pathname === \"/events\") {\n res.writeHead(200, {\n \"Content-Type\": \"text/event-stream\",\n \"Cache-Control\": \"no-cache\",\n Connection: \"keep-alive\",\n });\n\n const snapshot = collectSnapshot();\n res.write(`data: ${JSON.stringify(snapshot)}\\n\\n`);\n\n clients.add(res);\n req.on(\"close\", () => clients.delete(res));\n return;\n }\n\n if (pathname === \"/api/status\") {\n const snapshot = collectSnapshot();\n res.writeHead(200, {\n \"Content-Type\": \"application/json\",\n });\n res.end(JSON.stringify(snapshot, null, 2));\n return;\n }\n\n res.writeHead(200, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(html);\n });\n\n server.listen(port, \"127.0.0.1\", () => {\n intervalHandle = setInterval(broadcast, 5000);\n if (intervalHandle && typeof intervalHandle === \"object\" && \"unref\" in intervalHandle) {\n intervalHandle.unref();\n }\n });\n\n return {\n port,\n close: () => {\n if (intervalHandle) clearInterval(intervalHandle);\n for (const res of clients) {\n try { res.end(); } catch { /* noop */ }\n }\n clients.clear();\n server.close();\n },\n server,\n };\n}\n","/**\n * Self-contained HTML dashboard for q-ring quantum status.\n * Matches the gh-pages site design system: Outfit + JetBrains Mono fonts,\n * deep navy palette, neon cyan→violet SVG icons, glassmorphism cards.\n *\n * Zero dependencies — inline CSS + vanilla JS with EventSource for SSE.\n */\n\nexport function getDashboardHtml(): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\"/>\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/>\n<title>q-ring — quantum status</title>\n<style>\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n:root{\n --bg-deep:#04080f;\n --bg-section:#080e18;\n --bg-card:#0f1a2e;\n --bg-card-hover:#142240;\n --border:#1a2d4a;\n --border-glow:#0ea5e9;\n\n --text-primary:#f8f8ff;\n --text-secondary:#c8cfe0;\n --text-dim:#8899b4;\n\n --accent:#0ea5e9;\n --accent-bright:#38bdf8;\n --accent-dim:rgba(14,165,233,0.15);\n --accent-glow:rgba(14,165,233,0.4);\n\n --danger:#ff5e5b;\n --warning:#fbbf24;\n --green:#22c55e;\n --violet:#a855f7;\n --pink:#ff0055;\n\n --font-display:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;\n --font-mono:'SF Mono','Cascadia Code','Fira Code',Consolas,'Liberation Mono',monospace;\n\n --radius:12px;\n --radius-sm:8px;\n --radius-lg:20px;\n\n --glass-bg:rgba(15,26,46,0.65);\n --glass-border:rgba(26,45,74,0.8);\n --shadow-card:0 4px 24px -1px rgba(0,0,0,0.4),0 0 1px rgba(255,255,255,0.04);\n --shadow-glow:0 0 20px rgba(14,165,233,0.12),0 0 40px rgba(168,85,247,0.08);\n}\nhtml{background:var(--bg-deep);color:var(--text-primary);font-family:var(--font-display);font-size:16px;line-height:1.5}\nbody{min-height:100vh;overflow-x:hidden;position:relative}\n\n/* Hidden SVG defs for gradient icons */\n.svg-defs{position:absolute;width:0;height:0;overflow:hidden}\n\n/* Mesh blobs */\n.blob{position:fixed;border-radius:50%;filter:blur(100px);opacity:.12;pointer-events:none;z-index:0}\n.blob-1{width:600px;height:600px;top:-120px;left:-100px;background:radial-gradient(circle,var(--accent),transparent 70%);animation:drift1 22s ease-in-out infinite}\n.blob-2{width:500px;height:500px;bottom:-80px;right:-60px;background:radial-gradient(circle,var(--violet),transparent 70%);animation:drift2 26s ease-in-out infinite}\n.blob-3{width:350px;height:350px;top:40%;left:50%;background:radial-gradient(circle,var(--green),transparent 70%);animation:drift3 30s ease-in-out infinite;opacity:.06}\n@keyframes drift1{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(60px,-40px) scale(1.08)}66%{transform:translate(-30px,50px) scale(.95)}}\n@keyframes drift2{0%,100%{transform:translate(0,0) scale(1)}33%{transform:translate(-50px,30px) scale(1.05)}66%{transform:translate(40px,-60px) scale(.92)}}\n@keyframes drift3{0%,100%{transform:translate(-50%,-50%) scale(1)}50%{transform:translate(-40%,-60%) scale(1.1)}}\n\n.container{position:relative;z-index:1;max-width:1280px;margin:0 auto;padding:24px 20px 48px}\n\n/* Header */\n.header{display:flex;align-items:center;justify-content:space-between;margin-bottom:28px;padding:16px 20px;background:rgba(4,8,15,0.75);backdrop-filter:blur(16px) saturate(1.2);-webkit-backdrop-filter:blur(16px) saturate(1.2);border:1px solid var(--border);border-radius:var(--radius)}\n.header h1{font-family:var(--font-display);font-size:1.65rem;font-weight:700;letter-spacing:-.02em;display:flex;align-items:center;gap:10px}\n.header h1 .q-icon{display:flex;filter:drop-shadow(0 0 6px rgba(14,165,233,0.6))}\n.header h1 .brand{background:linear-gradient(135deg,#00D1FF,var(--violet));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}\n.header h1 .sub{color:var(--text-dim);font-weight:400;font-size:1.1rem;-webkit-text-fill-color:var(--text-dim)}\n.status-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 8px var(--green);display:inline-block;animation:pulse 2s ease-in-out infinite}\n.status-dot.disconnected{background:var(--danger);box-shadow:0 0 8px var(--danger)}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}\n.conn-label{font-size:.85rem;color:var(--text-dim);display:flex;align-items:center;gap:6px;font-family:var(--font-mono)}\n\n/* Grid */\n.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:16px}\n.grid-wide{grid-column:1/-1}\n\n/* Cards — reveal animation */\n.card{background:var(--glass-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--glass-border);border-radius:var(--radius);padding:18px 20px;box-shadow:var(--shadow-card);transition:border-color .3s,box-shadow .3s,transform .3s;opacity:0;transform:translateY(16px);animation:cardReveal .5s cubic-bezier(0.16,1,0.3,1) forwards}\n.card:hover{border-color:var(--border-glow);box-shadow:var(--shadow-card),var(--shadow-glow);transform:translateY(-2px)}\n@keyframes cardReveal{to{opacity:1;transform:translateY(0)}}\n\n.card-title{font-family:var(--font-display);font-size:.8rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);margin-bottom:14px;display:flex;align-items:center;gap:8px;font-weight:600}\n.card-title svg{width:16px;height:16px;flex-shrink:0;filter:drop-shadow(0 0 4px rgba(14,165,233,0.4))}\n\n/* Health donut */\n.health-row{display:flex;align-items:center;gap:24px}\n.donut-wrap{position:relative;width:100px;height:100px;flex-shrink:0}\n.donut-wrap svg{transform:rotate(-90deg)}\n.donut-wrap .donut-label{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;flex-direction:column;font-size:1.5rem;font-weight:700;line-height:1;font-family:var(--font-display)}\n.donut-wrap .donut-label small{font-size:.7rem;color:var(--text-dim);font-weight:400;margin-top:2px;letter-spacing:.04em}\n.health-legend{display:flex;flex-direction:column;gap:6px}\n.legend-item{display:flex;align-items:center;gap:8px;font-size:.88rem}\n.legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n\n/* Decay bars */\n.decay-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.decay-item{display:flex;align-items:center;gap:10px}\n.decay-key{font-family:var(--font-mono);font-size:.85rem;min-width:120px;max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.decay-bar{flex:1;height:6px;border-radius:3px;background:rgba(255,255,255,0.06);overflow:hidden;position:relative}\n.decay-fill{height:100%;border-radius:3px;transition:width .6s ease}\n.decay-time{font-size:.8rem;color:var(--text-dim);min-width:56px;text-align:right;font-family:var(--font-mono)}\n\n/* Superposition pills */\n.super-list{display:flex;flex-direction:column;gap:8px;max-height:280px;overflow-y:auto}\n.super-item{display:flex;align-items:center;gap:8px;flex-wrap:wrap}\n.super-key{font-family:var(--font-mono);font-size:.85rem;min-width:100px}\n.env-pill{font-size:.75rem;padding:2px 8px;border-radius:99px;font-weight:600;letter-spacing:.03em;font-family:var(--font-mono)}\n.env-prod{background:rgba(255,0,85,0.2);color:var(--pink);border:1px solid rgba(255,0,85,0.3)}\n.env-staging{background:rgba(251,191,36,0.15);color:var(--warning);border:1px solid rgba(251,191,36,0.25)}\n.env-dev{background:rgba(34,197,94,0.15);color:var(--green);border:1px solid rgba(34,197,94,0.25)}\n.env-default{background:rgba(168,85,247,0.15);color:var(--violet);border:1px solid rgba(168,85,247,0.25)}\n\n/* Entanglement */\n.entangle-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow-y:auto}\n.entangle-pair{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.85rem}\n.entangle-arrow{color:var(--accent-bright)}\n\n/* Tunnels */\n.tunnel-list{display:flex;flex-direction:column;gap:8px;max-height:240px;overflow-y:auto}\n.tunnel-card{background:rgba(168,85,247,0.06);border:1px solid rgba(168,85,247,0.15);border-radius:var(--radius-sm);padding:10px 12px;font-size:.85rem;font-family:var(--font-mono)}\n.tunnel-meta{display:flex;gap:12px;margin-top:4px;font-size:.8rem;color:var(--text-dim)}\n\n/* Audit feed */\n.audit-feed{display:flex;flex-direction:column;gap:4px;max-height:300px;overflow-y:auto;font-size:.85rem;font-family:var(--font-mono)}\n.audit-row{display:flex;gap:8px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.03)}\n.audit-ts{color:var(--text-dim);min-width:70px;flex-shrink:0}\n.audit-action{min-width:64px;font-weight:600}\n.audit-action.read{color:var(--accent)}.audit-action.write{color:var(--green)}.audit-action.delete{color:var(--danger)}\n.audit-action.entangle{color:var(--violet)}.audit-action.tunnel{color:var(--violet)}.audit-action.teleport{color:var(--warning)}\n.audit-action.generate{color:var(--warning)}.audit-action.list{color:var(--text-dim)}.audit-action.export{color:var(--text-dim)}\n.audit-action.collapse{color:var(--accent)}\n\n/* Anomalies */\n.anomaly-card{background:rgba(255,94,91,0.06);border:1px solid rgba(255,94,91,0.15);border-radius:var(--radius-sm);padding:10px 14px;animation:anomaly-pulse 3s ease-in-out infinite}\n@keyframes anomaly-pulse{0%,100%{border-color:rgba(255,94,91,0.15)}50%{border-color:rgba(255,94,91,0.4)}}\n.anomaly-type{font-size:.8rem;text-transform:uppercase;letter-spacing:.06em;color:var(--danger);font-weight:700;margin-bottom:2px;font-family:var(--font-display)}\n.anomaly-desc{font-size:.88rem;color:var(--text-primary)}\n\n/* Environment badge */\n.env-status{display:flex;align-items:center;gap:12px}\n.env-big{font-size:1.1rem;font-weight:700;padding:4px 14px;border-radius:var(--radius-sm)}\n.env-source{font-size:.85rem;color:var(--text-dim)}\n\n/* Empty states */\n.empty{color:var(--text-dim);font-size:.88rem;font-style:italic;padding:8px 0}\n\n/* Scrollbar */\n::-webkit-scrollbar{width:4px;height:4px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);border-radius:2px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.15)}\n</style>\n</head>\n<body>\n\n<!-- Shared SVG gradient definition -->\n<svg class=\"svg-defs\" aria-hidden=\"true\" focusable=\"false\">\n <defs>\n <linearGradient id=\"neon-grad\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\n <stop offset=\"0%\" stop-color=\"#00D1FF\"/>\n <stop offset=\"100%\" stop-color=\"#a855f7\"/>\n </linearGradient>\n </defs>\n</svg>\n\n<div class=\"blob blob-1\"></div>\n<div class=\"blob blob-2\"></div>\n<div class=\"blob blob-3\"></div>\n<div class=\"container\">\n <div class=\"header\">\n <h1>\n <span class=\"q-icon\"><svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg></span>\n <span class=\"brand\">q-ring</span>\n <span class=\"sub\">quantum status</span>\n </h1>\n <div class=\"conn-label\"><span class=\"status-dot\" id=\"connDot\"></span><span id=\"connText\">connecting\\u2026</span></div>\n </div>\n <div class=\"grid\" id=\"dashboard\">\n <div class=\"card\"><div class=\"empty\">Connecting to q-ring\\u2026</div></div>\n </div>\n</div>\n\n<script>\n(function(){\n const $ = (id) => document.getElementById(id);\n const dash = $('dashboard');\n const dot = $('connDot');\n const connText = $('connText');\n\n /* --- SVG icon library (matching gh-pages neon gradient) --- */\n const icons = {\n health: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/></svg>',\n environment: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/></svg>',\n decay: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M5 22h14\"/><path d=\"M5 2h14\"/><path d=\"M17 22v-4.172a2 2 0 0 0-.586-1.414L12 12l-4.414 4.414A2 2 0 0 0 7 17.828V22\"/><path d=\"M7 2v4.172a2 2 0 0 0 .586 1.414L12 12l4.414-4.414A2 2 0 0 0 17 6.172V2\"/></svg>',\n superposition: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"12 2 2 7 12 12 22 7 12 2\"/><polyline points=\"2 17 12 22 22 17\"/><polyline points=\"2 12 12 17 22 12\"/></svg>',\n entangle: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/></svg>',\n tunnel: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 10h.01\"/><path d=\"M15 10h.01\"/><path d=\"M12 2a8 8 0 0 0-8 8v12l3-3 2.5 2.5L12 19l2.5 2.5L17 19l3 3V10a8 8 0 0 0-8-8z\"/></svg>',\n anomaly: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z\"/><line x1=\"12\" y1=\"9\" x2=\"12\" y2=\"13\"/><line x1=\"12\" y1=\"17\" x2=\"12.01\" y2=\"17\"/></svg>',\n audit: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg>'\n };\n\n function esc(s){ const d=document.createElement('div');d.textContent=s;return d.innerHTML; }\n\n function envClass(e){\n if(e==='prod'||e==='production') return 'env-prod';\n if(e==='staging'||e==='stage') return 'env-staging';\n if(e==='dev'||e==='development') return 'env-dev';\n return 'env-default';\n }\n\n function decayColor(pct,expired){\n if(expired) return 'var(--danger)';\n if(pct>=90) return 'var(--danger)';\n if(pct>=75) return 'var(--warning)';\n return 'var(--accent)';\n }\n\n function fmtTime(ts){\n const d=new Date(ts);\n return d.getHours().toString().padStart(2,'0')+':'+d.getMinutes().toString().padStart(2,'0')+':'+d.getSeconds().toString().padStart(2,'0');\n }\n\n function renderHealth(h){\n const total=h.total||1;\n const r=42, circ=2*Math.PI*r;\n const slices=[\n {v:h.healthy,c:'var(--accent)'},\n {v:h.stale,c:'var(--warning)'},\n {v:h.expired,c:'var(--danger)'},\n {v:h.noDecay,c:'var(--text-dim)'}\n ];\n let offset=0;\n let rings='';\n for(const sl of slices){\n const pct=sl.v/total;\n const len=pct*circ;\n rings+=\\`<circle cx=\"50\" cy=\"50\" r=\"\\${r}\" fill=\"none\" stroke=\"\\${sl.c}\" stroke-width=\"12\" stroke-dasharray=\"\\${len} \\${circ-len}\" stroke-dashoffset=\"-\\${offset}\" stroke-linecap=\"round\" opacity=\"0.85\"/>\\`;\n offset+=len;\n }\n return \\`<div class=\"health-row\">\n <div class=\"donut-wrap\">\n <svg viewBox=\"0 0 100 100\" width=\"100\" height=\"100\">\\${rings}</svg>\n <div class=\"donut-label\">\\${h.total}<small>secrets</small></div>\n </div>\n <div class=\"health-legend\">\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--accent)\"></span>Healthy \\${h.healthy}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--warning)\"></span>Stale \\${h.stale}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--danger)\"></span>Expired \\${h.expired}</div>\n <div class=\"legend-item\"><span class=\"legend-dot\" style=\"background:var(--text-dim)\"></span>No decay \\${h.noDecay}</div>\n </div>\n </div>\\`;\n }\n\n function renderDecay(secrets){\n const withDecay=secrets.filter(s=>s.decay&&s.decay.timeRemaining);\n if(!withDecay.length) return '<div class=\"empty\">No secrets with decay configured</div>';\n return '<div class=\"decay-list\">'+withDecay.map(s=>{\n const pct=Math.min(s.decay.lifetimePercent,100);\n const col=decayColor(pct,s.decay.isExpired);\n const label=s.decay.isExpired?'expired':s.decay.timeRemaining||'';\n return \\`<div class=\"decay-item\">\n <span class=\"decay-key\" title=\"\\${esc(s.key)}\">\\${esc(s.key)}</span>\n <div class=\"decay-bar\"><div class=\"decay-fill\" style=\"width:\\${pct}%;background:\\${col}\"></div></div>\n <span class=\"decay-time\" style=\"color:\\${col}\">\\${esc(label)}</span>\n </div>\\`;\n }).join('')+'</div>';\n }\n\n function renderSuperposition(secrets){\n const sup=secrets.filter(s=>s.type==='superposition'&&s.environments);\n if(!sup.length) return '<div class=\"empty\">No secrets in superposition</div>';\n return '<div class=\"super-list\">'+sup.map(s=>{\n const pills=(s.environments||[]).map(e=>{\n const isDefault=e===s.defaultEnv;\n return \\`<span class=\"env-pill \\${envClass(e)}\">\\${esc(e)}\\${isDefault?' \\u2713':''}</span>\\`;\n }).join('');\n return \\`<div class=\"super-item\"><span class=\"super-key\">\\${esc(s.key)}</span>\\${pills}</div>\\`;\n }).join('')+'</div>';\n }\n\n function renderEntanglements(pairs){\n if(!pairs.length) return '<div class=\"empty\">No entangled secrets</div>';\n const seen=new Set();\n const unique=pairs.filter(p=>{\n const id=[p.source.service,p.source.key,p.target.service,p.target.key].sort().join('|');\n if(seen.has(id)) return false;\n seen.add(id);return true;\n });\n return '<div class=\"entangle-list\">'+unique.map(p=>\n \\`<div class=\"entangle-pair\"><span>\\${esc(p.source.key)}</span><span class=\"entangle-arrow\">\\u2194</span><span>\\${esc(p.target.key)}</span></div>\\`\n ).join('')+'</div>';\n }\n\n function renderTunnels(tunnels){\n if(!tunnels.length) return '<div class=\"empty\">No active tunnels</div>';\n return '<div class=\"tunnel-list\">'+tunnels.map(t=>{\n const rem=t.expiresAt?Math.max(0,Math.floor((t.expiresAt-Date.now())/1000)):null;\n return \\`<div class=\"tunnel-card\">\\${esc(t.id)}<div class=\"tunnel-meta\">\n <span>reads: \\${t.accessCount}\\${t.maxReads?'/'+t.maxReads:''}</span>\n \\${rem!==null?'<span>expires: '+rem+'s</span>':'<span>no expiry</span>'}\n </div></div>\\`;\n }).join('')+'</div>';\n }\n\n function renderAudit(events){\n if(!events.length) return '<div class=\"empty\">No audit events</div>';\n return '<div class=\"audit-feed\">'+events.slice(0,40).map(e=>\n \\`<div class=\"audit-row\">\n <span class=\"audit-ts\">\\${fmtTime(e.timestamp)}</span>\n <span class=\"audit-action \\${esc(e.action)}\">\\${esc(e.action)}</span>\n <span class=\"audit-key\">\\${e.key?esc(e.key):''}</span>\n <span class=\"audit-detail\">\\${e.detail?esc(e.detail):''}</span>\n </div>\\`\n ).join('')+'</div>';\n }\n\n function renderAnomalies(anomalies){\n if(!anomalies.length) return '<div class=\"empty\">No anomalies detected \\u2014 all clear</div>';\n return anomalies.map(a=>\n \\`<div class=\"anomaly-card\"><div class=\"anomaly-type\">\\${esc(a.type)}</div><div class=\"anomaly-desc\">\\${esc(a.description)}</div></div>\\`\n ).join('');\n }\n\n function renderEnvironment(env){\n if(!env) return '<div class=\"empty\">No environment detected</div>';\n return \\`<div class=\"env-status\">\n <span class=\"env-big env-pill \\${envClass(env.env)}\">\\${esc(env.env)}</span>\n <span class=\"env-source\">detected via \\${esc(env.source)}</span>\n </div>\\`;\n }\n\n const panels = [\n { id:'p-health', icon:icons.health, label:'Health Summary', wide:false, render:s=>renderHealth(s.health) },\n { id:'p-env', icon:icons.environment, label:'Environment', wide:false, render:s=>renderEnvironment(s.environment) },\n { id:'p-decay', icon:icons.decay, label:'Decay Timers', wide:false, render:s=>renderDecay(s.secrets) },\n { id:'p-super', icon:icons.superposition, label:'Superposition States', wide:false, render:s=>renderSuperposition(s.secrets) },\n { id:'p-ent', icon:icons.entangle, label:'Entanglement', wide:false, render:s=>renderEntanglements(s.entanglements) },\n { id:'p-tunnel', icon:icons.tunnel, label:'Quantum Tunnels', wide:false, render:s=>renderTunnels(s.tunnels) },\n { id:'p-anomaly', icon:icons.anomaly, label:'Anomaly Alerts', wide:true, render:s=>renderAnomalies(s.anomalies) },\n { id:'p-audit', icon:icons.audit, label:'Audit Log', wide:true, render:s=>renderAudit(s.audit) }\n ];\n\n let initialised = false;\n\n function render(snap){\n if(!initialised){\n initialised = true;\n dash.innerHTML = panels.map((p,i) =>\n \\`<div class=\"card\\${p.wide?' grid-wide':''}\" id=\"\\${p.id}\" style=\"animation-delay:\\${i*60}ms\">\n <div class=\"card-title\">\\${p.icon} \\${p.label}</div>\n <div class=\"card-body\" id=\"\\${p.id}-body\"></div>\n </div>\\`\n ).join('');\n }\n for(const p of panels){\n const body = $(\\`\\${p.id}-body\\`);\n if(body){\n const html = p.render(snap);\n if(body._prev !== html){ body.innerHTML = html; body._prev = html; }\n }\n }\n }\n\n function connect(){\n const es=new EventSource('/events');\n es.onopen=()=>{\n dot.classList.remove('disconnected');\n connText.textContent='live';\n };\n es.onmessage=(e)=>{\n try{ render(JSON.parse(e.data)); }catch(err){ console.error('render error',err); }\n };\n es.onerror=()=>{\n dot.classList.add('disconnected');\n connText.textContent='reconnecting\\u2026';\n };\n }\n\n connect();\n})();\n</script>\n</body>\n</html>`;\n}\n"],"mappings":";;;;;;;;;;;;AAOA,SAAS,oBAA4E;;;ACC9E,SAAS,mBAA2B;AACzC,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
checkDecay,
|
|
4
4
|
checkExecPolicy,
|
|
5
|
+
checkSSRF,
|
|
5
6
|
collapseEnvironment,
|
|
6
7
|
deleteSecret,
|
|
7
8
|
detectAnomalies,
|
|
@@ -35,7 +36,7 @@ import {
|
|
|
35
36
|
tunnelList,
|
|
36
37
|
tunnelRead,
|
|
37
38
|
verifyAuditChain
|
|
38
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-JV4JFWV6.js";
|
|
39
40
|
|
|
40
41
|
// src/cli/commands.ts
|
|
41
42
|
import { Command } from "commander";
|
|
@@ -527,7 +528,10 @@ async function execCommand(opts) {
|
|
|
527
528
|
throw new Error(`Policy Denied: ${policyDecision.reason}`);
|
|
528
529
|
}
|
|
529
530
|
if (profile.denyCommands) {
|
|
530
|
-
const denied = profile.denyCommands.find((d) =>
|
|
531
|
+
const denied = profile.denyCommands.find((d) => {
|
|
532
|
+
const pattern = new RegExp(`(^|[\\s/])${d.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(\\s|$)`, "i");
|
|
533
|
+
return pattern.test(fullCommand);
|
|
534
|
+
});
|
|
531
535
|
if (denied) {
|
|
532
536
|
throw new Error(`Exec profile "${profile.name}" denies command containing "${denied}"`);
|
|
533
537
|
}
|
|
@@ -1016,6 +1020,10 @@ var httpProvider = {
|
|
|
1016
1020
|
if (!url) {
|
|
1017
1021
|
return { valid: false, status: "unknown", message: "No validation URL configured", latencyMs: 0, provider: "http" };
|
|
1018
1022
|
}
|
|
1023
|
+
const ssrfBlock = await checkSSRF(url);
|
|
1024
|
+
if (ssrfBlock) {
|
|
1025
|
+
return { valid: false, status: "error", message: `SSRF blocked: ${ssrfBlock}`, latencyMs: Date.now() - start, provider: "http" };
|
|
1026
|
+
}
|
|
1019
1027
|
try {
|
|
1020
1028
|
const { statusCode } = await makeRequest(url, {
|
|
1021
1029
|
Authorization: `Bearer ${value}`,
|
|
@@ -1160,7 +1168,10 @@ import { existsSync as existsSync2, readFileSync as readFileSync4, writeFileSync
|
|
|
1160
1168
|
import { join as join2 } from "path";
|
|
1161
1169
|
import { homedir, hostname, userInfo } from "os";
|
|
1162
1170
|
import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHash, randomBytes as randomBytes3 } from "crypto";
|
|
1171
|
+
import { Entry } from "@napi-rs/keyring";
|
|
1163
1172
|
var MEMORY_FILE = "agent-memory.enc";
|
|
1173
|
+
var KEYRING_SERVICE = "qring-memory-key";
|
|
1174
|
+
var KEYRING_ACCOUNT = "encryption-key";
|
|
1164
1175
|
function getMemoryDir() {
|
|
1165
1176
|
const dir = join2(homedir(), ".config", "q-ring");
|
|
1166
1177
|
if (!existsSync2(dir)) {
|
|
@@ -1171,29 +1182,54 @@ function getMemoryDir() {
|
|
|
1171
1182
|
function getMemoryPath() {
|
|
1172
1183
|
return join2(getMemoryDir(), MEMORY_FILE);
|
|
1173
1184
|
}
|
|
1174
|
-
function
|
|
1185
|
+
function deriveLegacyKey() {
|
|
1175
1186
|
const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
|
|
1176
1187
|
return createHash("sha256").update(fingerprint).digest();
|
|
1177
1188
|
}
|
|
1178
|
-
function
|
|
1179
|
-
|
|
1189
|
+
function getOrCreateKey() {
|
|
1190
|
+
try {
|
|
1191
|
+
const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
|
|
1192
|
+
const stored = entry.getPassword();
|
|
1193
|
+
if (stored) return Buffer.from(stored, "base64");
|
|
1194
|
+
const key = randomBytes3(32);
|
|
1195
|
+
entry.setPassword(key.toString("base64"));
|
|
1196
|
+
return key;
|
|
1197
|
+
} catch {
|
|
1198
|
+
console.warn("[q-ring] OS keyring unavailable for memory key \u2014 falling back to machine-derived key");
|
|
1199
|
+
return deriveLegacyKey();
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function encryptWith(data, key) {
|
|
1180
1203
|
const iv = randomBytes3(12);
|
|
1181
1204
|
const cipher = createCipheriv2("aes-256-gcm", key, iv);
|
|
1182
1205
|
const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
|
|
1183
1206
|
const tag = cipher.getAuthTag();
|
|
1184
1207
|
return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
|
|
1185
1208
|
}
|
|
1186
|
-
function
|
|
1209
|
+
function decryptWith(blob, key) {
|
|
1187
1210
|
const parts = blob.split(":");
|
|
1188
1211
|
if (parts.length !== 3) throw new Error("Invalid encrypted format");
|
|
1189
1212
|
const iv = Buffer.from(parts[0], "base64");
|
|
1190
1213
|
const tag = Buffer.from(parts[1], "base64");
|
|
1191
1214
|
const encrypted = Buffer.from(parts[2], "base64");
|
|
1192
|
-
const key = deriveKey2();
|
|
1193
1215
|
const decipher = createDecipheriv2("aes-256-gcm", key, iv);
|
|
1194
1216
|
decipher.setAuthTag(tag);
|
|
1195
1217
|
return decipher.update(encrypted) + decipher.final("utf8");
|
|
1196
1218
|
}
|
|
1219
|
+
function encrypt(data) {
|
|
1220
|
+
return encryptWith(data, getOrCreateKey());
|
|
1221
|
+
}
|
|
1222
|
+
function decrypt(blob) {
|
|
1223
|
+
const key = getOrCreateKey();
|
|
1224
|
+
try {
|
|
1225
|
+
return decryptWith(blob, key);
|
|
1226
|
+
} catch {
|
|
1227
|
+
const legacy = deriveLegacyKey();
|
|
1228
|
+
const plain = decryptWith(blob, legacy);
|
|
1229
|
+
writeFileSync2(getMemoryPath(), encryptWith(plain, key), "utf8");
|
|
1230
|
+
return plain;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1197
1233
|
function loadStore() {
|
|
1198
1234
|
const path = getMemoryPath();
|
|
1199
1235
|
if (!existsSync2(path)) {
|
|
@@ -2641,7 +2677,7 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
|
|
|
2641
2677
|
}
|
|
2642
2678
|
});
|
|
2643
2679
|
program2.command("status").description("Launch the quantum status dashboard in your browser").option("--port <port>", "Port to serve on", "9876").option("--no-open", "Don't auto-open the browser").action(async (cmd) => {
|
|
2644
|
-
const { startDashboardServer } = await import("./dashboard-
|
|
2680
|
+
const { startDashboardServer } = await import("./dashboard-5L4ILERJ.js");
|
|
2645
2681
|
const { exec } = await import("child_process");
|
|
2646
2682
|
const { platform } = await import("os");
|
|
2647
2683
|
const port = Number(cmd.port);
|