@i4ctime/q-ring 0.10.1 → 0.11.0

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.
@@ -0,0 +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, type AuditAction } from \"./observer.js\";\nimport { collapseEnvironment, readProjectConfig, type CollapseResult } from \"./collapse.js\";\nimport { listHooks, type HookEntry, type HookType } from \"./hooks.js\";\nimport { listApprovals } from \"./approval.js\";\nimport { listMemory } from \"./memory.js\";\nimport { getPolicySummary } from \"./policy.js\";\nimport { getDashboardHtml } from \"./dashboard-html.js\";\nimport { PACKAGE_VERSION } from \"../version.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 /** Validation provider name (e.g. \"openai\", \"stripe\") */\n provider?: string;\n /** Whether reads require explicit approval via MCP */\n requiresApproval?: boolean;\n /** Just-In-Time provisioner name, if any */\n jitProvider?: string;\n /** Whether the rotation format is declared on this envelope */\n hasRotationFormat: boolean;\n}\n\nexport interface TunnelSnapshot {\n id: string;\n createdAt: number;\n expiresAt?: number;\n accessCount: number;\n maxReads?: number;\n}\n\nexport interface ApprovalSnapshot {\n id: string;\n key: string;\n scope: string;\n reason: string;\n grantedBy: string;\n grantedAt: string;\n expiresAt: string;\n /** Seconds remaining until expiry; negative if expired */\n secondsRemaining: number;\n /** True if HMAC verifies and not expired */\n valid: boolean;\n /** True if HMAC failed (forged/tampered) */\n tampered: boolean;\n}\n\nexport interface HookSnapshot {\n id: string;\n type: HookType;\n description?: string;\n enabled: boolean;\n match: HookEntry[\"match\"];\n /** A short, human-readable summary of the match criteria */\n matchSummary: string;\n}\n\nexport interface ManifestSnapshot {\n /** Total declared keys in `.q-ring.json` `secrets` */\n declared: number;\n /** Number marked `required: true` */\n required: number;\n /** Required keys that are missing from project scope */\n missing: string[];\n /** Required keys that are present but expired */\n expired: string[];\n /** Required keys that are present but stale */\n stale: string[];\n}\n\nexport interface PolicySnapshot {\n hasMcpPolicy: boolean;\n hasExecPolicy: boolean;\n hasSecretPolicy: boolean;\n /** Counts per restriction so the UI can render at-a-glance dots */\n counts: {\n allowTools: number;\n denyTools: number;\n deniedKeys: number;\n deniedTags: number;\n allowCommands: number;\n denyCommands: number;\n requireApprovalForTags: number;\n requireRotationFormatForTags: number;\n };\n maxTtlSeconds?: number;\n maxRuntimeSeconds?: number;\n}\n\nexport interface AuditMetrics {\n /** Counts grouped by action over the audited window */\n byAction: Record<AuditAction, number>;\n /** Counts by source (cli, mcp, agent, etc.) */\n bySource: Record<string, number>;\n /** Top accessed keys (read action) over the window */\n topRead: Array<{ key: string; reads: number }>;\n /** Total events read for the metrics window */\n total: number;\n /** Window covered by the metrics, in seconds */\n windowSeconds: number;\n}\n\nexport interface ScopeBreakdown {\n global: number;\n project: number;\n team: number;\n org: number;\n}\n\nexport interface DashboardSnapshot {\n /** Snapshot generation timestamp (ISO) */\n timestamp: string;\n /** q-ring version that produced the snapshot */\n version: string;\n /** Project path in use (for manifest/policy resolution) */\n projectPath: string;\n /** All visible secrets (no values) */\n secrets: SecretSnapshot[];\n /** Headline counts of secret health */\n health: { healthy: number; stale: number; expired: number; noDecay: number; total: number };\n /** Counts per scope */\n scopes: ScopeBreakdown;\n /** Secrets that require approval before MCP read */\n protectedCount: number;\n /** Active entanglement links */\n entanglements: EntanglementPair[];\n /** In-memory tunnels */\n tunnels: TunnelSnapshot[];\n /** Recent audit events (most recent first; \"list\" filtered out) */\n audit: AuditEvent[];\n /** Aggregate audit metrics (read counts, top keys, source breakdown) */\n auditMetrics: AuditMetrics;\n /** Detected access anomalies */\n anomalies: AccessAnomaly[];\n /** Auto-detected environment & detection source */\n environment: CollapseResult | null;\n /** `.q-ring.json` manifest analysis (declared vs missing) */\n manifest: ManifestSnapshot | null;\n /** Governance policy summary */\n policy: PolicySnapshot;\n /** Active approval tokens (HMAC-verified) */\n approvals: ApprovalSnapshot[];\n /** Registered hooks (shell/http/signal) */\n hooks: HookSnapshot[];\n /** Agent memory key count */\n memoryKeys: number;\n}\n\nconst AUDIT_WINDOW_SECONDS = 24 * 60 * 60; // 24h\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 provider: envelope?.meta.provider,\n requiresApproval: envelope?.meta.requiresApproval,\n jitProvider: envelope?.meta.jitProvider,\n hasRotationFormat: !!envelope?.meta.rotationFormat,\n };\n}\n\nfunction summariseHookMatch(match: HookEntry[\"match\"]): string {\n const parts: string[] = [];\n if (match.key) parts.push(`key=${match.key}`);\n if (match.keyPattern) parts.push(`pattern=${match.keyPattern}`);\n if (match.tag) parts.push(`tag=${match.tag}`);\n if (match.scope) parts.push(`scope=${match.scope}`);\n if (match.action?.length) parts.push(`on=${match.action.join(\"|\")}`);\n return parts.length ? parts.join(\" · \") : \"any change\";\n}\n\nfunction buildManifest(\n projectPath: string,\n secrets: SecretSnapshot[],\n): ManifestSnapshot | null {\n const config = readProjectConfig(projectPath);\n if (!config?.secrets) return null;\n const declared = Object.keys(config.secrets);\n const projectSecrets = new Map(\n secrets.filter((s) => s.scope === \"project\").map((s) => [s.key, s]),\n );\n\n const required: string[] = [];\n const missing: string[] = [];\n const expired: string[] = [];\n const stale: string[] = [];\n\n for (const key of declared) {\n const entry = config.secrets[key];\n if (entry.required) required.push(key);\n const present = projectSecrets.get(key);\n if (!present) {\n if (entry.required) missing.push(key);\n continue;\n }\n if (present.decay.isExpired) expired.push(key);\n else if (present.decay.isStale) stale.push(key);\n }\n\n return {\n declared: declared.length,\n required: required.length,\n missing,\n expired,\n stale,\n };\n}\n\nfunction buildPolicySnapshot(projectPath: string): PolicySnapshot {\n const summary = getPolicySummary(projectPath);\n const mcp = summary.details.mcp ?? {};\n const exec = summary.details.exec ?? {};\n const sec = summary.details.secrets ?? {};\n return {\n hasMcpPolicy: summary.hasMcpPolicy,\n hasExecPolicy: summary.hasExecPolicy,\n hasSecretPolicy: summary.hasSecretPolicy,\n counts: {\n allowTools: mcp.allowTools?.length ?? 0,\n denyTools: mcp.denyTools?.length ?? 0,\n deniedKeys: mcp.deniedKeys?.length ?? 0,\n deniedTags: mcp.deniedTags?.length ?? 0,\n allowCommands: exec.allowCommands?.length ?? 0,\n denyCommands: exec.denyCommands?.length ?? 0,\n requireApprovalForTags: sec.requireApprovalForTags?.length ?? 0,\n requireRotationFormatForTags: sec.requireRotationFormatForTags?.length ?? 0,\n },\n maxTtlSeconds: sec.maxTtlSeconds,\n maxRuntimeSeconds: exec.maxRuntimeSeconds,\n };\n}\n\nfunction buildAuditMetrics(events: AuditEvent[]): AuditMetrics {\n const byAction = {} as Record<AuditAction, number>;\n const bySource: Record<string, number> = {};\n const readCounts = new Map<string, number>();\n\n for (const e of events) {\n byAction[e.action] = (byAction[e.action] ?? 0) + 1;\n bySource[e.source] = (bySource[e.source] ?? 0) + 1;\n if (e.action === \"read\" && e.key) {\n readCounts.set(e.key, (readCounts.get(e.key) ?? 0) + 1);\n }\n }\n\n const topRead = [...readCounts.entries()]\n .map(([key, reads]) => ({ key, reads }))\n .sort((a, b) => b.reads - a.reads)\n .slice(0, 10);\n\n return {\n byAction,\n bySource,\n topRead,\n total: events.length,\n windowSeconds: AUDIT_WINDOW_SECONDS,\n };\n}\n\nexport function collectSnapshot(): DashboardSnapshot {\n const projectPath = process.cwd();\n const entries = listSecrets({ source: \"api\", silent: true, projectPath });\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 let protectedCount = 0;\n const scopes: ScopeBreakdown = { global: 0, project: 0, team: 0, org: 0 };\n\n for (const s of secrets) {\n if (!s.decay.timeRemaining) noDecay++;\n else if (s.decay.isExpired) expired++;\n else if (s.decay.isStale) stale++;\n else healthy++;\n if (s.requiresApproval) protectedCount++;\n if (s.scope === \"global\") scopes.global++;\n else if (s.scope === \"project\") scopes.project++;\n else if (s.scope === \"team\") scopes.team++;\n else if (s.scope === \"org\") scopes.org++;\n }\n\n const auditWindow = queryAudit({\n since: new Date(Date.now() - AUDIT_WINDOW_SECONDS * 1000).toISOString(),\n }).filter((e) => e.action !== \"list\");\n\n const recent = auditWindow.slice(0, 80);\n\n const approvals = listApprovals().map<ApprovalSnapshot>((a) => ({\n id: a.id,\n key: a.key,\n scope: a.scope,\n reason: a.reason,\n grantedBy: a.grantedBy,\n grantedAt: a.grantedAt,\n expiresAt: a.expiresAt,\n secondsRemaining: Math.floor(\n (new Date(a.expiresAt).getTime() - Date.now()) / 1000,\n ),\n valid: a.valid && !a.tampered,\n tampered: a.tampered,\n }));\n\n const hookEntries = listHooks().map<HookSnapshot>((h) => ({\n id: h.id,\n type: h.type,\n description: h.description,\n enabled: h.enabled,\n match: h.match,\n matchSummary: summariseHookMatch(h.match),\n }));\n\n return {\n timestamp: new Date().toISOString(),\n version: PACKAGE_VERSION,\n projectPath,\n secrets,\n health: { healthy, stale, expired, noDecay, total: secrets.length },\n scopes,\n protectedCount,\n entanglements: listEntanglements(),\n tunnels: tunnelList(),\n audit: recent,\n auditMetrics: buildAuditMetrics(auditWindow),\n anomalies: detectAnomalies(),\n environment: collapseEnvironment({ projectPath }),\n manifest: buildManifest(projectPath, secrets),\n policy: buildPolicySnapshot(projectPath),\n approvals,\n hooks: hookEntries,\n memoryKeys: listMemory().length,\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 *\n * Matches the gh-pages site design system: deep-navy palette, neon cyan→violet\n * SVG icons, glassmorphism cards. Zero dependencies — inline CSS + vanilla JS\n * with EventSource for SSE.\n *\n * Layout:\n * 1. Sticky header with brand, version, project path, live/paused state,\n * pause+refresh controls, last-update timestamp.\n * 2. KPI strip — at-a-glance counters (total, healthy, stale/expired,\n * protected, hooks, approvals, anomalies, audit-24h).\n * 3. Health donut + Environment + Manifest + Policy summary cards.\n * 4. Searchable + sortable secrets table (key, scope, env, type, decay,\n * tags, last accessed). Click a row to inspect details.\n * 5. Quantum side panels — Decay, Superposition, Entanglement, Tunnels.\n * 6. Governance — Approvals, Hooks, Memory.\n * 7. Anomalies (full-width).\n * 8. Audit log with action+source+text filters and an action-count strip.\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:1320px;margin:0 auto;padding:20px 20px 48px}\n\n/* Header */\n.header{position:sticky;top:0;z-index:5;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:14px;margin-bottom:20px;padding:14px 18px;background:rgba(4,8,15,0.85);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-left{display:flex;align-items:center;gap:14px;flex-wrap:wrap;min-width:0}\n.header h1{font-family:var(--font-display);font-size:1.45rem;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:1rem;-webkit-text-fill-color:var(--text-dim)}\n.meta-chip{font-family:var(--font-mono);font-size:.72rem;color:var(--text-dim);background:rgba(255,255,255,.04);border:1px solid var(--border);padding:3px 8px;border-radius:99px;display:inline-flex;align-items:center;gap:6px;max-width:340px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.meta-chip svg{width:11px;height:11px;flex-shrink:0;opacity:.7}\n.header-right{display:flex;align-items:center;gap:10px;flex-wrap:wrap}\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);animation:none}\n.status-dot.paused{background:var(--warning);box-shadow:0 0 8px var(--warning);animation:none}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}\n.conn-label{font-size:.78rem;color:var(--text-dim);display:flex;align-items:center;gap:6px;font-family:var(--font-mono)}\n.btn{font-family:var(--font-display);font-size:.78rem;font-weight:500;color:var(--text-secondary);background:rgba(255,255,255,.04);border:1px solid var(--border);padding:5px 11px;border-radius:var(--radius-sm);cursor:pointer;display:inline-flex;align-items:center;gap:5px;transition:background .2s,border-color .2s,color .2s}\n.btn:hover{background:rgba(14,165,233,.08);border-color:var(--accent);color:var(--text-primary)}\n.btn svg{width:13px;height:13px}\n.btn.active{background:var(--accent-dim);color:var(--accent-bright);border-color:rgba(14,165,233,.4)}\n.kbd{font-family:var(--font-mono);font-size:.65rem;color:var(--text-dim);background:rgba(255,255,255,.06);border:1px solid var(--border);border-bottom-width:2px;padding:1px 5px;border-radius:4px;margin-left:4px}\n\n/* KPI strip */\n.kpi-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:20px}\n.kpi{background:var(--glass-bg);backdrop-filter:blur(20px);-webkit-backdrop-filter:blur(20px);border:1px solid var(--glass-border);border-radius:var(--radius);padding:12px 14px;display:flex;flex-direction:column;gap:2px;transition:border-color .25s,transform .25s}\n.kpi:hover{border-color:var(--border-glow);transform:translateY(-2px)}\n.kpi-label{font-size:.66rem;text-transform:uppercase;letter-spacing:.1em;color:var(--text-dim);font-weight:600;display:flex;align-items:center;gap:6px}\n.kpi-label svg{width:12px;height:12px;flex-shrink:0}\n.kpi-value{font-family:var(--font-display);font-size:1.5rem;font-weight:700;line-height:1.1;color:var(--text-primary)}\n.kpi-value.warning{color:var(--warning)}\n.kpi-value.danger{color:var(--danger)}\n.kpi-value.green{color:var(--green)}\n.kpi-value.dim{color:var(--text-dim)}\n.kpi-sub{font-size:.7rem;color:var(--text-dim);font-family:var(--font-mono)}\n\n/* Grid */\n.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(360px,1fr));gap:16px;margin-bottom: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)}\n@keyframes cardReveal{to{opacity:1;transform:translateY(0)}}\n\n.card-title{font-family:var(--font-display);font-size:.78rem;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.card-title .title-aside{margin-left:auto;font-size:.7rem;color:var(--text-dim);font-weight:400;letter-spacing:.04em;text-transform:none;display:flex;align-items:center;gap:6px}\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;font-family:var(--font-mono)}\n.legend-item{display:flex;align-items:center;gap:8px;font-size:.82rem}\n.legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0}\n.scope-row{display:flex;gap:6px;margin-top:14px;flex-wrap:wrap;font-family:var(--font-mono);font-size:.72rem;color:var(--text-dim)}\n.scope-pill{padding:2px 8px;border-radius:99px;background:rgba(255,255,255,.04);border:1px solid var(--border)}\n.scope-pill strong{color:var(--text-primary);margin-right:4px;font-weight:600}\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:60px;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:.72rem;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.env-test{background:rgba(14,165,233,0.15);color:var(--accent-bright);border:1px solid rgba(14,165,233,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:.83rem}\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:.83rem;font-family:var(--font-mono)}\n.tunnel-meta{display:flex;gap:12px;margin-top:4px;font-size:.78rem;color:var(--text-dim)}\n\n/* Audit feed */\n.audit-toolbar{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px;align-items:center}\n.audit-actions-strip{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:10px}\n.audit-chip{font-family:var(--font-mono);font-size:.7rem;padding:2px 9px;border-radius:99px;background:rgba(255,255,255,.04);border:1px solid var(--border);color:var(--text-secondary);cursor:pointer;transition:all .2s}\n.audit-chip:hover{border-color:var(--accent);color:var(--text-primary)}\n.audit-chip.active{background:var(--accent-dim);color:var(--accent-bright);border-color:rgba(14,165,233,.4)}\n.audit-chip strong{color:var(--text-primary);margin-left:5px;font-weight:600}\n.audit-feed{display:flex;flex-direction:column;gap:2px;max-height:340px;overflow-y:auto;font-size:.82rem;font-family:var(--font-mono)}\n.audit-row{display:grid;grid-template-columns:78px 76px 56px 1fr;gap:8px;padding:4px 0;border-bottom:1px solid rgba(255,255,255,0.03);align-items:baseline}\n.audit-ts{color:var(--text-dim)}\n.audit-action{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)}.audit-action.approve{color:var(--green)}.audit-action.revoke{color:var(--danger)}\n.audit-action.policy_deny{color:var(--danger)}.audit-action.rotate{color:var(--violet)}\n.audit-source{color:var(--text-dim);font-size:.72rem;text-transform:uppercase;letter-spacing:.04em}\n.audit-detail{color:var(--text-dim);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.audit-key{color:var(--text-secondary)}\n\n/* Anomalies */\n.anomaly-list{display:flex;flex-direction:column;gap:8px}\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:.7rem;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:.86rem;color:var(--text-primary)}\n.anomaly-hint{font-size:.74rem;color:var(--text-dim);margin-top:4px;font-family:var(--font-mono)}\n\n/* Environment badge */\n.env-status{display:flex;align-items:center;gap:12px;flex-wrap:wrap}\n.env-big{font-size:1rem;font-weight:700;padding:4px 14px;border-radius:var(--radius-sm)}\n.env-source{font-size:.78rem;color:var(--text-dim);font-family:var(--font-mono)}\n\n/* Manifest */\n.manifest-bars{display:flex;flex-direction:column;gap:8px}\n.manifest-row{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.78rem}\n.manifest-bar{flex:1;height:6px;border-radius:3px;background:rgba(255,255,255,.06);overflow:hidden}\n.manifest-fill{height:100%;border-radius:3px}\n.manifest-list{margin-top:10px;display:flex;flex-direction:column;gap:3px;font-family:var(--font-mono);font-size:.78rem;color:var(--text-secondary)}\n.manifest-list .label{color:var(--text-dim);text-transform:uppercase;font-size:.66rem;letter-spacing:.08em;margin-top:6px}\n.manifest-list .key-pill{display:inline-block;padding:1px 6px;background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:4px;margin:1px 3px 1px 0;font-size:.74rem}\n.manifest-list .key-pill.miss{color:var(--danger);border-color:rgba(255,94,91,.3)}\n.manifest-list .key-pill.exp{color:var(--warning);border-color:rgba(251,191,36,.3)}\n\n/* Policy */\n.policy-rows{display:flex;flex-direction:column;gap:6px;font-family:var(--font-mono);font-size:.78rem;color:var(--text-secondary)}\n.policy-row{display:flex;justify-content:space-between;padding:2px 0;border-bottom:1px solid rgba(255,255,255,.04)}\n.policy-label{color:var(--text-dim)}\n.policy-value{color:var(--text-primary)}\n.policy-value.zero{color:var(--text-dim)}\n\n/* Approvals */\n.approval-list{display:flex;flex-direction:column;gap:8px;max-height:240px;overflow-y:auto}\n.approval-card{background:rgba(34,197,94,.05);border:1px solid rgba(34,197,94,.2);border-radius:var(--radius-sm);padding:10px 12px;font-size:.82rem}\n.approval-card.tampered{background:rgba(255,94,91,.06);border-color:rgba(255,94,91,.4)}\n.approval-card.expiring{background:rgba(251,191,36,.06);border-color:rgba(251,191,36,.3)}\n.approval-head{display:flex;justify-content:space-between;gap:8px;font-family:var(--font-mono);font-weight:600}\n.approval-reason{color:var(--text-dim);font-size:.76rem;margin-top:3px}\n.approval-meta{display:flex;gap:12px;margin-top:4px;font-size:.72rem;color:var(--text-dim);font-family:var(--font-mono)}\n\n/* Hooks */\n.hooks-list{display:flex;flex-direction:column;gap:6px;max-height:240px;overflow-y:auto}\n.hook-row{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:.8rem;padding:6px 10px;background:rgba(255,255,255,.03);border:1px solid var(--border);border-radius:var(--radius-sm)}\n.hook-row.disabled{opacity:.5}\n.hook-type{font-size:.7rem;text-transform:uppercase;font-weight:600;padding:1px 6px;border-radius:99px;letter-spacing:.04em}\n.hook-type.shell{background:rgba(168,85,247,.15);color:var(--violet)}\n.hook-type.http{background:rgba(14,165,233,.15);color:var(--accent-bright)}\n.hook-type.signal{background:rgba(251,191,36,.15);color:var(--warning)}\n.hook-summary{flex:1;color:var(--text-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.hook-id{color:var(--text-dim);font-size:.72rem}\n\n/* Secrets table */\n.secrets-toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px;align-items:center}\n.search{flex:1;min-width:180px;display:flex;align-items:center;gap:6px;background:rgba(255,255,255,.04);border:1px solid var(--border);border-radius:var(--radius-sm);padding:5px 10px;transition:border-color .2s}\n.search:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px rgba(14,165,233,.12)}\n.search svg{width:13px;height:13px;color:var(--text-dim);flex-shrink:0}\n.search input{flex:1;background:transparent;border:none;outline:none;color:var(--text-primary);font-family:var(--font-mono);font-size:.82rem}\n.search input::placeholder{color:var(--text-dim)}\n.secrets-table-wrap{max-height:380px;overflow:auto;border:1px solid var(--border);border-radius:var(--radius-sm);background:rgba(4,8,15,.4)}\n.secrets-table{width:100%;border-collapse:collapse;font-family:var(--font-mono);font-size:.78rem}\n.secrets-table thead th{position:sticky;top:0;background:rgba(8,14,24,.95);backdrop-filter:blur(8px);text-align:left;padding:8px 10px;color:var(--text-dim);font-weight:600;font-size:.7rem;text-transform:uppercase;letter-spacing:.05em;border-bottom:1px solid var(--border);cursor:pointer;user-select:none;white-space:nowrap}\n.secrets-table thead th:hover{color:var(--text-primary)}\n.secrets-table thead th .sort-arrow{display:inline-block;margin-left:4px;color:var(--accent-bright);opacity:0;font-size:.7rem}\n.secrets-table thead th.sorted .sort-arrow{opacity:1}\n.secrets-table tbody td{padding:6px 10px;border-bottom:1px solid rgba(255,255,255,.03);color:var(--text-secondary);vertical-align:middle}\n.secrets-table tbody tr:hover td{background:rgba(14,165,233,.05);color:var(--text-primary)}\n.secrets-table .col-key{color:var(--text-primary);font-weight:500;max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.secrets-table .col-decay{min-width:90px}\n.secrets-table .col-tags{max-width:200px;overflow:hidden}\n.tag-pill{display:inline-block;font-size:.68rem;padding:1px 6px;background:rgba(168,85,247,.1);color:var(--violet);border:1px solid rgba(168,85,247,.2);border-radius:99px;margin-right:3px;font-family:var(--font-mono)}\n.scope-tag{display:inline-block;font-size:.68rem;padding:1px 6px;border-radius:4px;letter-spacing:.04em;font-weight:600;text-transform:uppercase}\n.scope-tag.global{background:rgba(14,165,233,.15);color:var(--accent-bright)}\n.scope-tag.project{background:rgba(34,197,94,.15);color:var(--green)}\n.scope-tag.team{background:rgba(168,85,247,.15);color:var(--violet)}\n.scope-tag.org{background:rgba(251,191,36,.15);color:var(--warning)}\n.type-tag{display:inline-block;font-size:.68rem;padding:1px 6px;border-radius:4px;letter-spacing:.04em;text-transform:uppercase;background:rgba(255,255,255,.05);color:var(--text-dim)}\n.type-tag.superposition{background:rgba(168,85,247,.12);color:var(--violet)}\n.protected-icon{color:var(--warning);margin-left:4px;display:inline-flex;vertical-align:middle}\n.mini-decay{display:inline-flex;align-items:center;gap:6px;width:100%}\n.mini-decay-bar{flex:1;height:4px;border-radius:2px;background:rgba(255,255,255,.06);overflow:hidden;min-width:30px}\n.mini-decay-fill{height:100%;border-radius:2px}\n.mini-decay-time{color:var(--text-dim);font-size:.7rem;min-width:38px;text-align:right}\n\n/* Empty states */\n.empty{color:var(--text-dim);font-size:.86rem;font-style:italic;padding:8px 0}\n.empty-cta{margin-top:6px;display:block;font-style:normal;font-family:var(--font-mono);font-size:.78rem;color:var(--accent-bright)}\n\n/* Footer */\n.foot{margin-top:24px;text-align:center;color:var(--text-dim);font-size:.72rem;font-family:var(--font-mono)}\n.foot a{color:var(--accent-bright);text-decoration:none}\n.foot a:hover{text-decoration:underline}\n\n/* Scrollbar */\n::-webkit-scrollbar{width:6px;height:6px}\n::-webkit-scrollbar-track{background:transparent}\n::-webkit-scrollbar-thumb{background:rgba(255,255,255,0.08);border-radius:3px}\n::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,0.15)}\n\n/* Print */\n@media print{\n .blob,.header-right{display:none}\n .header{position:static}\n body{background:#fff;color:#000}\n}\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\n<div class=\"container\">\n <header class=\"header\">\n <div class=\"header-left\">\n <h1>\n <span class=\"q-icon\"><svg width=\"26\" height=\"26\" 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 <span class=\"meta-chip\" id=\"versionChip\" title=\"q-ring version\"></span>\n <span class=\"meta-chip\" id=\"projectChip\" title=\"Project working directory\"></span>\n </div>\n <div class=\"header-right\">\n <span class=\"conn-label\" title=\"Last update from the live stream\"><span id=\"lastUpdate\">—</span></span>\n <button class=\"btn\" id=\"pauseBtn\" type=\"button\" title=\"Pause / resume the live stream (P)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\" rx=\"1\"/><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\" rx=\"1\"/></svg>\n <span id=\"pauseLabel\">Pause</span>\n </button>\n <button class=\"btn\" id=\"refreshBtn\" type=\"button\" title=\"Force-refresh now (R)\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M21 12a9 9 0 1 1-3.5-7.1\"/><polyline points=\"21 3 21 9 15 9\"/></svg>\n Refresh\n </button>\n <a class=\"btn\" id=\"jsonLink\" href=\"/api/status\" target=\"_blank\" rel=\"noopener\" title=\"Open the raw JSON snapshot in a new tab\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"16 18 22 12 16 6\"/><polyline points=\"8 6 2 12 8 18\"/></svg>\n JSON\n </a>\n <span class=\"conn-label\"><span class=\"status-dot\" id=\"connDot\"></span><span id=\"connText\">connecting…</span></span>\n </div>\n </header>\n\n <section class=\"kpi-strip\" id=\"kpiStrip\"></section>\n\n <section class=\"grid\" id=\"overviewGrid\"></section>\n\n <section id=\"secretsSection\"></section>\n\n <section class=\"grid\" id=\"quantumGrid\"></section>\n\n <section class=\"grid\" id=\"govGrid\"></section>\n\n <section class=\"grid\" id=\"anomalyAuditGrid\"></section>\n\n <p class=\"foot\">\n <span id=\"footMeta\">—</span>\n · keyboard: <span class=\"kbd\">/</span> search · <span class=\"kbd\">P</span> pause · <span class=\"kbd\">R</span> refresh\n </p>\n</div>\n\n<script>\n(function(){\n const $ = (id) => document.getElementById(id);\n\n /* --- SVG icon library (matching gh-pages neon gradient) --- */\n const ic = (path) => '<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"url(#neon-grad)\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">'+path+'</svg>';\n const icons = {\n health: ic('<path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/>'),\n environment: ic('<polyline points=\"22 12 18 12 15 21 9 3 6 12 2 12\"/>'),\n decay: ic('<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\"/>'),\n superposition: ic('<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\"/>'),\n entangle: ic('<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\"/>'),\n tunnel: ic('<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\"/>'),\n anomaly: ic('<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\"/>'),\n audit: ic('<circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/>'),\n key: ic('<path d=\"M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4\"/>'),\n shield: ic('<path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/>'),\n hook: ic('<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\"/>'),\n approve: ic('<path d=\"M9 11l3 3L22 4\"/><path d=\"M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11\"/>'),\n manifest: ic('<path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"/><polyline points=\"14 2 14 8 20 8\"/><line x1=\"16\" y1=\"13\" x2=\"8\" y2=\"13\"/><line x1=\"16\" y1=\"17\" x2=\"8\" y2=\"17\"/>'),\n policy: ic('<path d=\"M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z\"/><path d=\"M9 12l2 2 4-4\"/>'),\n memory: ic('<rect x=\"4\" y=\"4\" width=\"16\" height=\"16\" rx=\"2\"/><rect x=\"9\" y=\"9\" width=\"6\" height=\"6\"/><line x1=\"9\" y1=\"2\" x2=\"9\" y2=\"4\"/><line x1=\"15\" y1=\"2\" x2=\"15\" y2=\"4\"/><line x1=\"9\" y1=\"20\" x2=\"9\" y2=\"22\"/><line x1=\"15\" y1=\"20\" x2=\"15\" y2=\"22\"/><line x1=\"20\" y1=\"9\" x2=\"22\" y2=\"9\"/><line x1=\"20\" y1=\"14\" x2=\"22\" y2=\"14\"/><line x1=\"2\" y1=\"9\" x2=\"4\" y2=\"9\"/><line x1=\"2\" y1=\"14\" x2=\"4\" y2=\"14\"/>'),\n folder: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z\"/></svg>',\n tag: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z\"/><line x1=\"7\" y1=\"7\" x2=\"7.01\" y2=\"7\"/></svg>',\n search: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\"/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\"/></svg>',\n lock: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\" ry=\"2\"/><path d=\"M7 11V7a5 5 0 0 1 10 0v4\"/></svg>'\n };\n\n function esc(s){ const d=document.createElement('div');d.textContent=s==null?'':String(s);return d.innerHTML; }\n\n function envClass(e){\n if(!e) return 'env-default';\n const k=e.toLowerCase();\n if(k==='prod'||k==='production') return 'env-prod';\n if(k==='staging'||k==='stage') return 'env-staging';\n if(k==='dev'||k==='development') return 'env-dev';\n if(k==='test'||k==='testing') return 'env-test';\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 fmtRelative(ts){\n if(!ts) return '—';\n const diff=Math.floor((Date.now()-new Date(ts).getTime())/1000);\n if(isNaN(diff)) return '—';\n if(diff<5) return 'just now';\n if(diff<60) return diff+'s ago';\n if(diff<3600) return Math.floor(diff/60)+'m ago';\n if(diff<86400) return Math.floor(diff/3600)+'h ago';\n return Math.floor(diff/86400)+'d ago';\n }\n\n function fmtDuration(seconds){\n if(seconds==null) return '—';\n if(seconds<0) return 'expired';\n if(seconds<60) return seconds+'s';\n if(seconds<3600) return Math.floor(seconds/60)+'m';\n if(seconds<86400) return Math.floor(seconds/3600)+'h';\n return Math.floor(seconds/86400)+'d';\n }\n\n /* --- State --- */\n const state = {\n snapshot: null,\n paused: false,\n secretsQuery: '',\n secretsSort: { col: 'lastAccessedAt', dir: 'desc' },\n auditFilter: { action: '', source: '', text: '' }\n };\n\n /* --- Renderers --- */\n\n function renderHeaderMeta(s){\n $('versionChip').innerHTML = '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"20 6 9 17 4 12\"/></svg> v'+esc(s.version);\n $('projectChip').innerHTML = icons.folder + ' ' + esc(s.projectPath);\n }\n\n function renderKpis(s){\n const m = s.auditMetrics || { total:0, byAction:{}, topRead:[] };\n const reads = m.byAction.read || 0;\n const writes = (m.byAction.write||0) + (m.byAction.delete||0) + (m.byAction.rotate||0);\n const denied = m.byAction.policy_deny || 0;\n const enabledHooks = (s.hooks||[]).filter(h=>h.enabled).length;\n const validApprovals = (s.approvals||[]).filter(a=>a.valid).length;\n\n const items = [\n { label: 'Secrets', icon: icons.key, value: s.health.total, sub: s.health.healthy+' healthy · '+s.health.stale+' stale · '+s.health.expired+' expired', cls: '' },\n { label: 'Environment', icon: icons.environment, value: s.environment ? s.environment.env : 'none', sub: s.environment ? 'via '+s.environment.source : 'no env detected', cls: s.environment ? '' : 'dim' },\n { label: 'Protected', icon: icons.lock, value: s.protectedCount, sub: 'require approval', cls: s.protectedCount>0 ? 'warning' : 'dim' },\n { label: 'Approvals', icon: icons.approve, value: validApprovals, sub: 'active grants', cls: validApprovals>0 ? 'green' : 'dim' },\n { label: 'Hooks', icon: icons.hook, value: enabledHooks, sub: 'enabled / '+(s.hooks||[]).length+' total', cls: enabledHooks>0 ? '' : 'dim' },\n { label: 'Reads (24h)', icon: icons.audit, value: reads, sub: writes+' writes · '+denied+' denied', cls: '' },\n { label: 'Anomalies', icon: icons.anomaly, value: (s.anomalies||[]).length, sub: (s.anomalies||[]).length ? 'investigate now' : 'all clear', cls: (s.anomalies||[]).length ? 'danger' : 'green' }\n ];\n $('kpiStrip').innerHTML = items.map(k =>\n '<div class=\"kpi\"><div class=\"kpi-label\">'+k.icon+esc(k.label)+'</div><div class=\"kpi-value '+k.cls+'\">'+esc(k.value)+'</div><div class=\"kpi-sub\">'+esc(k.sub)+'</div></div>'\n ).join('');\n }\n\n function renderHealthCard(s){\n const h=s.health;\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 const sc=s.scopes||{global:0,project:0,team:0,org:0};\n const scopeRow = '<div class=\"scope-row\">'+\n ['global','project','team','org'].filter(k=>sc[k]).map(k =>\n '<span class=\"scope-pill\"><strong>'+sc[k]+'</strong>'+k+'</span>'\n ).join('') + '</div>';\n\n return '<div class=\"card-title\">'+icons.health+'Health Summary'+\n '<span class=\"title-aside\">'+esc(h.healthy)+'/'+esc(h.total)+' healthy</span></div>'+\n '<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>'+ scopeRow;\n }\n\n function renderEnvCard(s){\n const env=s.environment;\n const body = env\n ? '<div class=\"env-status\"><span class=\"env-big env-pill '+envClass(env.env)+'\">'+esc(env.env)+'</span><span class=\"env-source\">detected via '+esc(env.source)+'</span></div>'\n : '<div class=\"empty\">No environment detected.<span class=\"empty-cta\">$ qring env --project-path .</span></div>';\n return '<div class=\"card-title\">'+icons.environment+'Environment'+'</div>'+ body;\n }\n\n function renderManifestCard(s){\n const m=s.manifest;\n if(!m){\n return '<div class=\"card-title\">'+icons.manifest+'Manifest</div>'+\n '<div class=\"empty\">No <code>.q-ring.json</code> manifest in this project.<span class=\"empty-cta\">$ qring wizard &lt;name&gt;</span></div>';\n }\n const ok = m.required - m.missing.length - m.expired.length - m.stale.length;\n const okPct = m.required ? Math.max(0, Math.round((ok/m.required)*100)) : 100;\n const fillCol = m.missing.length ? 'var(--danger)' : (m.expired.length||m.stale.length ? 'var(--warning)' : 'var(--green)');\n\n const list = [];\n if(m.missing.length){\n list.push('<div class=\"label\">Missing required ('+m.missing.length+')</div><div>'+m.missing.map(k=>'<span class=\"key-pill miss\">'+esc(k)+'</span>').join('')+'</div>');\n }\n if(m.expired.length){\n list.push('<div class=\"label\">Expired ('+m.expired.length+')</div><div>'+m.expired.map(k=>'<span class=\"key-pill exp\">'+esc(k)+'</span>').join('')+'</div>');\n }\n if(m.stale.length){\n list.push('<div class=\"label\">Stale ('+m.stale.length+')</div><div>'+m.stale.map(k=>'<span class=\"key-pill exp\">'+esc(k)+'</span>').join('')+'</div>');\n }\n if(!list.length) list.push('<div class=\"label\">All required keys present and healthy.</div>');\n\n return '<div class=\"card-title\">'+icons.manifest+'Manifest'+\n '<span class=\"title-aside\">'+esc(m.required)+' required / '+esc(m.declared)+' declared</span></div>'+\n '<div class=\"manifest-bars\">'+\n '<div class=\"manifest-row\"><span style=\"min-width:54px\">healthy</span><div class=\"manifest-bar\"><div class=\"manifest-fill\" style=\"width:'+okPct+'%;background:'+fillCol+'\"></div></div><span>'+okPct+'%</span></div>'+\n '</div>'+\n '<div class=\"manifest-list\">'+list.join('')+'</div>';\n }\n\n function renderPolicyCard(s){\n const p=s.policy||{counts:{}};\n const c=p.counts||{};\n const has = p.hasMcpPolicy || p.hasExecPolicy || p.hasSecretPolicy;\n if(!has){\n return '<div class=\"card-title\">'+icons.policy+'Policy</div>'+\n '<div class=\"empty\">No governance policy declared.<span class=\"empty-cta\">add <code>policy</code> to <code>.q-ring.json</code></span></div>';\n }\n const row = (label,value) => '<div class=\"policy-row\"><span class=\"policy-label\">'+esc(label)+'</span><span class=\"policy-value '+(value?'':'zero')+'\">'+esc(value || '—')+'</span></div>';\n return '<div class=\"card-title\">'+icons.policy+'Policy'+\n '<span class=\"title-aside\">'+esc([\n p.hasMcpPolicy?'mcp':'', p.hasExecPolicy?'exec':'', p.hasSecretPolicy?'secrets':''\n ].filter(Boolean).join(' · ') || 'none')+'</span></div>'+\n '<div class=\"policy-rows\">'+\n row('MCP allow tools', c.allowTools)+\n row('MCP deny tools', c.denyTools)+\n row('Denied keys', c.deniedKeys)+\n row('Denied tags', c.deniedTags)+\n row('Exec allow', c.allowCommands)+\n row('Exec deny', c.denyCommands)+\n row('Tag → approval', c.requireApprovalForTags)+\n row('Tag → rotation', c.requireRotationFormatForTags)+\n (p.maxTtlSeconds ? row('Max TTL', fmtDuration(p.maxTtlSeconds)) : '')+\n (p.maxRuntimeSeconds ? row('Max exec runtime', fmtDuration(p.maxRuntimeSeconds)) : '')+\n '</div>';\n }\n\n function renderDecayCard(s){\n const withDecay = (s.secrets||[]).filter(x=>x.decay && x.decay.timeRemaining)\n .sort((a,b)=>(a.decay.secondsRemaining||0)-(b.decay.secondsRemaining||0));\n const inner = !withDecay.length\n ? '<div class=\"empty\">No secrets with decay configured.<span class=\"empty-cta\">$ qring set KEY --ttl 86400</span></div>'\n : '<div class=\"decay-list\">'+withDecay.slice(0,12).map(x=>{\n const pct=Math.min(x.decay.lifetimePercent,100);\n const col=decayColor(pct,x.decay.isExpired);\n const label=x.decay.isExpired?'expired':x.decay.timeRemaining||'';\n return '<div class=\"decay-item\"><span class=\"decay-key\" title=\"'+esc(x.key)+'\">'+esc(x.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></div>';\n }).join('')+'</div>';\n return '<div class=\"card-title\">'+icons.decay+'Decay Timers'+\n '<span class=\"title-aside\">'+esc(withDecay.length)+' tracked</span></div>'+ inner;\n }\n\n function renderSuperpositionCard(s){\n const sup = (s.secrets||[]).filter(x=>x.type==='superposition' && x.environments && x.environments.length);\n const inner = !sup.length\n ? '<div class=\"empty\">No secrets in superposition.<span class=\"empty-cta\">$ qring set KEY --env prod</span></div>'\n : '<div class=\"super-list\">'+sup.slice(0,14).map(x=>{\n const pills=(x.environments||[]).map(e =>\n '<span class=\"env-pill '+envClass(e)+'\">'+esc(e)+(e===x.defaultEnv?' ✓':'')+'</span>'\n ).join('');\n return '<div class=\"super-item\"><span class=\"super-key\">'+esc(x.key)+'</span>'+pills+'</div>';\n }).join('')+'</div>';\n return '<div class=\"card-title\">'+icons.superposition+'Superposition'+\n '<span class=\"title-aside\">'+esc(sup.length)+' multi-env</span></div>'+ inner;\n }\n\n function renderEntanglementCard(s){\n const pairs=s.entanglements||[];\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 const inner = !unique.length\n ? '<div class=\"empty\">No entangled secrets.<span class=\"empty-cta\">$ qring entangle KEY_A KEY_B</span></div>'\n : '<div class=\"entangle-list\">'+unique.slice(0,12).map(p =>\n '<div class=\"entangle-pair\"><span>'+esc(p.source.key)+'</span><span class=\"entangle-arrow\">↔</span><span>'+esc(p.target.key)+'</span></div>'\n ).join('')+'</div>';\n return '<div class=\"card-title\">'+icons.entangle+'Entanglement'+\n '<span class=\"title-aside\">'+esc(unique.length)+' pair'+(unique.length===1?'':'s')+'</span></div>'+ inner;\n }\n\n function renderTunnelsCard(s){\n const tunnels=s.tunnels||[];\n const inner = !tunnels.length\n ? '<div class=\"empty\">No active tunnels.<span class=\"empty-cta\">$ qring tunnel create &lt;value&gt;</span></div>'\n : '<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 return '<div class=\"card-title\">'+icons.tunnel+'Quantum Tunnels'+\n '<span class=\"title-aside\">'+esc(tunnels.length)+' live</span></div>'+ inner;\n }\n\n function renderApprovalsCard(s){\n const list=s.approvals||[];\n const inner = !list.length\n ? '<div class=\"empty\">No active approvals.<span class=\"empty-cta\">$ qring approve KEY --for 1800 --reason \"...\"</span></div>'\n : '<div class=\"approval-list\">'+list.slice(0,8).map(a => {\n const cls = a.tampered ? 'tampered' : (a.secondsRemaining < 300 ? 'expiring' : '');\n return '<div class=\"approval-card '+cls+'\">'+\n '<div class=\"approval-head\"><span>'+esc(a.key)+(a.tampered?' ⚠ TAMPERED':'')+'</span><span>'+esc(fmtDuration(a.secondsRemaining))+' left</span></div>'+\n '<div class=\"approval-reason\">'+esc(a.reason||'(no reason)')+'</div>'+\n '<div class=\"approval-meta\"><span>scope:'+esc(a.scope)+'</span><span>by:'+esc(a.grantedBy)+'</span></div>'+\n '</div>';\n }).join('')+'</div>';\n return '<div class=\"card-title\">'+icons.approve+'Approvals'+\n '<span class=\"title-aside\">'+esc(list.filter(a=>a.valid).length)+' valid</span></div>'+ inner;\n }\n\n function renderHooksCard(s){\n const list=s.hooks||[];\n const inner = !list.length\n ? '<div class=\"empty\">No hooks registered.<span class=\"empty-cta\">$ qring hook add --tag payments --action rotate --exec \"...\"</span></div>'\n : '<div class=\"hooks-list\">'+list.slice(0,8).map(h =>\n '<div class=\"hook-row '+(h.enabled?'':'disabled')+'\">'+\n '<span class=\"hook-type '+esc(h.type)+'\">'+esc(h.type)+'</span>'+\n '<span class=\"hook-summary\" title=\"'+esc(h.matchSummary)+'\">'+esc(h.description||h.matchSummary)+'</span>'+\n '<span class=\"hook-id\">'+esc(h.id)+'</span>'+\n '</div>'\n ).join('')+'</div>';\n return '<div class=\"card-title\">'+icons.hook+'Hooks'+\n '<span class=\"title-aside\">'+esc(list.filter(h=>h.enabled).length)+' enabled / '+esc(list.length)+'</span></div>'+ inner;\n }\n\n function renderMemoryCard(s){\n const inner = '<div class=\"empty\" style=\"display:flex;align-items:baseline;gap:8px;font-style:normal\">'+\n '<span style=\"font-size:1.6rem;font-weight:700;color:var(--text-primary);font-family:var(--font-display)\">'+esc(s.memoryKeys||0)+'</span>'+\n '<span>memory key'+(s.memoryKeys===1?'':'s')+' encrypted at <code>~/.config/q-ring/agent-memory.enc</code></span>'+\n '</div>';\n return '<div class=\"card-title\">'+icons.memory+'Agent Memory</div>'+ inner;\n }\n\n function renderAnomalies(s){\n const list=s.anomalies||[];\n if(!list.length) return '<div class=\"card-title\">'+icons.anomaly+'Anomaly Alerts<span class=\"title-aside\" style=\"color:var(--green)\">all clear</span></div>'+\n '<div class=\"empty\">No anomalies detected — quantum field is stable.</div>';\n return '<div class=\"card-title\">'+icons.anomaly+'Anomaly Alerts'+\n '<span class=\"title-aside\" style=\"color:var(--danger)\">'+esc(list.length)+' active</span></div>'+\n '<div class=\"anomaly-list\">'+list.map(a=>{\n const hint = a.type==='tampered' ? '$ qring audit:verify' :\n a.type==='burst' ? '$ qring audit --key … --action read' :\n a.type==='unusual-hour' ? '$ qring audit --action read --since \"1am\"' :\n '$ qring audit --limit 100';\n return '<div class=\"anomaly-card\"><div class=\"anomaly-type\">'+esc(a.type)+'</div><div class=\"anomaly-desc\">'+esc(a.description)+'</div><div class=\"anomaly-hint\">'+esc(hint)+'</div></div>';\n }).join('')+'</div>';\n }\n\n /* --- Secrets table --- */\n\n function renderSecretsSection(){\n const s = state.snapshot;\n if(!s) return '';\n const filtered = filterSecrets(s.secrets||[]);\n sortRows(filtered, state.secretsSort);\n\n return '<div class=\"card grid-wide\" style=\"margin-bottom:16px\">'+\n '<div class=\"card-title\">'+icons.key+'Secrets'+\n '<span class=\"title-aside\">'+filtered.length+' of '+(s.secrets||[]).length+'</span></div>'+\n '<div class=\"secrets-toolbar\">'+\n '<label class=\"search\">'+icons.search+'<input id=\"secretsSearch\" type=\"search\" placeholder=\"Filter by key, scope, env, tag, provider…\" autocomplete=\"off\" value=\"'+esc(state.secretsQuery)+'\"/></label>'+\n '<button class=\"btn\" type=\"button\" data-quick=\"expired\">'+esc((s.health.expired||0))+' expired</button>'+\n '<button class=\"btn\" type=\"button\" data-quick=\"stale\">'+esc((s.health.stale||0))+' stale</button>'+\n '<button class=\"btn\" type=\"button\" data-quick=\"protected\">'+esc(s.protectedCount||0)+' protected</button>'+\n '<button class=\"btn\" type=\"button\" data-quick=\"\">clear</button>'+\n '</div>'+\n '<div class=\"secrets-table-wrap\">'+ renderTable(filtered) +'</div>'+\n '</div>';\n }\n\n function renderTable(rows){\n if(!rows.length) return '<div class=\"empty\" style=\"padding:18px;text-align:center\">No secrets match the filter.</div>';\n const cols = [\n { id:'key', label:'Key' },\n { id:'scope', label:'Scope' },\n { id:'env', label:'Env' },\n { id:'type', label:'Type' },\n { id:'decay', label:'Decay' },\n { id:'tags', label:'Tags' },\n { id:'lastAccessedAt', label:'Last read' }\n ];\n const sort = state.secretsSort;\n const head = '<thead><tr>'+cols.map(c =>\n '<th data-sort=\"'+c.id+'\" class=\"'+(sort.col===c.id?'sorted':'')+'\">'+esc(c.label)+'<span class=\"sort-arrow\">'+(sort.dir==='asc'?'▲':'▼')+'</span></th>'\n ).join('')+'</tr></thead>';\n const body = '<tbody>'+rows.map(r=>{\n const env = r.type==='superposition'\n ? (r.environments||[]).map(e=>'<span class=\"env-pill '+envClass(e)+'\" style=\"margin-right:3px\">'+esc(e)+(e===r.defaultEnv?' ✓':'')+'</span>').join('')\n : '<span class=\"env-pill env-default\">—</span>';\n const decayHtml = r.decay && r.decay.timeRemaining\n ? (function(){\n const pct=Math.min(r.decay.lifetimePercent,100);\n const col=decayColor(pct,r.decay.isExpired);\n return '<span class=\"mini-decay\"><span class=\"mini-decay-bar\"><span class=\"mini-decay-fill\" style=\"width:'+pct+'%;background:'+col+'\"></span></span><span class=\"mini-decay-time\" style=\"color:'+col+'\">'+esc(r.decay.isExpired?'expired':r.decay.timeRemaining)+'</span></span>';\n })()\n : '<span class=\"mini-decay-time\">—</span>';\n const tags = (r.tags||[]).slice(0,3).map(t=>'<span class=\"tag-pill\">'+esc(t)+'</span>').join('') + ((r.tags||[]).length>3?'+'+((r.tags||[]).length-3):'');\n const protectedIcon = r.requiresApproval ? '<span class=\"protected-icon\" title=\"requires approval for MCP read\">'+icons.lock+'</span>' : '';\n return '<tr>'+\n '<td class=\"col-key\" title=\"'+esc(r.key)+'\">'+esc(r.key)+protectedIcon+'</td>'+\n '<td><span class=\"scope-tag '+esc(r.scope)+'\">'+esc(r.scope)+'</span></td>'+\n '<td>'+env+'</td>'+\n '<td><span class=\"type-tag '+esc(r.type)+'\">'+esc(r.type)+'</span></td>'+\n '<td class=\"col-decay\">'+decayHtml+'</td>'+\n '<td class=\"col-tags\">'+tags+'</td>'+\n '<td>'+esc(fmtRelative(r.lastAccessedAt))+'</td>'+\n '</tr>';\n }).join('')+'</tbody>';\n return '<table class=\"secrets-table\">'+head+body+'</table>';\n }\n\n function filterSecrets(rows){\n const q=state.secretsQuery.trim().toLowerCase();\n if(!q) return rows.slice();\n if(q==='expired') return rows.filter(r=>r.decay && r.decay.isExpired);\n if(q==='stale') return rows.filter(r=>r.decay && r.decay.isStale && !r.decay.isExpired);\n if(q==='protected') return rows.filter(r=>r.requiresApproval);\n return rows.filter(r=>{\n const hay = [r.key, r.scope, r.type, r.provider||'', r.defaultEnv||'',\n (r.environments||[]).join(' '), (r.tags||[]).join(' ')].join(' ').toLowerCase();\n return hay.indexOf(q)!==-1;\n });\n }\n\n function sortRows(rows, sort){\n const dir = sort.dir==='asc' ? 1 : -1;\n rows.sort((a,b)=>{\n let av, bv;\n switch(sort.col){\n case 'key': av=a.key; bv=b.key; break;\n case 'scope': av=a.scope; bv=b.scope; break;\n case 'env': av=a.defaultEnv||(a.environments||[])[0]||''; bv=b.defaultEnv||(b.environments||[])[0]||''; break;\n case 'type': av=a.type; bv=b.type; break;\n case 'decay': av=a.decay && a.decay.secondsRemaining!=null?a.decay.secondsRemaining:Infinity; bv=b.decay && b.decay.secondsRemaining!=null?b.decay.secondsRemaining:Infinity; break;\n case 'tags': av=(a.tags||[]).join(','); bv=(b.tags||[]).join(','); break;\n case 'lastAccessedAt':\n default: av=a.lastAccessedAt?new Date(a.lastAccessedAt).getTime():0; bv=b.lastAccessedAt?new Date(b.lastAccessedAt).getTime():0;\n }\n if(av<bv) return -1*dir;\n if(av>bv) return 1*dir;\n return 0;\n });\n }\n\n /* --- Audit feed --- */\n\n function renderAuditCard(s){\n const events = s.audit || [];\n const m = s.auditMetrics || { byAction:{}, bySource:{} };\n const filtered = events.filter(e =>{\n if(state.auditFilter.action && e.action !== state.auditFilter.action) return false;\n if(state.auditFilter.source && e.source !== state.auditFilter.source) return false;\n const txt=state.auditFilter.text.trim().toLowerCase();\n if(txt){\n const hay = [e.key||'', e.action, e.source, e.detail||'', e.scope||'', e.env||''].join(' ').toLowerCase();\n if(hay.indexOf(txt)===-1) return false;\n }\n return true;\n });\n\n const allActions = ['read','write','delete','rotate','generate','entangle','tunnel','teleport','approve','revoke','policy_deny','collapse','export'];\n const actionChips = allActions.filter(a=>m.byAction[a])\n .map(a=>'<span class=\"audit-chip '+(state.auditFilter.action===a?'active':'')+'\" data-act=\"'+a+'\">'+a+'<strong>'+m.byAction[a]+'</strong></span>')\n .join('');\n const allSources = ['cli','mcp','agent','ci','hook','api'];\n const sourceChips = allSources.filter(src=>m.bySource[src])\n .map(src=>'<span class=\"audit-chip '+(state.auditFilter.source===src?'active':'')+'\" data-src=\"'+src+'\">'+src+'<strong>'+m.bySource[src]+'</strong></span>')\n .join('');\n\n const feed = !filtered.length\n ? '<div class=\"empty\">No audit events match the filter.</div>'\n : '<div class=\"audit-feed\">'+filtered.slice(0,80).map(e =>\n '<div class=\"audit-row\">'+\n '<span class=\"audit-ts\" title=\"'+esc(e.timestamp)+'\">'+esc(fmtTime(e.timestamp))+'</span>'+\n '<span class=\"audit-action '+esc(e.action)+'\">'+esc(e.action)+'</span>'+\n '<span class=\"audit-source\">'+esc(e.source)+'</span>'+\n '<span><span class=\"audit-key\">'+esc(e.key||'—')+'</span> <span class=\"audit-detail\">'+esc(e.detail||'')+'</span></span>'+\n '</div>'\n ).join('')+'</div>';\n\n return '<div class=\"card-title\">'+icons.audit+'Audit Log (24h)'+\n '<span class=\"title-aside\">'+esc(filtered.length)+' of '+esc(events.length)+' shown · window '+esc((s.auditMetrics?.windowSeconds||86400)/3600)+'h</span></div>'+\n '<div class=\"audit-actions-strip\">'+(actionChips||'<span class=\"empty\" style=\"padding:0\">no actions</span>')+'</div>'+\n (sourceChips ? '<div class=\"audit-actions-strip\">'+sourceChips+'</div>' : '')+\n '<div class=\"audit-toolbar\">'+\n '<label class=\"search\">'+icons.search+'<input id=\"auditSearch\" type=\"search\" placeholder=\"Filter by key, detail, scope…\" autocomplete=\"off\" value=\"'+esc(state.auditFilter.text)+'\"/></label>'+\n '<button class=\"btn\" type=\"button\" id=\"auditClear\">clear</button>'+\n '</div>'+ feed;\n }\n\n /* --- Top-level layout render --- */\n\n /**\n * Replace innerHTML on a host element while preserving the focused\n * input/textarea inside it, including caret position. Without this guard\n * each 5-second SSE snapshot would reset whatever the user is typing in\n * the secrets / audit search boxes.\n */\n function setHtmlPreservingFocus(host, html){\n const active = document.activeElement;\n const insideHost = active && host.contains(active);\n let focusId, selStart, selEnd, scrollTop;\n if(insideHost && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')){\n focusId = active.id;\n try { selStart = active.selectionStart; selEnd = active.selectionEnd; } catch(_) { /* type lacks selection */ }\n }\n if(insideHost){\n const scroller = active && active.closest && active.closest('.audit-feed,.secrets-table-wrap,.decay-list,.super-list,.entangle-list,.tunnel-list,.approval-list,.hooks-list');\n if(scroller) scrollTop = scroller.scrollTop;\n }\n host.innerHTML = html;\n if(focusId){\n const next = document.getElementById(focusId);\n if(next){\n next.focus();\n try { if(selStart != null) next.setSelectionRange(selStart, selEnd ?? selStart); } catch(_) { /* ignore */ }\n }\n }\n if(scrollTop != null){\n const scroller = host.querySelector('.audit-feed,.secrets-table-wrap,.decay-list,.super-list,.entangle-list,.tunnel-list,.approval-list,.hooks-list');\n if(scroller) scroller.scrollTop = scrollTop;\n }\n }\n\n function render(snap){\n state.snapshot = snap;\n renderHeaderMeta(snap);\n renderKpis(snap);\n\n setHtmlPreservingFocus($('overviewGrid'), [\n '<div class=\"card\" style=\"animation-delay:0ms\">'+renderHealthCard(snap)+'</div>',\n '<div class=\"card\" style=\"animation-delay:60ms\">'+renderEnvCard(snap)+'</div>',\n '<div class=\"card\" style=\"animation-delay:120ms\">'+renderManifestCard(snap)+'</div>',\n '<div class=\"card\" style=\"animation-delay:180ms\">'+renderPolicyCard(snap)+'</div>'\n ].join(''));\n\n setHtmlPreservingFocus($('secretsSection'), renderSecretsSection());\n\n setHtmlPreservingFocus($('quantumGrid'), [\n '<div class=\"card\" style=\"animation-delay:0ms\">'+renderDecayCard(snap)+'</div>',\n '<div class=\"card\" style=\"animation-delay:60ms\">'+renderSuperpositionCard(snap)+'</div>',\n '<div class=\"card\" style=\"animation-delay:120ms\">'+renderEntanglementCard(snap)+'</div>',\n '<div class=\"card\" style=\"animation-delay:180ms\">'+renderTunnelsCard(snap)+'</div>'\n ].join(''));\n\n setHtmlPreservingFocus($('govGrid'), [\n '<div class=\"card\" style=\"animation-delay:0ms\">'+renderApprovalsCard(snap)+'</div>',\n '<div class=\"card\" style=\"animation-delay:60ms\">'+renderHooksCard(snap)+'</div>',\n '<div class=\"card\" style=\"animation-delay:120ms\">'+renderMemoryCard(snap)+'</div>'\n ].join(''));\n\n setHtmlPreservingFocus($('anomalyAuditGrid'), [\n '<div class=\"card grid-wide\" style=\"animation-delay:0ms\">'+renderAnomalies(snap)+'</div>',\n '<div class=\"card grid-wide\" style=\"animation-delay:60ms\">'+renderAuditCard(snap)+'</div>'\n ].join(''));\n\n bindDelegated();\n\n $('lastUpdate').textContent = 'updated '+fmtRelative(snap.timestamp);\n const m = snap.auditMetrics || { total:0 };\n $('footMeta').textContent = 'q-ring v'+snap.version+' · snapshot '+fmtTime(snap.timestamp)+' · '+m.total+' audit events in last 24h';\n }\n\n /* --- Event delegation --- */\n\n let bound = false;\n function bindDelegated(){\n if(bound) return;\n bound = true;\n\n document.addEventListener('input', (e)=>{\n const t = e.target;\n if(t.id === 'secretsSearch'){\n state.secretsQuery = t.value;\n setHtmlPreservingFocus($('secretsSection'), renderSecretsSection());\n } else if(t.id === 'auditSearch'){\n state.auditFilter.text = t.value;\n rerenderAudit();\n }\n });\n\n document.addEventListener('click', (e)=>{\n const t = e.target.closest('[data-quick],[data-act],[data-src],th[data-sort],#auditClear');\n if(!t) return;\n if(t.matches('[data-quick]')){\n state.secretsQuery = t.getAttribute('data-quick');\n setHtmlPreservingFocus($('secretsSection'), renderSecretsSection());\n } else if(t.matches('[data-act]')){\n const a = t.getAttribute('data-act');\n state.auditFilter.action = state.auditFilter.action === a ? '' : a;\n rerenderAudit();\n } else if(t.matches('[data-src]')){\n const src = t.getAttribute('data-src');\n state.auditFilter.source = state.auditFilter.source === src ? '' : src;\n rerenderAudit();\n } else if(t.matches('th[data-sort]')){\n const col = t.getAttribute('data-sort');\n if(state.secretsSort.col === col){\n state.secretsSort.dir = state.secretsSort.dir === 'asc' ? 'desc' : 'asc';\n } else {\n state.secretsSort = { col, dir: col==='key' ? 'asc' : 'desc' };\n }\n setHtmlPreservingFocus($('secretsSection'), renderSecretsSection());\n } else if(t.id === 'auditClear'){\n state.auditFilter = { action:'', source:'', text:'' };\n rerenderAudit();\n }\n });\n }\n\n function rerenderAudit(){\n if(!state.snapshot) return;\n setHtmlPreservingFocus($('anomalyAuditGrid'), [\n '<div class=\"card grid-wide\">'+renderAnomalies(state.snapshot)+'</div>',\n '<div class=\"card grid-wide\">'+renderAuditCard(state.snapshot)+'</div>'\n ].join(''));\n }\n\n /* --- Pause / refresh / shortcuts --- */\n\n $('pauseBtn').addEventListener('click', togglePause);\n $('refreshBtn').addEventListener('click', forceRefresh);\n document.addEventListener('keydown', (e)=>{\n if(e.target && (e.target.tagName==='INPUT' || e.target.tagName==='TEXTAREA')){\n if(e.key==='Escape'){ e.target.blur(); }\n return;\n }\n if(e.key==='/'){\n e.preventDefault();\n const f=$('secretsSearch'); if(f) f.focus();\n } else if(e.key==='p' || e.key==='P'){\n togglePause();\n } else if(e.key==='r' || e.key==='R'){\n forceRefresh();\n }\n });\n\n function togglePause(){\n state.paused = !state.paused;\n $('pauseLabel').textContent = state.paused ? 'Resume' : 'Pause';\n $('connDot').classList.toggle('paused', state.paused);\n $('connText').textContent = state.paused ? 'paused' : 'live';\n }\n\n function forceRefresh(){\n fetch('/api/status', {cache:'no-store'}).then(r=>r.json()).then(s=>render(s)).catch(()=>{});\n }\n\n /* --- SSE connection --- */\n\n const dot = $('connDot');\n const connText = $('connText');\n\n function connect(){\n const es=new EventSource('/events');\n es.onopen=()=>{\n dot.classList.remove('disconnected');\n if(!state.paused){\n dot.classList.remove('paused');\n connText.textContent='live';\n }\n };\n es.onmessage=(e)=>{\n if(state.paused) return;\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…';\n };\n }\n\n connect();\n\n // refresh the relative timestamp every 5s so it stays accurate while paused\n setInterval(()=>{\n if(state.snapshot){\n $('lastUpdate').textContent = 'updated '+fmtRelative(state.snapshot.timestamp);\n }\n }, 5000);\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;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;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
@@ -1,8 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ PACKAGE_VERSION,
3
4
  checkDecay,
4
5
  checkExecPolicy,
5
6
  checkSSRF,
7
+ clearMemory,
6
8
  collapseEnvironment,
7
9
  deleteSecret,
8
10
  detectAnomalies,
@@ -13,6 +15,7 @@ import {
13
15
  exportAudit,
14
16
  exportSecrets,
15
17
  fireHooks,
18
+ forget,
16
19
  getEnvelope,
17
20
  getExecMaxRuntime,
18
21
  getPolicySummary,
@@ -22,12 +25,15 @@ import {
22
25
  httpRequest,
23
26
  listApprovals,
24
27
  listHooks,
28
+ listMemory,
25
29
  listSecrets,
26
30
  logAudit,
27
31
  queryAudit,
28
32
  readProjectConfig,
33
+ recall,
29
34
  registerHook,
30
35
  registry,
36
+ remember,
31
37
  removeHook,
32
38
  revokeApproval,
33
39
  setSecret,
@@ -36,28 +42,11 @@ import {
36
42
  tunnelList,
37
43
  tunnelRead,
38
44
  verifyAuditChain
39
- } from "./chunk-LLMO64FE.js";
45
+ } from "./chunk-A4RZVP3P.js";
40
46
 
41
47
  // src/cli/commands.ts
42
48
  import { Command, Help } from "commander";
43
49
 
44
- // src/version.ts
45
- import { readFileSync } from "fs";
46
- import { dirname, join } from "path";
47
- import { fileURLToPath } from "url";
48
- function readPackageVersion() {
49
- try {
50
- const here = dirname(fileURLToPath(import.meta.url));
51
- const pkgPath = join(here, "..", "package.json");
52
- const raw = readFileSync(pkgPath, "utf8");
53
- const pkg = JSON.parse(raw);
54
- return typeof pkg.version === "string" ? pkg.version : "0.0.0";
55
- } catch {
56
- return "0.0.0";
57
- }
58
- }
59
- var PACKAGE_VERSION = readPackageVersion();
60
-
61
50
  // src/utils/colors.ts
62
51
  var enabled = process.stdout.isTTY !== false && !process.env.NO_COLOR;
63
52
  function wrap(code, text) {
@@ -126,7 +115,7 @@ var SYMBOLS = {
126
115
  };
127
116
 
128
117
  // src/core/import.ts
129
- import { readFileSync as readFileSync2 } from "fs";
118
+ import { readFileSync } from "fs";
130
119
  function parseDotenv(content) {
131
120
  const result = /* @__PURE__ */ new Map();
132
121
  const lines = content.split(/\r?\n/);
@@ -158,7 +147,7 @@ function parseDotenv(content) {
158
147
  function importDotenv(filePathOrContent, options = {}) {
159
148
  let content;
160
149
  try {
161
- content = readFileSync2(filePathOrContent, "utf8");
150
+ content = readFileSync(filePathOrContent, "utf8");
162
151
  } catch {
163
152
  content = filePathOrContent;
164
153
  }
@@ -915,7 +904,7 @@ function getProjectContext(opts = {}) {
915
904
  }
916
905
 
917
906
  // src/cli/commands/project.ts
918
- import { writeFileSync, readFileSync as readFileSync3, existsSync } from "fs";
907
+ import { writeFileSync, readFileSync as readFileSync2, existsSync } from "fs";
919
908
  function registerProjectCommands(program2) {
920
909
  program2.command("context").alias("describe").description(
921
910
  "Show safe, redacted project context for AI agents (no secret values exposed)"
@@ -1183,7 +1172,7 @@ ${SYMBOLS.zap} ${c.bold(`Setting up service: ${name}`)}
1183
1172
  let config = {};
1184
1173
  try {
1185
1174
  if (existsSync(manifestPath)) {
1186
- config = JSON.parse(readFileSync3(manifestPath, "utf8"));
1175
+ config = JSON.parse(readFileSync2(manifestPath, "utf8"));
1187
1176
  }
1188
1177
  } catch {
1189
1178
  }
@@ -1908,8 +1897,8 @@ async function execCommand(opts) {
1908
1897
  }
1909
1898
 
1910
1899
  // src/core/scan.ts
1911
- import { readFileSync as readFileSync4, readdirSync, statSync } from "fs";
1912
- import { join as join2 } from "path";
1900
+ import { readFileSync as readFileSync3, readdirSync, statSync } from "fs";
1901
+ import { join } from "path";
1913
1902
 
1914
1903
  // src/core/secrets-detect.ts
1915
1904
  var SECRET_ASSIGNMENT_PATTERN = /((?:api_?key|secret|token|password|auth|credential|access_?key)[a-z0-9_]*)\s*[:=]\s*(['"])([^'"]+)\2/gi;
@@ -2003,7 +1992,7 @@ function scanCodebase(dir) {
2003
1992
  }
2004
1993
  for (const entry of entries) {
2005
1994
  if (IGNORE_DIRS.has(entry)) continue;
2006
- const fullPath = join2(currentDir, entry);
1995
+ const fullPath = join(currentDir, entry);
2007
1996
  let stat;
2008
1997
  try {
2009
1998
  stat = statSync(fullPath);
@@ -2017,7 +2006,7 @@ function scanCodebase(dir) {
2017
2006
  if (IGNORE_EXTS.has(ext) || entry.endsWith(".lock")) continue;
2018
2007
  let content;
2019
2008
  try {
2020
- content = readFileSync4(fullPath, "utf8");
2009
+ content = readFileSync3(fullPath, "utf8");
2021
2010
  } catch {
2022
2011
  continue;
2023
2012
  }
@@ -2047,7 +2036,7 @@ function scanCodebase(dir) {
2047
2036
  }
2048
2037
 
2049
2038
  // src/core/linter.ts
2050
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
2039
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
2051
2040
  import { basename, extname } from "path";
2052
2041
  var ENV_REF_BY_EXT = {
2053
2042
  ".ts": (k) => `process.env.${k}`,
@@ -2078,7 +2067,7 @@ function lintFiles(files, opts = {}) {
2078
2067
  if (!existsSync2(file)) continue;
2079
2068
  let content;
2080
2069
  try {
2081
- content = readFileSync5(file, "utf8");
2070
+ content = readFileSync4(file, "utf8");
2082
2071
  } catch {
2083
2072
  continue;
2084
2073
  }
@@ -2262,7 +2251,7 @@ function registerToolingCommands(program2) {
2262
2251
  }
2263
2252
  });
2264
2253
  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) => {
2265
- const { startDashboardServer } = await import("./dashboard-CMXRH4AQ.js");
2254
+ const { startDashboardServer } = await import("./dashboard-OXUD62RV.js");
2266
2255
  const { exec } = await import("child_process");
2267
2256
  const { platform } = await import("os");
2268
2257
  const port = Number(cmd.port);
@@ -2437,8 +2426,8 @@ ${SYMBOLS.warning} ${c.bold(c.yellow(`${anomalies.length} anomaly/anomalies dete
2437
2426
 
2438
2427
  // src/hooks/precommit.ts
2439
2428
  import { execSync } from "child_process";
2440
- import { existsSync as existsSync3, writeFileSync as writeFileSync4, chmodSync, readFileSync as readFileSync6, unlinkSync } from "fs";
2441
- import { join as join3 } from "path";
2429
+ import { existsSync as existsSync3, writeFileSync as writeFileSync4, chmodSync, readFileSync as readFileSync5, unlinkSync } from "fs";
2430
+ import { join as join2 } from "path";
2442
2431
  function getStagedFiles() {
2443
2432
  try {
2444
2433
  const output = execSync("git diff --cached --name-only --diff-filter=ACM", {
@@ -2473,13 +2462,13 @@ exit $?
2473
2462
  `;
2474
2463
  function installPreCommitHook(repoPath) {
2475
2464
  const root = repoPath ?? process.cwd();
2476
- const hooksDir = join3(root, ".git", "hooks");
2477
- if (!existsSync3(join3(root, ".git"))) {
2465
+ const hooksDir = join2(root, ".git", "hooks");
2466
+ if (!existsSync3(join2(root, ".git"))) {
2478
2467
  return { installed: false, path: "", message: "Not a git repository" };
2479
2468
  }
2480
- const hookPath = join3(hooksDir, "pre-commit");
2469
+ const hookPath = join2(hooksDir, "pre-commit");
2481
2470
  if (existsSync3(hookPath)) {
2482
- const existing = readFileSync6(hookPath, "utf8");
2471
+ const existing = readFileSync5(hookPath, "utf8");
2483
2472
  if (existing.includes("q-ring")) {
2484
2473
  return { installed: true, path: hookPath, message: "Hook already installed" };
2485
2474
  }
@@ -2492,9 +2481,9 @@ function installPreCommitHook(repoPath) {
2492
2481
  }
2493
2482
  function uninstallPreCommitHook(repoPath) {
2494
2483
  const root = repoPath ?? process.cwd();
2495
- const hookPath = join3(root, ".git", "hooks", "pre-commit");
2484
+ const hookPath = join2(root, ".git", "hooks", "pre-commit");
2496
2485
  if (!existsSync3(hookPath)) return false;
2497
- const content = readFileSync6(hookPath, "utf8");
2486
+ const content = readFileSync5(hookPath, "utf8");
2498
2487
  if (!content.includes("q-ring")) return false;
2499
2488
  const lines = content.split("\n");
2500
2489
  const cleaned = lines.filter(
@@ -2803,123 +2792,6 @@ ${c.dim("q-ring agent stopped")}`);
2803
2792
  });
2804
2793
  }
2805
2794
 
2806
- // src/core/memory.ts
2807
- import { existsSync as existsSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5, mkdirSync } from "fs";
2808
- import { join as join4 } from "path";
2809
- import { homedir, hostname, userInfo } from "os";
2810
- import { createCipheriv as createCipheriv2, createDecipheriv as createDecipheriv2, createHash, randomBytes as randomBytes3 } from "crypto";
2811
- import { Entry } from "@napi-rs/keyring";
2812
- var MEMORY_FILE = "agent-memory.enc";
2813
- var KEYRING_SERVICE = "qring-memory-key";
2814
- var KEYRING_ACCOUNT = "encryption-key";
2815
- function getMemoryDir() {
2816
- const dir = join4(homedir(), ".config", "q-ring");
2817
- if (!existsSync4(dir)) {
2818
- mkdirSync(dir, { recursive: true });
2819
- }
2820
- return dir;
2821
- }
2822
- function getMemoryPath() {
2823
- return join4(getMemoryDir(), MEMORY_FILE);
2824
- }
2825
- function deriveLegacyKey() {
2826
- const fingerprint = `qring-memory:${hostname()}:${userInfo().username}`;
2827
- return createHash("sha256").update(fingerprint).digest();
2828
- }
2829
- function getOrCreateKey() {
2830
- try {
2831
- const entry = new Entry(KEYRING_SERVICE, KEYRING_ACCOUNT);
2832
- const stored = entry.getPassword();
2833
- if (stored) return Buffer.from(stored, "base64");
2834
- const key = randomBytes3(32);
2835
- entry.setPassword(key.toString("base64"));
2836
- return key;
2837
- } catch {
2838
- console.warn("[q-ring] OS keyring unavailable for memory key \u2014 falling back to machine-derived key");
2839
- return deriveLegacyKey();
2840
- }
2841
- }
2842
- function encryptWith(data, key) {
2843
- const iv = randomBytes3(12);
2844
- const cipher = createCipheriv2("aes-256-gcm", key, iv);
2845
- const encrypted = Buffer.concat([cipher.update(data, "utf8"), cipher.final()]);
2846
- const tag = cipher.getAuthTag();
2847
- return `${iv.toString("base64")}:${tag.toString("base64")}:${encrypted.toString("base64")}`;
2848
- }
2849
- function decryptWith(blob, key) {
2850
- const parts = blob.split(":");
2851
- if (parts.length !== 3) throw new Error("Invalid encrypted format");
2852
- const iv = Buffer.from(parts[0], "base64");
2853
- const tag = Buffer.from(parts[1], "base64");
2854
- const encrypted = Buffer.from(parts[2], "base64");
2855
- const decipher = createDecipheriv2("aes-256-gcm", key, iv);
2856
- decipher.setAuthTag(tag);
2857
- return decipher.update(encrypted) + decipher.final("utf8");
2858
- }
2859
- function encrypt(data) {
2860
- return encryptWith(data, getOrCreateKey());
2861
- }
2862
- function decrypt(blob) {
2863
- const key = getOrCreateKey();
2864
- try {
2865
- return decryptWith(blob, key);
2866
- } catch {
2867
- const legacy = deriveLegacyKey();
2868
- const plain = decryptWith(blob, legacy);
2869
- writeFileSync5(getMemoryPath(), encryptWith(plain, key), "utf8");
2870
- return plain;
2871
- }
2872
- }
2873
- function loadStore() {
2874
- const path = getMemoryPath();
2875
- if (!existsSync4(path)) {
2876
- return { entries: {} };
2877
- }
2878
- try {
2879
- const raw = readFileSync7(path, "utf8");
2880
- const decrypted = decrypt(raw);
2881
- return JSON.parse(decrypted);
2882
- } catch {
2883
- return { entries: {} };
2884
- }
2885
- }
2886
- function saveStore(store) {
2887
- const json = JSON.stringify(store);
2888
- const encrypted = encrypt(json);
2889
- writeFileSync5(getMemoryPath(), encrypted, "utf8");
2890
- }
2891
- function remember(key, value) {
2892
- const store = loadStore();
2893
- store.entries[key] = {
2894
- value,
2895
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2896
- };
2897
- saveStore(store);
2898
- }
2899
- function recall(key) {
2900
- const store = loadStore();
2901
- return store.entries[key]?.value ?? null;
2902
- }
2903
- function listMemory() {
2904
- const store = loadStore();
2905
- return Object.entries(store.entries).map(([key, entry]) => ({
2906
- key,
2907
- updatedAt: entry.updatedAt
2908
- }));
2909
- }
2910
- function forget(key) {
2911
- const store = loadStore();
2912
- if (key in store.entries) {
2913
- delete store.entries[key];
2914
- saveStore(store);
2915
- return true;
2916
- }
2917
- return false;
2918
- }
2919
- function clearMemory() {
2920
- saveStore({ entries: {} });
2921
- }
2922
-
2923
2795
  // src/cli/commands/agent.ts
2924
2796
  function registerAgentCommands(program2) {
2925
2797
  program2.command("remember <key> <value>").description(