<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Genesis Neural Monitor</title>
<link rel="icon" type="image/svg+xml" href="/public/favicon.svg">
<link rel="stylesheet" href="/index.css">
<style>
:root { --color-text-secondary: var(--color-text-muted, #d4d4d4e4); }
html, body { overflow: auto !important; position: static !important; height: auto !important; }
body { overscroll-behavior: auto !important; }
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
    background: var(--color-background, #0f172a);
    color: var(--color-text, #e2e8f0);
    font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
    font-size: 14px;
    line-height: 1.5;
}
.header {
    display: flex; justify-content: space-between; align-items: center;
    padding: 16px 24px; border-bottom: 1px solid var(--color-border, #1e293b);
}
.header h1 { font-size: 18px; font-weight: 600; letter-spacing: 2.5px; color: var(--color-text-secondary); }
.header .meta { color: var(--color-text-secondary); font-size: 12px; display: flex; align-items: center; gap: 16px; }
.header-badge {
    display: inline-flex; align-items: center; gap: 6px; padding: 2px 10px;
    border-radius: 4px; font-size: 11px; font-weight: 600; letter-spacing: 0.5px;
}
.header-badge .hb-dot { width: 6px; height: 6px; border-radius: 50%; }

/* Layout: main panel + side panel */
.nm-container {
    display: flex; min-height: calc(100vh - 92px);
}
.main-panel {
    flex: 1; padding: 0 24px 60px; overflow-y: auto;
}
.side-panel {
    width: 380px; min-width: 380px; border-left: 1px solid var(--color-border, #1e293b);
    padding: 18px; overflow-y: auto;
    scrollbar-width: thin; scrollbar-color: #334155 transparent;
}
.section {
    margin-bottom: 20px; padding: 10px 12px; border-radius: 6px;
    background: rgba(30, 41, 59, 0.3); border: 1px solid rgba(51, 65, 85, 0.3);
}
.section h3 {
    font-size: 12px; text-transform: uppercase; letter-spacing: 1.2px;
    color: var(--color-text-secondary); margin-bottom: 10px; padding-bottom: 6px;
    border-bottom: 1px solid var(--color-border, #1e293b);
    border-left: 3px solid var(--color-accent, #27875e); padding-left: 8px;
}
.detail-panel { display: none; }
.detail-panel.active { display: block; }

/* Grid topology */
.monitor-grid {
    display: flex; flex-direction: column; gap: 20px;
    max-width: 1200px; margin: 0 auto; padding: 16px;
}

/* Subsystem region */
.region {
    background: var(--color-panel, #1e293b); border: 1px solid var(--color-border, #334155);
    border-radius: 8px; padding: 14px 16px; border-left: 3px solid #475569;
}
.region-header {
    display: flex; align-items: center; gap: 8px; margin-bottom: 10px;
    padding-bottom: 6px; border-bottom: 1px solid var(--color-border, #334155);
}
.region-header h3 {
    font-size: 12px; text-transform: uppercase; letter-spacing: 1px;
    color: var(--color-text-secondary); margin: 0; flex: 1;
}
.region-header .agg-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.region-count { font-size: 10px; color: #64748b; }
.region-desc { font-size: 10px; color: #64748b; margin-bottom: 8px; line-height: 1.3; font-style: italic; }

/* Dot grid within region */
.dot-grid {
    display: grid; grid-template-columns: repeat(4, 28px); gap: 16px;
    padding: 4px 0;
}
.grid-dot {
    width: 14px; height: 14px; border-radius: 50%; cursor: pointer;
    transition: transform 0.15s, box-shadow 0.2s;
    position: relative;
}
.grid-dot:hover { transform: scale(1.8); z-index: 10; }
.grid-dot-healthy { background: #4ade80; box-shadow: 0 0 4px rgba(74,222,128,0.4); }
.grid-dot-active { background: #4ade80; box-shadow: 0 0 4px rgba(74,222,128,0.4); }
.grid-dot-degraded { background: #facc15; box-shadow: 0 0 4px rgba(250,204,21,0.4); }
.grid-dot-down { background: #ef4444; box-shadow: 0 0 6px rgba(239,68,68,0.5); animation: pulse-alert 1.5s ease-in-out infinite; }
.grid-dot-warning { background: #f59e0b; box-shadow: 0 0 4px rgba(245,158,11,0.4); }
.grid-dot-idle { background: rgba(85, 85, 85, 0.5); }
.grid-dot-disabled { background: rgba(85, 85, 85, 0.5); }
.grid-dot-unknown { background: rgba(85, 85, 85, 0.5); }

@keyframes pulse-alert {
    0%, 100% { transform: scale(1); opacity: 1; }
    50% { transform: scale(1.12); opacity: 0.85; }
}
.grid-dot:hover { animation: none; }

/* Shared tooltip (fixed position, page-level) */
#shared-tip {
    display: none; position: fixed; z-index: 9999;
    background: var(--color-panel, #1e293b);
    border: 1px solid var(--color-border, #334155); border-radius: 5px;
    padding: 8px 12px; font-size: 11px; pointer-events: none;
    min-width: 150px; max-width: 260px; word-wrap: break-word;
    box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
#shared-tip.visible { display: block; }
.dot-tip-name { color: var(--color-text); font-weight: 600; margin-bottom: 3px; }
.dot-tip-row { color: var(--color-text-secondary); font-size: 10px; }

/* Enrichment below dots */
.region-enrichment {
    margin-top: 8px; padding-top: 8px;
    border-top: 1px solid rgba(51,65,85,0.3);
}
.region-enrichment .section-row { padding: 2px 0; }
.region-enrichment .section-row .label,
.region-enrichment .section-row .value { font-size: 11px; }

/* Battleship-style flat dot grid — explicit 7×7 block layout */
.battleship-grid {
    display: grid;
    grid-template-columns: repeat(7, 72px);
    grid-template-rows: repeat(7, auto);
    gap: 4px; padding: 14px;
    background: rgba(15, 23, 42, 0.5);
    border-radius: 8px; border: 1px solid var(--color-border, #334155);
    align-self: center;
}
.bg-cell {
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    padding: 6px 4px; border-radius: 4px;
}
.bg-cell-empty { width: 72px; height: 72px; }
.bg-cell-label {
    font-size: 10px; color: #94a3b8; text-align: center;
    line-height: 1.2; width: 100%; word-break: break-word;
}
/* Per-subsystem tints */
.bg-ego        { background: rgba(192,132,252,0.08); }
.bg-reflection { background: rgba(129,140,248,0.08); }
.bg-memory     { background: rgba(56,189,248,0.08); }
.bg-surplus    { background: rgba(74,222,128,0.08); }
.bg-outreach   { background: rgba(251,191,36,0.08); }
.bg-executor   { background: rgba(167,139,250,0.08); }
.bg-learning   { background: rgba(251,146,60,0.08); }
.bg-contingency{ background: rgba(248,113,113,0.08); }
.bg-utility    { background: rgba(148,163,184,0.08); }

/* Enrichment cards grid below battleship */
.enrichment-cards {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;
}
.enrich-card {
    background: var(--color-panel, #1e293b); border: 1px solid var(--color-border, #334155);
    border-radius: 8px; padding: 12px 14px; border-left: 3px solid #475569;
}

/* CC badge */
.cc-badge {
    background: #60a5fa; color: #0f172a; font-size: 9px; padding: 1px 4px;
    border-radius: 3px; font-weight: 700; margin-right: 4px; display: inline-block;
}

/* Status dots (inline) */
.status-dot {
    display: inline-block; width: 8px; height: 8px; border-radius: 50%;
    margin-right: 6px; vertical-align: middle;
}
.status-healthy { background: #4ade80; }
.status-active { background: #4ade80; }
.status-degraded { background: #ffe600; }
.status-down { background: #ef4444; }
.status-idle { background: #888; }
.status-warning { background: #f59e0b; }
.status-unknown { background: #6b7280; }

.badge {
    display: inline-block; padding: 1px 6px; border-radius: 3px;
    font-size: 10px; font-weight: 600;
}
.badge-normal { background: #166534; color: #4ade80; }
.badge-warning { background: #854d0e; color: #ffe600; }
.badge-critical { background: #991b1b; color: #ef4444; }
.badge-info { background: #1e3a8a; color: #93c5fd; }
.badge-disabled { background: #374151; color: #9ca3af; }
.detail-status-reason { margin-bottom: 8px; }
.see-also-list { display: flex; flex-direction: column; gap: 4px; }
.see-also-item {
    cursor: pointer; color: #60a5fa; font-size: 12px;
    padding: 4px 8px; border-radius: 3px;
    background: rgba(96, 165, 250, 0.08);
    transition: background 0.15s;
}
.see-also-item:hover { background: rgba(96, 165, 250, 0.18); text-decoration: underline; }

/* Section rows (reused in ops strip, enrichment, detail panel) */
.section-row { display: flex; justify-content: space-between; align-items: center; padding: 4px 0; }
.section-row .label { color: var(--color-text-secondary); font-size: 13px; }
.section-row .value { font-weight: 600; font-size: 13px; }

/* Category health summary */
.cat-health { font-size: 11px; color: var(--color-text-secondary); padding: 2px 0; }
.cat-health .cat-name { text-transform: capitalize; }
.cat-health .cat-count { float: right; font-weight: 600; }

.detail-back {
    cursor: pointer; color: #60a5fa; font-size: 12px; margin-bottom: 12px;
    display: inline-block;
}
.detail-back:hover { text-decoration: underline; }
.detail-section { margin-top: 16px; }
.detail-section h4 {
    font-size: 11px; text-transform: uppercase; letter-spacing: 1px;
    color: #94a3b8; margin-bottom: 8px;
}
.chain-item {
    display: flex; align-items: center; gap: 8px;
    padding: 6px 0; border-bottom: 1px solid var(--color-border, #1e293b);
}
.chain-item:last-child { border-bottom: none; }

.detail-meta { font-size: 11px; color: var(--color-text-secondary); margin: 4px 0; }
.detail-meta strong { color: var(--color-text, #e2e8f0); }
.detail-desc { color: #94a3b8; font-size: 12px; margin: 8px 0 12px; line-height: 1.4; }
.detail-cat-badge {
    display: inline-block; padding: 2px 8px; border-radius: 4px;
    font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
    margin-bottom: 8px;
}

/* Routing config editor */
.config-editor {
    margin-top: 16px; padding: 12px; border: 1px solid var(--color-border, #334155);
    border-radius: 6px; background: rgba(15, 23, 42, 0.5);
}
.config-editor .chain-editor-list {
    list-style: none; padding: 0; margin: 8px 0;
}
.config-editor .chain-editor-item {
    display: flex; align-items: center; gap: 8px; padding: 6px 8px;
    background: rgba(30, 41, 59, 0.8); border-radius: 4px; margin-bottom: 4px;
    font-size: 12px;
}
.config-editor .chain-editor-item .provider-name { flex: 1; }
.config-editor .chain-editor-item .cb-dot {
    width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0;
}
.config-editor .chain-editor-item .move-btn,
.config-editor .chain-editor-item .remove-btn {
    background: none; border: none; cursor: pointer; font-size: 12px; padding: 2px 4px;
    color: #64748b; border-radius: 3px;
}
.config-editor .chain-editor-item .move-btn:hover { color: #e2e8f0; background: rgba(100,116,139,0.3); }
.config-editor .chain-editor-item .remove-btn:hover { color: #ef4444; background: rgba(239,68,68,0.15); }
.config-editor .editor-actions {
    display: flex; gap: 8px; margin-top: 8px; align-items: center;
}
.config-editor .editor-actions select {
    flex: 1; background: #1e293b; color: #e2e8f0; border: 1px solid #334155;
    border-radius: 4px; padding: 4px 8px; font-size: 11px;
}
.config-editor .editor-actions button {
    padding: 4px 12px; border-radius: 4px; font-size: 11px; cursor: pointer; border: none;
}
.config-editor .btn-add { background: #334155; color: #e2e8f0; }
.config-editor .btn-add:hover { background: #475569; }
.config-editor .btn-save { background: #22c55e; color: #0f172a; font-weight: 600; }
.config-editor .btn-save:hover { background: #16a34a; }
.config-editor .btn-save:disabled { background: #334155; color: #64748b; cursor: not-allowed; }
.config-editor .btn-revert { background: #334155; color: #94a3b8; }
.config-editor .btn-revert:hover { background: #475569; }
.config-editor .editor-toggles {
    display: flex; gap: 12px; margin-top: 8px; font-size: 11px; color: #94a3b8;
}
.config-editor .editor-toggles label { display: flex; align-items: center; gap: 4px; cursor: pointer; }
.config-editor .editor-feedback {
    font-size: 11px; margin-top: 6px; padding: 4px 8px; border-radius: 4px;
}
.config-editor .feedback-ok { background: rgba(34,197,94,0.15); color: #4ade80; }
.config-editor .feedback-err { background: rgba(239,68,68,0.15); color: #f87171; }

/* Surplus detail panel (expandable inside subsystem box) */
.surplus-detail-toggle {
    cursor: pointer; color: #4ade80; font-size: 11px; font-weight: 600;
    padding: 4px 0; display: flex; justify-content: space-between; align-items: center;
    margin-top: 8px; border-top: 1px solid rgba(74, 222, 128, 0.2); padding-top: 8px;
}
.surplus-detail-toggle:hover { text-decoration: underline; }
.surplus-detail-toggle .arrow { font-size: 8px; transition: transform 0.2s; }
.surplus-detail-toggle .arrow.open { transform: rotate(90deg); }
.surplus-detail-content { display: none; margin-top: 4px; }
.surplus-detail-content.open { display: block; }
.surplus-detail-content .detail-section {
    margin: 8px 0; padding: 6px 0; border-top: 1px solid rgba(51, 65, 85, 0.5);
}
.surplus-detail-content .detail-section h4 {
    font-size: 10px; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px;
    margin: 0 0 4px 0;
}
.surplus-task-row {
    display: flex; gap: 6px; align-items: center;
    font-size: 10px; color: var(--color-text-secondary); padding: 2px 0;
}
.surplus-task-row .task-type { flex: 1; }
.surplus-task-row .task-status { font-weight: 600; }
.surplus-task-row .task-time { color: #64748b; }
.follow-up-badge {
    display: inline-block; padding: 1px 6px; border-radius: 3px;
    font-size: 10px; font-weight: 600; margin-left: 4px;
}
.follow-up-badge.pending { background: rgba(250,204,21,0.15); color: #fbbf24; }
.follow-up-badge.failed { background: rgba(239,68,68,0.15); color: #ef4444; }
.follow-up-badge.blocked { background: rgba(251,146,60,0.15); color: #fb923c; }

/* Discarded items */
.discarded-toggle {
    cursor: pointer; color: #ef4444; font-size: 11px; font-weight: 600;
    padding: 4px 0; display: flex; justify-content: space-between; align-items: center;
    margin-top: 8px; border-top: 1px solid rgba(239, 68, 68, 0.2); padding-top: 8px;
}
.discarded-toggle:hover { text-decoration: underline; }
.discarded-toggle .arrow { font-size: 8px; transition: transform 0.2s; }
.discarded-toggle .arrow.open { transform: rotate(90deg); }
.discarded-details { display: none; margin-top: 4px; }
.discarded-details.open { display: block; }
.discarded-item {
    background: rgba(239, 68, 68, 0.06); border: 1px solid rgba(239, 68, 68, 0.15);
    border-radius: 4px; padding: 8px; margin: 4px 0; font-size: 11px;
}
.discarded-item .di-header { font-weight: 600; color: var(--color-text, #e2e8f0); margin-bottom: 4px; }
.discarded-item .di-row { color: var(--color-text-secondary); font-size: 10px; margin: 2px 0; }
.discarded-item .di-error { color: #ef4444; font-size: 10px; margin: 4px 0; }
.discarded-item .di-actions { text-align: right; margin-top: 6px; }
.clear-btn {
    background: rgba(239, 68, 68, 0.12); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.25);
    border-radius: 3px; padding: 2px 10px; font-size: 10px; cursor: pointer;
    font-family: inherit;
}
.clear-btn:hover { background: rgba(239, 68, 68, 0.25); }
.clear-all-btn { margin-top: 8px; width: 100%; padding: 4px 10px; }

/* Loading */
.loading { text-align: center; color: var(--color-text-secondary); padding: 40px; }

/* Nav bar */
.genesis-nav {
    display: flex; gap: 1.5rem; padding: 0.5rem 24px;
    border-bottom: 1px solid var(--color-border, #1e293b);
}
.genesis-nav a {
    color: var(--color-text-secondary); text-decoration: none; font-size: 0.85rem;
    padding: 0.3rem 0;
}
.genesis-nav a:hover { color: var(--color-text); }
.genesis-nav a.active { color: var(--color-accent, #27875e); border-bottom: 2px solid var(--color-accent, #27875e); }

/* Responsive */
@media (max-width: 1000px) {
    .nm-container { flex-direction: column; }
    .side-panel { width: 100%; min-width: 0; border-left: none; border-top: 1px solid var(--color-border, #1e293b); }
}
@media (max-width: 600px) {
    .monitor-grid { grid-template-columns: 1fr; }
}
</style>
</head>
<body>

<nav class="genesis-nav">
    <a href="/genesis">Dashboard</a>
    <a href="/genesis/logs">Event Log</a>
    <a href="/genesis/errors">Error Log</a>
    <a href="/genesis/monitor" class="active">Neural Monitor</a>
</nav>

<div class="header">
    <h1>GENESIS NEURAL MONITOR</h1>
    <div class="meta">
        <span id="hb-resilience" class="header-badge" style="display:none;"><span class="hb-dot"></span><span class="hb-text"></span></span>
        <span id="hb-cc" class="header-badge" style="display:none;"><span class="hb-dot"></span><span class="hb-text"></span></span>
        <span id="hb-sites" class="header-badge" style="display:none;"><span class="hb-text"></span></span>
        <span>Last update: <span id="last-update">--</span></span>
    </div>
</div>

<div class="nm-container">
    <div class="main-panel">
        <div id="loading" class="loading">Connecting...</div>
        <div id="monitor-grid" class="monitor-grid" style="display:none;">
            <div id="battleship-grid" class="battleship-grid"></div>
            <div id="enrichment-cards" class="enrichment-cards"></div>
        </div>
    </div>
    <div class="side-panel">
        <div id="side-default">
            <div class="section">
                <h3>Awareness Loop</h3>
                <div id="awareness-content"></div>
            </div>
            <div class="section">
                <h3>Services</h3>
                <div id="services-content"></div>
            </div>
            <div class="section">
                <h3>Infrastructure</h3>
                <div id="infra-content"></div>
            </div>
            <div class="section">
                <h3>Resilience</h3>
                <div id="resilience-content"></div>
            </div>
            <div class="section">
                <h3>G. Sessions</h3>
                <div id="cc-content"></div>
            </div>
            <div class="section">
                <h3>Queues</h3>
                <div id="queue-content"></div>
            </div>
            <div class="section">
                <h3>Cost</h3>
                <div id="cost-content"></div>
            </div>
        </div>
        <div id="side-detail" class="detail-panel">
            <span class="detail-back" id="detail-back-btn">&larr; Back</span>
            <div id="detail-content"></div>
        </div>
    </div>
</div>

<div id="shared-tip"></div>

<script>
// NOTE: All data rendered in this dashboard originates from Genesis's internal
// health API (local-only Flask server). No user-supplied or external content
// is rendered. All dynamic content uses textContent for plain text values
// and controlled DOM construction for structured elements.

// ── Call site metadata ─────────────────────────────────────────────────
// Sourced from docs/architecture/genesis-v3-model-routing-registry.md.
// The API also serves this metadata; JS copy provides instant render.

const SITE_META = {
    // ── Core (non-routing) — keep full metadata, backend doesn't provide these ──
    'cc_foreground': {
        name: 'G. Foreground', desc: 'Primary interactive conversation with the user. Always-on via CC subscription.',
        category: 'core', frequency: 'Continuous', costPolicy: 'Subscription',
    },
    'cc_background': {
        name: 'G. Background', desc: 'Autonomous background sessions for reflections, inbox, and surplus tasks.',
        category: 'core', frequency: 'On demand', costPolicy: 'Subscription',
    },
    // ── Routing call sites — name + category only (desc/frequency/costPolicy from backend) ──
    '3_micro_reflection': { name: 'Micro Reflect', category: 'reflection' },
    '4_light_reflection': { name: 'Light Reflect', category: 'reflection' },
    '5_deep_reflection': { name: 'Deep Reflect', category: 'reflection' },
    '6_strategic_reflection': { name: 'Strategic Reflect', category: 'reflection' },
    '9_fact_extraction': { name: 'Fact Extract', category: 'processing' },
    '12_surplus_brainstorm': { name: 'Brainstorm', category: 'content' },
    '14_weekly_self_assessment': { name: 'Self Assessment', category: 'reasoning' },
    '16_quality_calibration': { name: 'Output QA', category: 'calibration' },
    '21_embeddings': { name: 'Store Embed', category: 'embedding' },
    '21b_query_embedding': { name: 'Query Embed', category: 'embedding' },
    '23_outreach_review': { name: 'Outreach Review', category: 'assessment' },
    '29_retrospective_triage': { name: 'Depth Classifier', category: 'classification' },
    '30_triage_calibration': { name: 'Classifier Cal', category: 'calibration' },
    '31_outcome_classification': { name: 'Outcome Class', category: 'processing' },
    '32_delta_assessment': { name: 'State Delta', category: 'processing' },
    '35_content_draft': { name: 'Content', category: 'content' },
    '36_code_auditor': { name: 'Code Audit', category: 'surplus' },
    'cc_update_analysis': { name: 'CC Updates', category: 'processing' },
    'outreach_email_triage': { name: 'Email Filter', category: 'classification' },
    '13_morning_report': { name: 'Morning Rpt', category: 'content' },
    'bookmark_enrichment': { name: 'Bookmark Enrich', category: 'content' },
    '40_knowledge_distillation': { name: 'Knowledge Distill', category: 'learning' },
    '41_resume_review_pass1': { name: 'Resume Review P1', category: 'memory' },
    '42_resume_review_pass2': { name: 'Resume Review P2', category: 'memory' },
    // Partially wired
    '27_pre_execution_assessment': { name: 'Pre-Exec', category: 'reasoning' },
    '33_skill_refiner': { name: 'Skill Refine', category: 'content' },
    '38_procedure_extraction': { name: 'Procedure', category: 'content' },
    'contingency_foreground': { name: 'CC Fallback (FG)', category: 'reasoning' },
    'contingency_micro': { name: 'CC Fallback (Micro)', category: 'processing' },
    // Groundwork / disabled
    '2_triage': { name: 'Triage', category: 'classification' },
    '7_ego_cycle': { name: 'Ego Cycle', category: 'reasoning' },
    '7_task_retrospective': { name: 'Task Retro', category: 'processing' },
    '8_ego_compaction': { name: 'Ego Compact', category: 'processing' },
    '10_cognitive_state': { name: 'Cog State', category: 'reasoning' },
    '11_user_model_synthesis': { name: 'User Model', category: 'reasoning' },
    '17_executor_review': { name: 'Executor Review', category: 'assessment' },
    '18_meta_prompting': { name: 'Meta Prompt', category: 'calibration' },
    '20_adversarial_counterargument': { name: 'Adversarial', category: 'assessment' },
    '22_tagging': { name: 'Tagging', category: 'classification' },
    '28_observation_sweep': { name: 'Obs Sweep', category: 'processing' },
    '34_research_synthesis': { name: 'Research Synth', category: 'content' },
    '37_infrastructure_monitor': { name: 'Infra Monitor', category: 'surplus' },
    'autonomous_executor_reasoning': { name: 'Executor', category: 'reasoning' },
};

// Category colors (used in detail panel category badge)
const CAT_COLORS = {
    core: '#60a5fa', reflection: '#818cf8', reasoning: '#a78bfa',
    processing: '#fb923c', calibration: '#fbbf24', content: '#34d399',
    embedding: '#22d3ee', classification: '#94a3b8', assessment: '#f472b6',
    surplus: '#a3e635', learning: '#fb923c', memory: '#38bdf8',
};

let lastData = null;
let lastFetchTime = 0;

document.getElementById('detail-back-btn').addEventListener('click', showDefaultPanel);

function getMeta(siteId) {
    return SITE_META[siteId] || { name: siteId, desc: '', category: 'unknown', frequency: '', costPolicy: '' };
}

function gridDotClass(status) {
    const map = {
        healthy: 'grid-dot-healthy', active: 'grid-dot-active',
        degraded: 'grid-dot-degraded', down: 'grid-dot-down',
        warning: 'grid-dot-warning', idle: 'grid-dot-idle',
        disabled: 'grid-dot-disabled',
    };
    return map[status] || 'grid-dot-unknown';
}

function infraStatusClass(probe) {
    const status = typeof probe === 'string' ? probe : probe?.status;
    if (status === 'healthy' || status === 'active') return 'status-healthy';
    if (status === 'degraded' || status === 'warning') return 'status-degraded';
    if (status === 'down') return 'status-down';
    // cc_tmp uses tier (green/orange/red) instead of status
    const tier = probe?.tier || probe?.cc_tier;
    if (tier === 'green') return 'status-healthy';
    if (tier === 'orange') return 'status-degraded';
    if (tier === 'red') return 'status-down';
    if (probe?.free_pct != null) {
        if (probe.free_pct > 20) return 'status-healthy';
        if (probe.free_pct > 10) return 'status-degraded';
        return 'status-down';
    }
    return 'status-unknown';
}

function el(tag, cls, text) {
    const e = document.createElement(tag);
    if (cls) e.className = cls;
    if (text !== undefined) e.textContent = text;
    return e;
}

function formatAge(seconds) {
    if (seconds == null) return '';
    if (seconds < 120) return Math.round(seconds) + 's';
    if (seconds < 7200) return Math.round(seconds / 60) + 'm';
    return (seconds / 3600).toFixed(1) + 'h';
}

function sectionRow(label, value, valueStyle) {
    const row = el('div', 'section-row');
    row.appendChild(el('span', 'label', label));
    const val = el('span', 'value', value);
    if (valueStyle) val.style.cssText = valueStyle;
    row.appendChild(val);
    return row;
}

// ── Shared Tooltip ────────────────────────────────────────────────────

const _tip = document.getElementById('shared-tip');

function showTip(dotEl) {
    // Populate from data attributes stored on the dot
    _tip.replaceChildren();
    const rows = JSON.parse(dotEl.dataset.tip || '[]');
    for (const r of rows) {
        const d = el('div', r.cls || 'dot-tip-row', r.text);
        if (r.style) d.style.cssText = r.style;
        _tip.appendChild(d);
    }

    // Position: prefer above the dot, flip below if clipping top
    const rect = dotEl.getBoundingClientRect();
    _tip.classList.add('visible');
    const tipRect = _tip.getBoundingClientRect();

    let top = rect.top - tipRect.height - 8;
    if (top < 8) top = rect.bottom + 8; // flip below

    let left = rect.left + rect.width / 2 - tipRect.width / 2;
    if (left < 4) left = 4; // clamp left
    if (left + tipRect.width > window.innerWidth - 4) left = window.innerWidth - tipRect.width - 4; // clamp right

    _tip.style.top = top + 'px';
    _tip.style.left = left + 'px';
}

function hideTip() {
    _tip.classList.remove('visible');
}

// ── Side Panel ────────────────────────────────────────────────────────

function updateSidePanel(data) {
    // Services
    const services = data.services || {};
    const svcEl = document.getElementById('services-content');
    if (svcEl) {
        svcEl.replaceChildren();
        const bridge = services.bridge || {};
        const bridgeState = bridge.active_state || 'unknown';
        const bridgeColor = bridgeState === 'active' ? 'color:#4ade80' : 'color:#ef4444';
        let bridgeDetail = bridgeState;
        if (bridge.uptime_seconds != null) {
            bridgeDetail += ' (' + Math.round(bridge.uptime_seconds / 60) + 'm)';
        }
        svcEl.appendChild(sectionRow('Server', bridgeDetail, bridgeColor + ';font-weight:600'));

        const timer = services.watchdog_timer || {};
        const timerState = timer.active_state || 'unknown';
        const timerColor = timerState === 'active' ? 'color:#4ade80' : 'color:#ef4444';
        svcEl.appendChild(sectionRow('Watchdog', timerState, timerColor));

        const wd = services.watchdog || {};
        if (wd.consecutive_failures > 0) {
            svcEl.appendChild(sectionRow('WD Failures', String(wd.consecutive_failures), 'color:#ffe600;font-weight:600'));
        }
        if (wd.in_backoff) {
            svcEl.appendChild(sectionRow('WD Status', 'in backoff', 'color:#ffe600'));
        }

        const ccTmp = (data.infrastructure || {}).cc_tmp;
        if (ccTmp && ccTmp.cc_used_mb != null) {
            const ccTier = ccTmp.cc_tier || 'unknown';
            const ccColor = ccTier === 'green' ? 'color:#4ade80' : ccTier === 'orange' ? 'color:#ffe600' : ccTier === 'red' ? 'color:#ef4444' : 'color:#888';
            svcEl.appendChild(sectionRow('CC Tmp', ccTmp.cc_used_mb + 'MB / ' + ccTmp.cc_budget_mb + 'MB', ccColor));
        }
    }

    // Infrastructure
    const infra = data.infrastructure || {};
    const infraEl = document.getElementById('infra-content');
    if (infraEl) {
        infraEl.replaceChildren();
        for (const [name, info] of Object.entries(infra)) {
            if (name === 'cc_tmp') continue;
            const row = el('div', 'section-row');
            const labelSpan = el('span', 'label');
            const dot = el('span', 'status-dot ' + infraStatusClass(info));
            labelSpan.appendChild(dot);
            labelSpan.appendChild(document.createTextNode(name));
            row.appendChild(labelSpan);
            let detail = '';
            if (info.latency_ms !== undefined) {
                detail = (info.status || 'ok') + ' (' + Math.round(info.latency_ms) + 'ms)';
            } else if (info.free_pct !== undefined) {
                const freeVal = info.free_gb != null
                    ? info.free_gb.toFixed(1) + ' GB'
                    : info.free_mb != null
                    ? Math.round(info.free_mb) + ' MB'
                    : '?';
                detail = freeVal + ' free (' + Math.round(info.free_pct) + '%)';
            } else {
                detail = info.status || 'ok';
            }
            row.appendChild(el('span', 'value', detail));
            infraEl.appendChild(row);
        }
        if (!Object.keys(infra).length) infraEl.appendChild(el('div', null, 'No data'));
        // CC dispatch policy — cached, refreshed every 5 min
        const _renderCcPolicy = (policy) => {
            const row = el('div', 'section-row');
            const label = el('span', 'label');
            if (!policy) {
                label.appendChild(el('span', 'status-dot status-unknown'));
                label.appendChild(document.createTextNode('CC Background'));
                row.appendChild(label);
                row.appendChild(el('span', 'value', 'unknown'));
            } else {
                const dot = el('span', 'status-dot ' + (policy.cli ? 'status-healthy' : 'status-down'));
                label.appendChild(dot);
                label.appendChild(document.createTextNode('CC Background'));
                row.appendChild(label);
                const detail = policy.cli
                    ? (policy.approval ? 'Approval required' : 'Auto-dispatch')
                    : 'Disabled';
                row.appendChild(el('span', 'value', detail));
            }
            infraEl.appendChild(row);
        };
        if (window._ccPolicyCache && (Date.now() - window._ccPolicyCacheTs) < 300000) {
            _renderCcPolicy(window._ccPolicyCache);
        } else {
            fetch('/api/genesis/settings/autonomous_cli_policy').then(r => r.ok ? r.json() : null).then(d => {
                if (d && d.config) {
                    window._ccPolicyCache = { cli: d.config.autonomous_cli_fallback_enabled, approval: d.config.manual_approval_required };
                    window._ccPolicyCacheTs = Date.now();
                }
                _renderCcPolicy(window._ccPolicyCache || null);
            }).catch(() => _renderCcPolicy(null));
        }
    }

    // Awareness Loop
    const awareness = data.awareness || {};
    const awarenessEl = document.getElementById('awareness-content');
    if (awarenessEl) {
        awarenessEl.replaceChildren();
        const aStatus = awareness.status || 'unknown';
        const aColor = aStatus === 'healthy' ? 'color:#4ade80' : aStatus === 'stale' ? 'color:#ffe600' : 'color:#6b7280';
        awarenessEl.appendChild(sectionRow('Status', aStatus, aColor + ';font-weight:600'));
        if (awareness.ticks_24h !== undefined) awarenessEl.appendChild(sectionRow('Ticks/24h', String(awareness.ticks_24h)));
        if (awareness.time_since_last_tick_seconds !== undefined) {
            const secs = Math.round(awareness.time_since_last_tick_seconds);
            awarenessEl.appendChild(sectionRow('Last tick', secs < 120 ? secs + 's ago' : Math.round(secs / 60) + 'm ago'));
        }
        if (awareness.critical_bypasses_24h) {
            awarenessEl.appendChild(sectionRow('Critical bypasses', String(awareness.critical_bypasses_24h), 'color:#ffe600'));
        }
    }

    // Resilience
    const resEl = document.getElementById('resilience-content');
    if (resEl) {
        resEl.replaceChildren();
        const resObj = data.resilience || {};
        const resState = resObj.level || resObj || 'not configured';
        const resSummary = resObj.summary || '';
        const rColor = (resState === 'L0' || resState === 'normal') ? 'color:#4ade80' : resState === 'not configured' ? 'color:#6b7280' : 'color:#ffe600';
        const resLabel = resSummary ? resState + ' \u2014 ' + resSummary : String(resState);
        resEl.appendChild(sectionRow('State', resLabel, rColor + ';font-weight:600'));

        const sites = data.call_sites || {};
        const siteIds = Object.keys(sites);
        const activeSiteIds = siteIds.filter(id => sites[id].status !== 'idle' && sites[id].status !== 'disabled');
        const totalSites = activeSiteIds.length;
        const healthySites = activeSiteIds.filter(id => sites[id].status === 'healthy' || sites[id].status === 'active' || sites[id].status === 'warning').length;
        if (totalSites > 0) {
            const summaryColor = healthySites === totalSites ? 'color:#4ade80' : healthySites > totalSites * 0.5 ? 'color:#ffe600' : 'color:#ef4444';
            resEl.appendChild(sectionRow('Call sites', healthySites + '/' + totalSites + ' healthy', summaryColor));
        }

        const catHealth = {};
        for (const [siteId, info] of Object.entries(sites)) {
            if (info.status === 'idle' || info.status === 'disabled') continue;
            const meta = getMeta(siteId);
            const cat = meta.category;
            if (!catHealth[cat]) catHealth[cat] = { total: 0, healthy: 0 };
            catHealth[cat].total++;
            if (info.status === 'healthy' || info.status === 'active' || info.status === 'warning') catHealth[cat].healthy++;
        }
        const cats = Object.keys(catHealth).sort();
        if (cats.length > 0) {
            const catHeader = el('div', 'section-row');
            catHeader.style.cssText = 'margin-top:8px;padding-bottom:4px;border-bottom:1px solid #1e293b';
            catHeader.appendChild(el('span', 'label', 'By Category'));
            catHeader.appendChild(el('span', 'value', ''));
            resEl.appendChild(catHeader);

            for (const cat of cats) {
                const c = catHealth[cat];
                const row = el('div', 'cat-health');
                const nameSpan = el('span', 'cat-name', cat);
                const countSpan = el('span', 'cat-count', c.healthy + '/' + c.total);
                countSpan.style.color = c.healthy === c.total ? '#4ade80' : '#ffe600';
                row.appendChild(nameSpan);
                row.appendChild(countSpan);
                resEl.appendChild(row);
            }
        }
    }

    // CC Sessions
    const cc = data.cc_sessions || {};
    const ccEl = document.getElementById('cc-content');
    if (ccEl) {
        ccEl.replaceChildren();
        if (cc.foreground) ccEl.appendChild(sectionRow('Foreground', (cc.foreground.active || 0) + ' active'));
        if (cc.background) {
            ccEl.appendChild(sectionRow('Background', (cc.background.active || 0) + ' active'));
            if (cc.background.hourly_budget) ccEl.appendChild(sectionRow('Budget', cc.background.hourly_budget + '/hr'));
        }
        if (cc.failed_24h) ccEl.appendChild(sectionRow('Failed/24h', String(cc.failed_24h), 'color:#ef4444'));
    }

    // Queues
    const queues = data.queues || {};
    const qEl = document.getElementById('queue-content');
    if (qEl) {
        qEl.replaceChildren();
        for (const [name, val] of Object.entries(queues)) {
            if (typeof val !== 'number' && val !== null) continue;
            if (name.endsWith('_age_seconds')) {
                if (val == null) continue;
                const label = name.replace(/_age_seconds$/, '').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
                qEl.appendChild(sectionRow(label + ' Age', formatAge(val), 'color:#94a3b8'));
                continue;
            }
            if (typeof val !== 'number') continue;
            const label = name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
            const color = val > 0 ? 'color:#ffe600' : 'color:#4ade80';
            qEl.appendChild(sectionRow(label, String(val), color));
        }
        if (queues.deferred_items && queues.deferred_items.length > 0) {
            for (const item of queues.deferred_items) {
                const desc = (item.site || item.type) + ' \u2014 ' + item.reason;
                const age = formatAge(item.age_s) + (item.attempts > 0 ? ', ' + item.attempts + ' retries' : '');
                qEl.appendChild(sectionRow(desc, age, 'color:#94a3b8;font-size:10px'));
            }
        }
        renderDiscardedItems(qEl, queues);
    }

    // Cost
    const cost = data.cost || {};
    const costEl = document.getElementById('cost-content');
    if (costEl) {
        costEl.replaceChildren();
        if (cost.monthly_usd !== undefined) costEl.appendChild(sectionRow('Month', '$' + cost.monthly_usd.toFixed(2)));
        if (cost.daily_usd !== undefined) costEl.appendChild(sectionRow('Today', '$' + cost.daily_usd.toFixed(3)));
        if (cost.budget_status) {
            const cls = cost.budget_status === 'UNDER_LIMIT' ? 'badge-normal' : cost.budget_status === 'WARNING' ? 'badge-warning' : 'badge-critical';
            const row = el('div', 'section-row');
            row.appendChild(el('span', 'label', 'Status'));
            row.appendChild(el('span', 'badge ' + cls, cost.budget_status));
            costEl.appendChild(row);
        }
    }
}

// ── Detail panel ─────────────────────────────────────────────────────

function showDetail(item) {
    const content = document.getElementById('detail-content');
    content.replaceChildren();
    const meta = getMeta(item.id);

    // Title
    const heading = el('h3', null, meta.name);
    heading.style.cssText = 'color:#f8fafc;margin:12px 0 4px';
    content.appendChild(heading);

    // Category badge
    const catBadge = el('span', 'detail-cat-badge', meta.category);
    catBadge.style.background = (CAT_COLORS[meta.category] || '#6b7280') + '33';
    catBadge.style.color = CAT_COLORS[meta.category] || '#6b7280';
    content.appendChild(catBadge);

    // Status-reason badge — clarifies WHY a site is wired/idle/disabled.
    // Maps from machine-readable status_reason (set in _call_site_meta.py).
    const statusReason = item.data.status_reason;
    if (statusReason) {
        const REASON_LABELS = {
            WIRED: 'WIRED',
            WIRED_DIFFERENT_MECHANISM: 'WIRED — DIFFERENT MECHANISM',
            PARTIALLY_WIRED: 'PARTIALLY WIRED',
            V4_PLACEHOLDER: 'V4 PLACEHOLDER',
            DEPRECATED_REMOVED: 'DEPRECATED — REMOVED',
            SUPERSEDED: 'SUPERSEDED',
            TEMP_DISABLED: 'TEMPORARILY DISABLED',
            REPLACED_BY_AWARENESS_LOOP: 'REPLACED BY AWARENESS LOOP',
            NO_API_KEYS: 'NO API KEY CONFIGURED',  // pragma: allowlist secret
        };
        const REASON_CLASS = {
            WIRED: 'badge-normal',
            WIRED_DIFFERENT_MECHANISM: 'badge-normal',
            PARTIALLY_WIRED: 'badge',
            V4_PLACEHOLDER: 'badge-info',
            DEPRECATED_REMOVED: 'badge-disabled',
            SUPERSEDED: 'badge-disabled',
            TEMP_DISABLED: 'badge-warning',
            REPLACED_BY_AWARENESS_LOOP: 'badge-disabled',
            NO_API_KEYS: 'badge-critical',  // pragma: allowlist secret
        };
        const reasonWrap = el('div', 'detail-status-reason');
        const reasonBadge = el('span', 'badge ' + (REASON_CLASS[statusReason] || 'badge'),
                               REASON_LABELS[statusReason] || statusReason);
        reasonWrap.appendChild(reasonBadge);
        content.appendChild(reasonWrap);
    }

    // Description — prefer backend data over static SITE_META
    const descText = item.data.description || meta.desc;
    if (descText) {
        content.appendChild(el('div', 'detail-desc', descText));
    }

    // Status + disabled banner
    const STATUS_COLORS = { healthy: '#4ade80', active: '#4ade80', warning: '#f59e0b', degraded: '#ffe600', down: '#ef4444', idle: '#888', disabled: '#888' };
    const statusColor = STATUS_COLORS[item.status] || '#888';
    let statusLabel = item.status;
    if (item.status === 'idle') statusLabel = 'Disabled (groundwork)';
    else if (item.status === 'disabled') statusLabel = 'Disabled (no API key)';
    content.appendChild(sectionRow('Status', statusLabel, 'color:' + statusColor));
    if (item.status === 'idle') {
        const banner = el('div', null, 'This call site is groundwork \u2014 config exists but no code invokes it yet.');
        banner.style.cssText = 'background:rgba(136,136,136,0.15);color:#888;padding:6px 10px;border-radius:4px;font-size:11px;margin:8px 0;';
        content.appendChild(banner);
    } else if (item.status === 'disabled') {
        const banner = el('div', null);
        banner.style.cssText = 'background:rgba(153,27,27,0.15);color:#fca5a5;padding:8px 10px;border-radius:4px;font-size:11px;margin:8px 0;line-height:1.5;';
        if (item.data.disabled_reason === 'no_api_keys_configured') {
            const intro = el('div', null,
                'No API key configured for any provider in this chain. This is a deployment-time config state, not an outage — partial API-key configuration is the normal install state.');
            banner.appendChild(intro);
            // Enumerate missing env vars from chain_health
            const chain = item.data.chain_health || [];
            const missing = chain.filter(c => c.missing_env_var);
            if (missing.length > 0) {
                const list = el('div', null);
                list.style.cssText = 'margin-top:6px;';
                list.appendChild(el('div', null, 'Configure one of these in secrets.env to enable:'));
                const ul = el('ul', null);
                ul.style.cssText = 'margin:4px 0 0 0;padding-left:18px;';
                missing.forEach(c => {
                    const li = el('li', null);
                    const code = el('code', null, c.missing_env_var || '');
                    code.style.color = '#fde68a';
                    li.appendChild(code);
                    li.appendChild(document.createTextNode(
                        ' — ' + (c.provider || '') + ' (' + (c.type || '') + ')',
                    ));
                    ul.appendChild(li);
                });
                list.appendChild(ul);
                const note = el('div', null);
                note.appendChild(document.createTextNode('Genesis also accepts '));
                const alt1 = el('code', null, '<TYPE>_API_KEY');
                note.appendChild(alt1);
                note.appendChild(document.createTextNode(' or '));
                const alt2 = el('code', null, '<TYPE>_API_TOKEN');
                note.appendChild(alt2);
                note.appendChild(document.createTextNode(
                    ' if your install uses a different convention.',
                ));
                note.style.cssText = 'margin-top:6px;color:#a8a29e;font-size:10px;';
                list.appendChild(note);
                banner.appendChild(list);
            }
        } else {
            banner.textContent = 'This call site is disabled.';
        }
        content.appendChild(banner);
    }
    // Warning/failure details
    if (item.data.recent_failures) {
        content.appendChild(sectionRow('Recent Failures', item.data.recent_failures + ' in last 30 min', 'color:#f59e0b;font-weight:600'));
        if (item.data.last_failure_at) {
            const failAge = Math.round((Date.now() - new Date(item.data.last_failure_at).getTime()) / 1000);
            content.appendChild(sectionRow('Last Failure', formatAge(failAge) + ' ago', 'color:#f59e0b'));
        }
    }

    // Metadata — prefer backend data over static SITE_META
    const freq = item.data.frequency || meta.frequency;
    if (freq) content.appendChild(sectionRow('Frequency', freq));
    const costPol = item.data.cost_policy || meta.costPolicy;
    if (costPol) content.appendChild(sectionRow('Cost Policy', costPol));
    const tierLabels = {
        embedding: 'Embedding (0.6B)', slm: 'SLM / Free API', mid: 'Mid-tier (30B)',
        frontier: 'Frontier', cc: 'CC Session',
    };
    const tier = item.data.model_tier;
    if (tier) content.appendChild(sectionRow('Model Tier', tierLabels[tier] || tier));

    // Active provider
    if (item.data.active_provider) {
        content.appendChild(sectionRow('Active Provider', item.data.active_provider));
    }

    // Routing Chain Config — consolidated editor with health dots
    if (item.data.chain_health && item.data.chain_health.length > 0) {
        const editorDiv = el('div', 'config-editor');
        editorDiv.appendChild(el('div', 'chain-label', 'Routing Chain Config'));

        // Static info — prefer backend-derived data over static SITE_META
        const edCostPolicy = item.data.cost_policy || SITE_META[item.id]?.costPolicy || 'unknown';
        const edFrequency = item.data.frequency || SITE_META[item.id]?.frequency || 'unknown';
        editorDiv.appendChild(sectionRow('Cost Policy', edCostPolicy));
        editorDiv.appendChild(sectionRow('Frequency', edFrequency));

        // Dispatch mode toggle — three-state: API / Auto / CLI
        const modeRow = el('div', 'editor-mode-row');
        modeRow.style.cssText = 'display:flex;align-items:center;gap:6px;margin:4px 0 6px;';
        const modeLabel = el('span', null, 'Mode:');
        modeLabel.style.cssText = 'font-size:11px;color:#94a3b8;';
        modeRow.appendChild(modeLabel);
        const modeSelect = document.createElement('select');
        modeSelect.className = 'dispatch-mode-select';
        modeSelect.style.cssText = 'font-size:11px;padding:2px 6px;background:#1e293b;color:#e2e8f0;border:1px solid #334155;border-radius:3px;';
        [
            ['api',  'API only'],
            ['dual', 'Auto (dual)'],
            ['cli',  'CLI only'],
        ].forEach(([val, label]) => {
            const opt = document.createElement('option');
            opt.value = val;
            opt.textContent = label;
            modeSelect.appendChild(opt);
        });
        modeRow.appendChild(modeSelect);
        const modeHint = el('span', null, '');
        modeHint.className = 'mode-hint';
        modeHint.style.cssText = 'font-size:10px;color:#64748b;margin-left:4px;';
        modeRow.appendChild(modeHint);
        editorDiv.appendChild(modeRow);

        // Chain editor placeholder — populated async
        const chainList = document.createElement('ul');
        chainList.className = 'chain-editor-list';
        const loadingLi = document.createElement('li');
        loadingLi.style.cssText = 'color:#64748b;font-size:11px;';
        loadingLi.textContent = 'Loading config...';
        chainList.appendChild(loadingLi);
        editorDiv.appendChild(chainList);

        // Action row: add provider dropdown + add button
        const actionsDiv = el('div', 'editor-actions');
        const addSelect = document.createElement('select');
        const defaultOpt = document.createElement('option');
        defaultOpt.value = '';
        defaultOpt.textContent = 'Add provider...';
        addSelect.appendChild(defaultOpt);
        actionsDiv.appendChild(addSelect);
        const addBtn = document.createElement('button');
        addBtn.className = 'btn-add';
        addBtn.textContent = 'Add';
        actionsDiv.appendChild(addBtn);
        editorDiv.appendChild(actionsDiv);

        // Toggles row
        const togglesDiv = el('div', 'editor-toggles');
        const dpLabel = document.createElement('label');
        const dpCheck = document.createElement('input');
        dpCheck.type = 'checkbox';
        dpLabel.appendChild(dpCheck);
        dpLabel.appendChild(document.createTextNode(' default_paid'));
        togglesDiv.appendChild(dpLabel);
        const npLabel = document.createElement('label');
        const npCheck = document.createElement('input');
        npCheck.type = 'checkbox';
        npLabel.appendChild(npCheck);
        npLabel.appendChild(document.createTextNode(' never_pays'));
        togglesDiv.appendChild(npLabel);
        editorDiv.appendChild(togglesDiv);

        // Save / Revert buttons
        const saveRow = el('div', 'editor-actions');
        const saveBtn = document.createElement('button');
        saveBtn.className = 'btn-save';
        saveBtn.textContent = 'Save';
        saveBtn.disabled = true;
        saveRow.appendChild(saveBtn);
        const revertBtn = document.createElement('button');
        revertBtn.className = 'btn-revert';
        revertBtn.textContent = 'Revert';
        revertBtn.disabled = true;
        saveRow.appendChild(revertBtn);
        editorDiv.appendChild(saveRow);

        // Feedback area
        const feedbackDiv = el('div', 'editor-feedback');
        feedbackDiv.style.display = 'none';
        editorDiv.appendChild(feedbackDiv);

        content.appendChild(editorDiv);

        // Load config from API and populate editor
        _loadChainEditor(item.id, chainList, addSelect, addBtn, dpCheck, npCheck, saveBtn, revertBtn, feedbackDiv, modeSelect, modeHint, item.data.chain_health);
    }

    // CC session details
    if (item.data.active !== undefined) {
        content.appendChild(sectionRow('Active Sessions', String(item.data.active)));
    }
    if (item.data.hourly_budget) {
        content.appendChild(sectionRow('Hourly Budget', item.data.hourly_budget));
    }

    // Last run info
    if (item.data.last_run_at) {
        const lastRunSection = el('div', 'detail-section');
        lastRunSection.appendChild(el('h4', null, 'Last Run'));
        const runTime = new Date(item.data.last_run_at);
        const runStr = runTime.toLocaleDateString('en-US', {month: 'short', day: 'numeric'})
            + ' ' + runTime.toLocaleTimeString('en-US', {hour12: false, hour: '2-digit', minute: '2-digit'});
        lastRunSection.appendChild(sectionRow('Time', runStr));
        if (item.data.last_run_provider) {
            lastRunSection.appendChild(sectionRow('Provider', item.data.last_run_provider + (item.data.last_run_model ? ' (' + item.data.last_run_model + ')' : '')));
        }
        if (item.data.last_run_tokens) {
            lastRunSection.appendChild(sectionRow('Tokens', String(item.data.last_run_tokens)));
        }
        content.appendChild(lastRunSection);

        // Full response in scrollable box
        if (item.data.last_response) {
            const respSection = el('div', 'detail-section');
            respSection.appendChild(el('h4', null, 'Last Response'));
            const respBox = document.createElement('pre');
            respBox.textContent = item.data.last_response;
            respBox.style.cssText = 'font-size: 11px; max-height: 200px; overflow-y: auto; background: #1e293b; padding: 8px; border-radius: 4px; white-space: pre-wrap; word-break: break-word; margin: 4px 0; color: #e2e8f0;';
            respSection.appendChild(respBox);
            content.appendChild(respSection);
        }
    } else {
        content.appendChild(sectionRow('Last Run', 'No runs recorded'));
    }

    // See Also — cross-references to sibling call sites in the same vocabulary
    // family (triage, fresh_eyes_review, task_retrospective, ego_compaction).
    // Click to switch the detail panel to the sibling's view.
    const seeAlso = item.data.see_also;
    if (seeAlso && Array.isArray(seeAlso) && seeAlso.length > 0) {
        const seeAlsoSection = el('div', 'detail-section');
        seeAlsoSection.appendChild(el('h4', null, 'See Also'));
        const list = el('div', 'see-also-list');
        seeAlso.forEach(sid => {
            const linkItem = el('div', 'see-also-item', sid);
            linkItem.addEventListener('click', () => {
                // Fetch fresh health data and re-show detail for the sibling.
                fetch('/api/genesis/health').then(r => r.json()).then(h => {
                    const siteData = (h.call_sites && h.call_sites[sid]) || {};
                    showDetail({ id: sid, status: siteData.status || 'unknown', data: siteData });
                }).catch(() => {});
            });
            list.appendChild(linkItem);
        });
        seeAlsoSection.appendChild(list);
        content.appendChild(seeAlsoSection);
    }

    // Switch side panel to detail view
    document.getElementById('side-default').style.display = 'none';
    document.getElementById('side-detail').classList.add('active');
}

// ── Chain editor logic ───────────────────────────────────────────────

let _editorState = {};  // {chain, originalChain, allProviders, cbStates, default_paid, never_pays, original_dp, original_np}

async function _loadChainEditor(callSiteId, listEl, selectEl, addBtn, dpCheck, npCheck, saveBtn, revertBtn, feedbackEl, modeSelect, modeHint, chainHealth) {
    try {
        const resp = await fetch('/api/genesis/routing/config');
        if (!resp.ok) throw new Error('API error');
        const cfg = await resp.json();

        const site = cfg.call_sites[callSiteId];
        if (!site) {
            listEl.textContent = '';
            const noSiteLi = document.createElement('li');
            noSiteLi.style.cssText = 'color:#64748b;font-size:11px;';
            noSiteLi.textContent = 'Not a router call site';
            listEl.appendChild(noSiteLi);
            return;
        }

        // Resolve current mode: 'api' / 'cli' / 'dual'. Legacy 'cc' maps to
        // 'dual'; anything missing defaults to 'dual' (existing auto behavior).
        const rawDispatch = site.dispatch || null;
        let currentMode;
        if (rawDispatch === 'api') currentMode = 'api';
        else if (rawDispatch === 'cli') currentMode = 'cli';
        else currentMode = 'dual';  // 'dual', 'cc', or null

        // Build chain: API providers + CC entry if dispatch=cc/dual
        const editorChain = [...site.chain];
        const ccModel = site.cc_model || null;
        if ((rawDispatch === 'cc' || rawDispatch === 'dual' || rawDispatch === 'cli') && ccModel) {
            const ccPos = site.cc_position;
            const ccEntry = 'CC/' + ccModel;
            if (ccPos != null && ccPos >= 0 && ccPos <= editorChain.length) {
                editorChain.splice(ccPos, 0, ccEntry);
            } else {
                editorChain.push(ccEntry);
            }
        }

        // Build chain_health lookup: provider_name → health object
        const chainHealthMap = {};
        if (chainHealth) {
            for (const ch of chainHealth) {
                chainHealthMap[ch.provider] = ch;
            }
        }

        _editorState = {
            callSiteId,
            chain: editorChain,
            originalChain: [...editorChain],
            allProviders: cfg.providers,
            disabledProviders: cfg.disabled_providers || {},
            cbStates: cfg.cb_states || {},
            chainHealth: chainHealthMap,
            default_paid: site.default_paid,
            never_pays: site.never_pays,
            original_dp: site.default_paid,
            original_np: site.never_pays,
            mode: currentMode,
            originalMode: currentMode,
            originalDispatch: rawDispatch,
            originalCcModel: ccModel,
        };

        if (modeSelect) {
            modeSelect.value = currentMode;
            modeSelect.onchange = () => {
                _editorState.mode = modeSelect.value;
                _renderModeHint(modeHint, modeSelect.value);
                _updateDirtyState(saveBtn, revertBtn);
            };
            _renderModeHint(modeHint, currentMode);
        }

        dpCheck.checked = site.default_paid;
        npCheck.checked = site.never_pays;

        _renderChainList(listEl);
        _populateAddSelect(selectEl);
        _updateDirtyState(saveBtn, revertBtn);

        // Event handlers
        addBtn.onclick = () => {
            if (selectEl.value) {
                _editorState.chain.push(selectEl.value);
                _renderChainList(listEl);
                _populateAddSelect(selectEl);
                _updateDirtyState(saveBtn, revertBtn);
            }
        };

        dpCheck.onchange = () => {
            _editorState.default_paid = dpCheck.checked;
            _updateDirtyState(saveBtn, revertBtn);
        };
        npCheck.onchange = () => {
            _editorState.never_pays = npCheck.checked;
            _updateDirtyState(saveBtn, revertBtn);
        };

        saveBtn.onclick = () => _saveConfig(saveBtn, feedbackEl, listEl, selectEl, revertBtn);
        revertBtn.onclick = () => {
            _editorState.chain = [..._editorState.originalChain];
            _editorState.default_paid = _editorState.original_dp;
            _editorState.never_pays = _editorState.original_np;
            _editorState.mode = _editorState.originalMode;
            dpCheck.checked = _editorState.original_dp;
            npCheck.checked = _editorState.original_np;
            if (modeSelect) {
                modeSelect.value = _editorState.originalMode;
                _renderModeHint(modeHint, _editorState.originalMode);
            }
            _renderChainList(listEl);
            _populateAddSelect(selectEl);
            _updateDirtyState(saveBtn, revertBtn);
            feedbackEl.style.display = 'none';
        };
    } catch (e) {
        listEl.textContent = '';
        const errLi = document.createElement('li');
        errLi.style.cssText = 'color:#f87171;font-size:11px;';
        errLi.textContent = 'Failed to load config';
        listEl.appendChild(errLi);
    }
}

function _isCC(prov) { return prov.startsWith('CC/'); }
function _ccModelFromEntry(prov) { return prov.replace('CC/', ''); }

function _renderModeHint(hintEl, mode) {
    if (!hintEl) return;
    if (mode === 'api') {
        hintEl.textContent = 'API chain only \u2014 no CC subprocess';
    } else if (mode === 'cli') {
        hintEl.textContent = 'CC subprocess only \u2014 API chain unused';
    } else {
        hintEl.textContent = 'Dispatcher auto-decides';
    }
}

function _renderChainList(listEl) {
    while (listEl.firstChild) listEl.removeChild(listEl.firstChild);
    _editorState.chain.forEach((prov, idx) => {
        const li = document.createElement('li');
        li.className = 'chain-editor-item';
        const isCC = _isCC(prov);

        if (isCC) {
            const dot = document.createElement('span');
            dot.className = 'cb-dot';
            dot.style.background = '#818cf8';
            dot.title = 'Claude Code dispatch';
            li.appendChild(dot);

            const badge = el('span', null, 'CC');
            badge.style.cssText = 'background:#818cf8;color:#fff;padding:1px 5px;border-radius:3px;font-size:10px;font-weight:600;margin-right:4px;';
            li.appendChild(badge);

            const select = document.createElement('select');
            select.style.cssText = 'font-size:11px;padding:1px 4px;background:#1e293b;color:#e2e8f0;border:1px solid #334155;border-radius:3px;';
            ['Haiku', 'Sonnet', 'Opus'].forEach(m => {
                const opt = document.createElement('option');
                opt.value = m;
                opt.textContent = m;
                if (_ccModelFromEntry(prov) === m) opt.selected = true;
                select.appendChild(opt);
            });
            select.onchange = () => {
                _editorState.chain[idx] = 'CC/' + select.value;
                const editor = listEl.closest('.config-editor');
                if (editor) {
                    const btns = editor.querySelectorAll('.editor-actions button');
                    _updateDirtyState(btns[btns.length - 2], btns[btns.length - 1]);
                }
            };
            li.appendChild(select);
        } else {
            const ch = _editorState.chainHealth?.[prov];
            const cbState = _editorState.cbStates[prov] || ch?.state || 'closed';
            const ps = ch?.probe_status;
            const cbDown = cbState === 'open' || cbState === 'error';
            const cbSuspect = cbState === 'half_open';
            const dotColor = ps === 'not_configured' ? '#888'
                : cbDown ? '#ef4444'
                : cbSuspect ? '#facc15'
                : ps === 'unreachable' ? '#ef4444'
                : ps === 'rate_limited' ? '#facc15'
                : '#4ade80';

            const dot = document.createElement('span');
            dot.className = 'cb-dot';
            dot.style.background = dotColor;
            dot.title = ps === 'not_configured' ? 'No API key' : 'CB: ' + cbState + (ps ? ', probe: ' + ps : '');
            li.appendChild(dot);

            const provCfg = _editorState.allProviders[prov];
            let edDisplayName = prov.startsWith('claude-') ? prov.replace('claude-', 'anthropic-') : prov;
            const provType = ch?.type || provCfg?.provider_type;
            if (provType === 'openrouter') edDisplayName += ' (via OpenRouter)';
            else if (provType === 'zenmux') edDisplayName += ' (via ZenMux)';
            else if (provType === 'lmstudio') edDisplayName += ' (LM Studio)';
            const nameText = edDisplayName + (provCfg?.free ? ' [free]' : '');
            const nameSpan = el('span', 'provider-name', nameText);
            if (provCfg?.free) nameSpan.style.color = '#4ade80';
            li.appendChild(nameSpan);
            if (provCfg?.model) {
                const modelSpan = el('span', null, provCfg.model);
                modelSpan.style.cssText = 'color:#64748b;font-size:10px;margin-left:6px;';
                li.appendChild(modelSpan);
            }

            let stateText, stateColor;
            if (ps === 'not_configured') {
                stateText = 'no API key';
                stateColor = '#888';
            } else {
                const failures = ch?.failures || 0;
                stateText = cbState + (failures > 0 ? ' (' + failures + ' fails)' : '');
                stateColor = cbDown ? '#ef4444' : cbSuspect ? '#facc15' : '#4ade80';
            }
            const stateSpan = el('span', null, stateText);
            stateSpan.style.cssText = 'color:' + stateColor + ';font-size:10px;margin-left:auto;margin-right:6px;';
            li.appendChild(stateSpan);
        }

        // Move up
        if (idx > 0) {
            const upBtn = document.createElement('button');
            upBtn.className = 'move-btn';
            upBtn.textContent = '\u25B2';
            upBtn.title = 'Move up';
            upBtn.onclick = () => {
                [_editorState.chain[idx - 1], _editorState.chain[idx]] = [_editorState.chain[idx], _editorState.chain[idx - 1]];
                _renderChainList(listEl);
                const editor = listEl.closest('.config-editor');
                if (editor) {
                    const btns = editor.querySelectorAll('.editor-actions button');
                    _updateDirtyState(btns[btns.length - 2], btns[btns.length - 1]);
                }
            };
            li.appendChild(upBtn);
        }

        // Move down
        if (idx < _editorState.chain.length - 1) {
            const downBtn = document.createElement('button');
            downBtn.className = 'move-btn';
            downBtn.textContent = '\u25BC';
            downBtn.title = 'Move down';
            downBtn.onclick = () => {
                [_editorState.chain[idx], _editorState.chain[idx + 1]] = [_editorState.chain[idx + 1], _editorState.chain[idx]];
                _renderChainList(listEl);
                const editor = listEl.closest('.config-editor');
                if (editor) {
                    const btns = editor.querySelectorAll('.editor-actions button');
                    _updateDirtyState(btns[btns.length - 2], btns[btns.length - 1]);
                }
            };
            li.appendChild(downBtn);
        }

        // Remove (only if chain has >1)
        if (_editorState.chain.length > 1) {
            const rmBtn = document.createElement('button');
            rmBtn.className = 'remove-btn';
            rmBtn.textContent = '\u2715';
            rmBtn.title = 'Remove';
            rmBtn.onclick = () => {
                _editorState.chain.splice(idx, 1);
                _renderChainList(listEl);
                const selectEl = listEl.parentElement.querySelector('select');
                if (selectEl) _populateAddSelect(selectEl);
                const editor = listEl.closest('.config-editor');
                if (editor) {
                    const btns = editor.querySelectorAll('.editor-actions button');
                    _updateDirtyState(btns[btns.length - 2], btns[btns.length - 1]);
                }
            };
            li.appendChild(rmBtn);
        }

        listEl.appendChild(li);
    });
}

function _populateAddSelect(selectEl) {
    while (selectEl.firstChild) selectEl.removeChild(selectEl.firstChild);
    const defaultOpt = document.createElement('option');
    defaultOpt.value = '';
    defaultOpt.textContent = 'Add provider...';
    selectEl.appendChild(defaultOpt);
    for (const [name, prov] of Object.entries(_editorState.allProviders)) {
        if (!_editorState.chain.includes(name)) {
            const opt = document.createElement('option');
            opt.value = name;
            const edName = name.startsWith('claude-') ? name.replace('claude-', 'anthropic-') : name;
            opt.textContent = edName + (prov.free ? ' [free]' : '') + (prov.model ? ' \u2014 ' + prov.model : '');
            selectEl.appendChild(opt);
        }
    }
    // Disabled providers (no API key) — shown grayed out for visibility
    for (const [name, provType] of Object.entries(_editorState.disabledProviders || {})) {
        if (!_editorState.chain.includes(name)) {
            const opt = document.createElement('option');
            opt.value = '';
            opt.disabled = true;
            opt.textContent = name + ' \u2014 no API key (' + provType + ')';
            opt.style.color = '#64748b';
            selectEl.appendChild(opt);
        }
    }
    // CC dispatch options (only if no CC entry already in chain)
    const hasCC = _editorState.chain.some(_isCC);
    if (!hasCC) {
        for (const model of ['Haiku', 'Sonnet', 'Opus']) {
            const opt = document.createElement('option');
            opt.value = 'CC/' + model;
            opt.textContent = 'CC/' + model + ' (Claude Code)';
            selectEl.appendChild(opt);
        }
    }
}

function _updateDirtyState(saveBtn, revertBtn) {
    const chainChanged = JSON.stringify(_editorState.chain) !== JSON.stringify(_editorState.originalChain);
    const dpChanged = _editorState.default_paid !== _editorState.original_dp;
    const npChanged = _editorState.never_pays !== _editorState.original_np;
    const modeChanged = _editorState.mode !== _editorState.originalMode;
    const dirty = chainChanged || dpChanged || npChanged || modeChanged;
    saveBtn.disabled = !dirty;
    revertBtn.disabled = !dirty;
}

async function _saveConfig(saveBtn, feedbackEl, listEl, selectEl, revertBtn) {
    saveBtn.disabled = true;
    saveBtn.textContent = 'Saving...';
    feedbackEl.style.display = 'none';

    // Separate API chain from CC entry
    const apiChain = _editorState.chain.filter(p => !_isCC(p));
    const ccEntry = _editorState.chain.find(_isCC);
    let ccModel = ccEntry ? _ccModelFromEntry(ccEntry) : null;
    let ccPosition = ccEntry ? _editorState.chain.indexOf(ccEntry) : null;
    const mode = _editorState.mode || 'dual';

    // API mode: drop any CC entry from the save payload.
    if (mode === 'api') {
        ccModel = null;
        ccPosition = null;
    }
    // CLI mode: a CC entry is required. Preserve the last-known cc_model
    // from originalCcModel if the user hasn't added one to the chain yet.
    if (mode === 'cli' && !ccModel) {
        ccModel = _editorState.originalCcModel || 'Sonnet';
    }

    try {
        const payload = {
            chain: apiChain,
            default_paid: _editorState.default_paid,
            never_pays: _editorState.never_pays,
            cc_model: ccModel,
            cc_position: ccPosition,
            dispatch: mode,
        };
        const resp = await fetch('/api/genesis/routing/config/' + encodeURIComponent(_editorState.callSiteId), {
            method: 'PUT',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify(payload),
        });
        const data = await resp.json();
        if (!resp.ok) throw new Error(data.error || 'Save failed');

        // Re-fetch the current config to verify the server actually applied
        // our changes. This guards against silent save failures where the
        // server returns 200 but the yaml state doesn't match what we sent.
        const verifyResp = await fetch('/api/genesis/routing/config');
        if (!verifyResp.ok) throw new Error('Post-save verify fetch failed');
        const verifyCfg = await verifyResp.json();
        const verifySite = (verifyCfg.call_sites || {})[_editorState.callSiteId];
        if (!verifySite) throw new Error('Post-save verify: call site missing');
        const verifyDispatch = verifySite.dispatch || 'dual';
        if (verifyDispatch !== mode) {
            throw new Error(
                'Save reported success but dispatch mismatch: expected ' +
                mode + ', got ' + verifyDispatch + '. Please retry.',
            );
        }

        // Update original state to match what's now on disk
        _editorState.originalChain = [..._editorState.chain];
        _editorState.original_dp = _editorState.default_paid;
        _editorState.original_np = _editorState.never_pays;
        _editorState.originalMode = mode;
        _editorState.originalDispatch = mode;
        _editorState.originalCcModel = ccModel;

        feedbackEl.textContent = 'Saved + verified (' + mode + ')';
        feedbackEl.className = 'editor-feedback feedback-ok';
        feedbackEl.style.display = 'block';
        setTimeout(() => feedbackEl.style.display = 'none', 3000);

        // Refresh health data and re-show this call site's detail
        await fetchHealth();
        if (lastData) {
            const cs = lastData.call_sites || {};
            const siteData = cs[_editorState.callSiteId];
            if (siteData) {
                showDetail({ id: _editorState.callSiteId, status: siteData.status, data: siteData });
            }
        }
    } catch (e) {
        feedbackEl.textContent = e.message;
        feedbackEl.className = 'editor-feedback feedback-err';
        feedbackEl.style.display = 'block';
    }

    saveBtn.textContent = 'Save';
    _updateDirtyState(saveBtn, revertBtn);
}

function showDefaultPanel() {
    document.getElementById('side-detail').classList.remove('active');
    document.getElementById('side-default').style.display = 'block';
}

// ── Discarded items — iLO-style expand/clear ────────────────────────

function renderDiscardedItems(container, queues) {
    const items = queues.discarded_items || [];
    const count = queues.discarded_count || items.length;
    if (count === 0) return;

    // Toggle header
    const toggle = el('div', 'discarded-toggle');
    const toggleText = el('span', null, 'Discarded (' + count + ')');
    const arrow = el('span', 'arrow', '\u25B6');
    toggle.appendChild(toggleText);
    toggle.appendChild(arrow);
    container.appendChild(toggle);

    // Collapsible details container
    const details = el('div', 'discarded-details');
    container.appendChild(details);

    toggle.addEventListener('click', function() {
        const isOpen = details.classList.toggle('open');
        arrow.classList.toggle('open', isOpen);
    });

    // Render each discarded item
    for (const item of items) {
        const card = el('div', 'discarded-item');

        const header = el('div', 'di-header', (item.site || item.type));
        card.appendChild(header);

        const statusLabel = item.status === 'expired' ? 'Expired' : 'Discarded';
        card.appendChild(el('div', 'di-row', 'Status: ' + statusLabel));
        card.appendChild(el('div', 'di-row', 'Reason: ' + (item.reason || 'unknown')));

        if (item.error) {
            card.appendChild(el('div', 'di-error', 'Error: ' + item.error));
        }

        const meta = [];
        if (item.attempts > 0) meta.push(item.attempts + ' attempts');
        meta.push('age: ' + formatAge(item.age_s));
        if (item.discarded_at) {
            const d = new Date(item.discarded_at);
            meta.push('at ' + d.toLocaleTimeString());
        }
        card.appendChild(el('div', 'di-row', meta.join(' | ')));

        // Clear button
        const actions = el('div', 'di-actions');
        const clearBtn = el('button', 'clear-btn', 'Clear');
        clearBtn.addEventListener('click', function() {
            clearDiscardedItem(item.id).then(function() { fetchHealth(); });
        });
        actions.appendChild(clearBtn);
        card.appendChild(actions);

        details.appendChild(card);
    }

    // Clear All button
    if (items.length > 1) {
        const clearAllBtn = el('button', 'clear-btn clear-all-btn', 'Clear All Discarded (' + count + ')');
        clearAllBtn.addEventListener('click', function() {
            clearDiscardedItem('all').then(function() { fetchHealth(); });
        });
        details.appendChild(clearAllBtn);
    }
}

async function clearDiscardedItem(itemId) {
    try {
        const resp = await fetch('/api/genesis/deferred/' + itemId + '/clear', { method: 'DELETE' });
        if (!resp.ok) {
            console.error('Clear failed:', resp.status);
        }
    } catch (e) {
        console.error('Clear request failed:', e);
    }
}

// ── Surplus detail (expandable) ─────────────────────────────────────

async function loadSurplusDetail(container) {
    container.replaceChildren();
    container.appendChild(el('div', null, 'Loading...'));

    try {
        const resp = await fetch('/api/genesis/surplus/config');
        if (!resp.ok) throw new Error('HTTP ' + resp.status);
        const data = await resp.json();
        container.replaceChildren();

        // Section 1: Task Catalog
        const catSection = el('div', 'detail-section');
        catSection.appendChild(el('h4', null, 'Task Catalog'));
        const catalog = data.catalog || [];
        for (const item of catalog) {
            const row = el('div', 'surplus-task-row');
            const typeName = el('span', 'task-type', item.type.replace(/_/g, ' '));
            row.appendChild(typeName);
            if (item.last_status) {
                const st = el('span', 'task-status', item.last_status);
                const sc = { completed: '#4ade80', failed: '#ef4444', pending: '#fbbf24', running: '#38bdf8' };
                st.style.color = sc[item.last_status] || '#888';
                row.appendChild(st);
            }
            if (item.last_run) {
                const age = Math.round((Date.now() - new Date(item.last_run).getTime()) / 1000);
                row.appendChild(el('span', 'task-time', formatAge(age) + ' ago'));
            }
            catSection.appendChild(row);
        }
        container.appendChild(catSection);

        // Section 2: Drive Weights
        const drives = data.drives || [];
        if (drives.length > 0) {
            const driveSection = el('div', 'detail-section');
            driveSection.appendChild(el('h4', null, 'Drive Weights'));
            for (const d of drives) {
                const name = d.drive_name || d[0];
                const weight = d.current_weight != null ? d.current_weight : d[1];
                const min = d.min_weight != null ? d.min_weight : 0.1;
                const max = d.max_weight != null ? d.max_weight : 0.5;
                const pct = max > min ? Math.round(((weight - min) / (max - min)) * 100) : 50;
                const row = el('div', 'section-row');
                row.appendChild(el('span', 'label', name));
                const bar = el('span', 'value');
                bar.innerHTML = '<span style="display:inline-block;width:60px;height:6px;background:#334155;border-radius:3px;vertical-align:middle;">' +
                    '<span style="display:block;width:' + pct + '%;height:100%;background:#4ade80;border-radius:3px;"></span></span> ' +
                    weight.toFixed(2);
                row.appendChild(bar);
                driveSection.appendChild(row);
            }
            container.appendChild(driveSection);
        }

        // Section 3: Eval Metrics
        const evalData = data.eval_staleness || {};
        const evalMetrics = data.eval_metrics || {};
        const providers = evalData.providers || [];
        const evalRuns = evalMetrics.runs || [];
        const j9Snaps = evalMetrics.j9 || [];

        if (providers.length > 0 || evalRuns.length > 0 || j9Snaps.length > 0) {
            const evalSection = el('div', 'detail-section');
            evalSection.appendChild(el('h4', null, 'Eval & Assessment'));

            // Provider freshness
            for (const p of providers) {
                const row = el('div', 'surplus-task-row');
                row.appendChild(el('span', 'task-type', p.provider));
                const age = p.days_ago != null ? p.days_ago + 'd ago' : 'no data';
                const ageColor = p.stale ? '#ef4444' : p.days_ago > 14 ? '#fbbf24' : '#4ade80';
                const ageEl = el('span', 'task-time', age);
                ageEl.style.color = ageColor;
                row.appendChild(ageEl);
                if (p.unreliable) {
                    const badge = el('span', 'follow-up-badge failed', 'unreliable');
                    row.appendChild(badge);
                }
                evalSection.appendChild(row);
            }

            // Recent eval run scores
            if (evalRuns.length > 0) {
                const runsHeader = el('div', null, '');
                runsHeader.style.cssText = 'margin-top:8px;font-size:11px;color:#94a3b8;';
                runsHeader.textContent = 'Recent Runs';
                evalSection.appendChild(runsHeader);
                for (const run of evalRuns.slice(0, 5)) {
                    const row = el('div', 'surplus-task-row');
                    row.appendChild(el('span', 'task-type', `${run.model_id} / ${run.dataset}`));
                    const score = run.aggregate_score != null ? Math.round(run.aggregate_score * 100) + '%' : '?';
                    const scoreColor = run.aggregate_score == null ? '#94a3b8' : run.aggregate_score >= 0.8 ? '#4ade80' : run.aggregate_score >= 0.6 ? '#fbbf24' : '#ef4444';
                    const passed = run.passed_cases || 0;
                    const failed = run.failed_cases || 0;
                    const scoreEl = el('span', 'task-time', `${score} (${passed}/${passed + failed})`);
                    scoreEl.style.color = scoreColor;
                    row.appendChild(scoreEl);
                    evalSection.appendChild(row);
                }
            }

            // J-9 dimension scores
            if (j9Snaps.length > 0) {
                const j9Header = el('div', null, '');
                j9Header.style.cssText = 'margin-top:8px;font-size:11px;color:#94a3b8;';
                j9Header.textContent = 'J-9 Dimensions';
                evalSection.appendChild(j9Header);
                for (const snap of j9Snaps) {
                    if (snap.dimension === 'composite') continue;
                    const row = el('div', 'surplus-task-row');
                    row.appendChild(el('span', 'task-type', snap.dimension));
                    const samples = el('span', 'task-time', `${snap.sample_count} samples`);
                    samples.style.color = snap.sample_count > 0 ? '#94a3b8' : '#ef4444';
                    row.appendChild(samples);
                    evalSection.appendChild(row);
                }
            }

            container.appendChild(evalSection);
        }

        // Section 4: Follow-ups
        await loadFollowUpsBadge(container);

    } catch (e) {
        container.replaceChildren();
        container.appendChild(el('div', null, 'Failed to load: ' + e.message));
        container.firstChild.style.cssText = 'color:#ef4444;font-size:11px;';
    }
}

async function loadFollowUpsBadge(container) {
    try {
        const resp = await fetch('/api/genesis/follow-ups/summary');
        if (!resp.ok) return;
        const data = await resp.json();
        const counts = data.counts || {};
        const total = data.total || 0;
        if (total === 0) return;

        const section = el('div', 'detail-section');
        section.appendChild(el('h4', null, 'Follow-ups'));

        const row = el('div', 'surplus-task-row');
        row.appendChild(el('span', 'task-type', total + ' total'));

        if (counts.pending) {
            const badge = el('span', 'follow-up-badge pending', counts.pending + ' pending');
            row.appendChild(badge);
        }
        if (counts.failed) {
            const badge = el('span', 'follow-up-badge failed', counts.failed + ' failed');
            row.appendChild(badge);
        }
        if (counts.blocked) {
            const badge = el('span', 'follow-up-badge blocked', counts.blocked + ' blocked');
            row.appendChild(badge);
        }
        section.appendChild(row);
        container.appendChild(section);
    } catch (e) {
        // Non-critical — silently skip
    }
}

// ── Subsystem group definitions ─────────────────────────────────────

const SUBSYSTEM_GROUPS = {
    ego: {
        name: 'Ego', domId: 'sub-ego',
        desc: 'Autonomous decision cycle \u2014 periodic thinking, proposals, adaptive cadence',
        sites: ['7_ego_cycle', '8_ego_compaction'],
    },
    reflection: {
        name: 'Reflection', domId: 'sub-reflection',
        desc: 'Periodic thinking cycles at micro, light, deep, and strategic depth',
        sites: ['3_micro_reflection', '4_light_reflection', '5_deep_reflection',
                '6_strategic_reflection', '14_weekly_self_assessment', '16_quality_calibration'],
    },
    memory: {
        name: 'Memory', domId: 'sub-memory',
        desc: 'Storage, fact extraction, embeddings, knowledge retrieval, and resume review',
        sites: ['9_fact_extraction', '21_embeddings', '21b_query_embedding',
                '41_resume_review_pass1', '42_resume_review_pass2'],
    },
    surplus: {
        name: 'Surplus', domId: 'sub-surplus',
        desc: 'Idle-time tasks \u2014 brainstorms, code audits, infrastructure monitoring',
        sites: ['12_surplus_brainstorm', '36_code_auditor', '37_infrastructure_monitor', 'bookmark_enrichment'],
    },
    outreach: {
        name: 'Outreach', domId: 'sub-outreach',
        desc: 'Morning reports, fresh-eyes reviews, and proactive Telegram notifications',
        sites: ['13_morning_report', '23_outreach_review'],
    },
    executor: {
        name: 'Executor', domId: 'sub-executor',
        desc: 'Task execution review \u2014 pre-assessment, adversarial challenge, autonomous dispatch',
        sites: ['27_pre_execution_assessment', '17_executor_review',
                '20_adversarial_counterargument', 'autonomous_executor_reasoning'],
    },
    learning: {
        name: 'Learning', domId: 'sub-learning',
        desc: 'Observation triage, calibration, outcome classification, skill refinement',
        sites: ['2_triage', '29_retrospective_triage',
                '30_triage_calibration', '31_outcome_classification', '32_delta_assessment',
                '33_skill_refiner', '38_procedure_extraction', '40_knowledge_distillation'],
    },
    contingency: {
        name: 'Contingency', domId: 'sub-contingency',
        desc: 'Fallback API routing when primary CC providers are unavailable',
        sites: ['contingency_foreground', 'contingency_micro'],
    },
    utility: {
        name: 'Utility', domId: 'sub-utility',
        desc: 'Housekeeping \u2014 email triage, tagging, state synthesis, content drafting',
        sites: ['cc_update_analysis', 'outreach_email_triage', '10_cognitive_state',
                '11_user_model_synthesis', '18_meta_prompting', '22_tagging',
                '28_observation_sweep', '34_research_synthesis', '35_content_draft'],
    },
};

function aggregateStatus(statuses) {
    const active = statuses.filter(s => s !== 'idle' && s !== 'disabled' && s !== 'unknown');
    if (active.length === 0) {
        if (statuses.includes('disabled')) return 'idle';
        if (statuses.includes('idle')) return 'idle';
        return 'unknown';
    }
    if (active.includes('down')) return 'down';
    if (active.includes('degraded') || active.includes('warning')) return 'degraded';
    if (active.some(s => s === 'healthy' || s === 'active')) return 'healthy';
    return 'unknown';
}

function statusDotColor(status) {
    const c = { healthy: '#4ade80', active: '#4ade80', warning: '#f59e0b', degraded: '#ffe600', down: '#ef4444', idle: '#888' };
    return c[status] || '#888';
}

const SUBSYSTEM_BORDER_COLORS = {
    ego: '#c084fc', reflection: '#818cf8', memory: '#38bdf8', surplus: '#4ade80',
    outreach: '#fbbf24', executor: '#a78bfa', learning: '#fb923c',
    contingency: '#f87171', utility: '#94a3b8',
};


function updateSubsystemGrid(data) {
    const container = document.getElementById('monitor-grid');
    if (!container) return;
    container.style.display = 'flex';

    const bgGrid = document.getElementById('battleship-grid');
    const enrichGrid = document.getElementById('enrichment-cards');
    if (!bgGrid || !enrichGrid) return;

    // Preserve surplus detail state before clearing
    const oldSurplus = document.getElementById('enrich-surplus');
    const surplusWasOpen = oldSurplus && oldSurplus.querySelector('.surplus-detail-content.open');

    bgGrid.replaceChildren();
    enrichGrid.replaceChildren();

    const callSites = data.call_sites || {};

    // Explicit 7×7 block layout — each subsystem occupies a compact 2D region
    // Positions: [col, row] (1-indexed). Order matches group.sites order.
    const GRID_POSITIONS = {
        utility:     [[1,1],[2,1],[3,1],[1,2],[2,2],[3,2],[1,3],[2,3],[3,3]],
        learning:    [[4,1],[5,1],[6,1],[7,1],[4,2],[5,2],[6,2],[7,2]],
        reflection:  [[4,3],[5,3],[6,3],[4,4],[5,4],[6,4]],
        ego:         [[7,3],[7,4],[7,5]],
        memory:      [[1,4],[3,4],[1,5],[2,5],[2,4]],
        surplus:     [[3,5],[4,5],[3,6],[4,6]],
        executor:    [[5,5],[6,5],[5,6],[6,6]],
        outreach:    [[3,7],[4,7]],
        contingency: [[5,7],[6,7]],
    };

    for (const [key, group] of Object.entries(SUBSYSTEM_GROUPS)) {
        // ── Top: flat grid cells, explicitly positioned ──────────────────
        const positions = GRID_POSITIONS[key] || [];
        for (let si = 0; si < group.sites.length; si++) {
            const siteId = group.sites[si];
            const [col, row] = positions[si] || [1, 1];
            const siteData = callSites[siteId] || {};
            const meta = getMeta(siteId);
            const status = siteData.status || 'unknown';
            const cell = el('div', 'bg-cell bg-' + key);
            cell.style.gridColumn = col;
            cell.style.gridRow = row;
            const dot = el('div', 'grid-dot ' + gridDotClass(status));
            dot.addEventListener('click', () => showDetail({ id: siteId, status, data: siteData }));
            const tipRows = [{ cls: 'dot-tip-name', text: meta.name }];
            if (status === 'idle') tipRows.push({ text: 'Groundwork' });
            else if (status === 'disabled') tipRows.push({ text: 'No API key' });
            else tipRows.push({ text: status });
            if (siteData.active_provider) tipRows.push({ text: siteData.active_provider });
            if (siteData.recent_failures) {
                const failAge = siteData.last_failure_at
                    ? formatAge(Math.round((Date.now() - new Date(siteData.last_failure_at).getTime()) / 1000)) + ' ago'
                    : 'recently';
                tipRows.push({ text: siteData.recent_failures + ' failure(s) in 30min, latest ' + failAge, style: 'color:#f59e0b' });
            }
            const descText = siteData.description || meta.desc;
            if (descText) tipRows.push({ text: descText, style: 'color:#94a3b8;margin-top:3px' });
            if (siteData.chain_health) {
                const apiChain = siteData.chain_health.filter(c => !c.is_cc);
                const ccChain = siteData.chain_health.filter(c => c.is_cc);
                const apiUp = apiChain.filter(c => {
                    const ps = c.probe_status;
                    return ps ? ps === 'reachable' : c.state !== 'open';
                }).length;
                let chainSummary = apiUp + '/' + apiChain.length + ' API';
                if (ccChain.length > 0) {
                    const ccPs = ccChain[0].probe_status;
                    const ccOk = ccPs ? ccPs !== 'unreachable' : ccChain[0].state !== 'open';
                    chainSummary += ', CC ' + (ccOk ? 'up' : 'down');
                }
                tipRows.push({ text: chainSummary });
            }
            dot.dataset.tip = JSON.stringify(tipRows);
            dot.addEventListener('mouseenter', () => showTip(dot));
            dot.addEventListener('mouseleave', hideTip);
            cell.appendChild(dot);
            cell.appendChild(el('div', 'bg-cell-label', meta.name));
            bgGrid.appendChild(cell);
        }

        // ── Bottom: enrichment card ──────────────────────────────────────
        const bColor = SUBSYSTEM_BORDER_COLORS[key] || '#475569';
        const card = el('div', 'enrich-card');
        card.id = 'enrich-' + key;
        card.style.borderLeftColor = bColor;

        // Card header: agg dot + name + count
        const memberStatuses = group.sites.map(id => (callSites[id] || {}).status || 'unknown');
        const aggStatus = aggregateStatus(memberStatuses);
        const activeCount = memberStatuses.filter(s => s === 'healthy' || s === 'active').length;

        const header = el('div', 'region-header');
        const aggDot = el('span', 'agg-dot');
        aggDot.style.background = statusDotColor(aggStatus);
        header.appendChild(aggDot);
        header.appendChild(el('h3', null, group.name));
        header.appendChild(el('span', 'region-count', activeCount + '/' + group.sites.length));
        card.appendChild(header);

        if (group.desc) card.appendChild(el('div', 'region-desc', group.desc));

        const enrichment = el('div', 'region-enrichment');
        let hasEnrichment = false;

        if (key === 'ego') {
            enrichment.appendChild(sectionRow('Status', 'Active', 'color:#c084fc;font-weight:600'));
            hasEnrichment = true;
        }
        if (key === 'reflection') {
            const aw = data.awareness || {};
            if (aw.ticks_24h !== undefined) { enrichment.appendChild(sectionRow('Ticks/24h', String(aw.ticks_24h))); hasEnrichment = true; }
            if (aw.time_since_last_tick_seconds != null) {
                enrichment.appendChild(sectionRow('Last Tick', formatAge(aw.time_since_last_tick_seconds) + ' ago'));
                hasEnrichment = true;
            }
            if (aw.depth_distribution_24h) {
                const dist = Object.entries(aw.depth_distribution_24h).map(([k,v]) => k + ':' + v).join(', ');
                if (dist) { enrichment.appendChild(sectionRow('Depths', dist)); hasEnrichment = true; }
            }
        }
        if (key === 'memory') {
            const q = data.queues || {};
            const mh = data.memory_health || {};
            const pm = data.proactive_memory || {};

            const links = mh.links || {};
            if (q.embedded_total || links.total) {
                const parts = [];
                if (q.embedded_total) parts.push(q.embedded_total + ' vectors');
                if (links.total) parts.push(links.total.toLocaleString() + ' links');
                enrichment.appendChild(sectionRow('Store', parts.join(' \u00B7 ')));
                hasEnrichment = true;
            }

            const ek = mh.essential_knowledge || {};
            if (ek.age_hours !== null && ek.age_hours !== undefined) {
                const age = ek.age_hours;
                const c = age > 24 ? 'color:#ef4444' : age > 6 ? 'color:#ffe600' : 'color:#4ade80';
                const size = ek.size_bytes ? ' \u00B7 ' + (ek.size_bytes / 1024).toFixed(1) + 'KB' : '';
                enrichment.appendChild(sectionRow('L1 EK', age.toFixed(1) + 'h ago' + size, c));
                hasEnrichment = true;
            }

            if (pm.latency_ms !== undefined) {
                const lat = pm.latency_ms;
                const fts = pm.fts5_results || 0;
                const vec = pm.vector_results || 0;
                const fused = pm.fused_results || 0;
                enrichment.appendChild(sectionRow('L2 Hook', lat + 'ms \u00B7 FTS:' + fts + ' Vec:' + vec + ' \u2192 ' + fused));
                hasEnrichment = true;
            }

            const wings = mh.wings || [];
            if (wings.length > 0) {
                const wStr = wings.slice(0, 5).map(w => w[0] + ':' + w[1]).join(' ');
                enrichment.appendChild(sectionRow('Wings', wStr));
                hasEnrichment = true;
            }

            const classes = mh.classes || [];
            if (classes.length > 0) {
                const cStr = classes.map(c => c[0] + ':' + c[1]).join(' ');
                enrichment.appendChild(sectionRow('Classes', cStr));
                hasEnrichment = true;
            }

            const conf = mh.confidence || {};
            if (conf.high !== undefined) {
                enrichment.appendChild(sectionRow('Confidence', 'hi:' + conf.high + ' med:' + conf.medium + ' lo:' + conf.low));
                hasEnrichment = true;
            }

            if (links.by_type && links.by_type.length > 0) {
                const lStr = links.by_type.slice(0, 4).map(l => l[0] + ':' + l[1]).join(' ');
                enrichment.appendChild(sectionRow('Link Types', lStr));
                hasEnrichment = true;
            }

            const ext = mh.extraction || {};
            if (ext.total_sessions !== undefined) {
                const c = ext.coverage_pct >= 90 ? 'color:#4ade80' : ext.coverage_pct >= 50 ? 'color:#ffe600' : 'color:#ef4444';
                enrichment.appendChild(sectionRow('Extraction', ext.extracted + '/' + ext.total_sessions + ' \u00B7 ' + ext.coverage_pct + '%', c));
                hasEnrichment = true;
            }

            const ci = mh.code_index || {};
            if (ci.modules) {
                const age = ci.age_hours;
                const c = age > 24 ? 'color:#ffe600' : 'color:#4ade80';
                enrichment.appendChild(sectionRow('Code Index', ci.modules + ' mod \u00B7 ' + (ci.symbols || 0).toLocaleString() + ' sym \u00B7 ' + (age ? age + 'h' : '?'), c));
                hasEnrichment = true;
            }

            if (q.pending_embeddings) { enrichment.appendChild(sectionRow('Pending', String(q.pending_embeddings), 'color:#ffe600')); hasEnrichment = true; }
            if (q.failed_embeddings) { enrichment.appendChild(sectionRow('Failed', String(q.failed_embeddings), 'color:#ef4444')); hasEnrichment = true; }

            if (mh.growth) {
                const g = mh.growth;
                if (g.last_7d !== undefined) { enrichment.appendChild(sectionRow('+7d', String(g.last_7d))); hasEnrichment = true; }
                if (g.avg_per_day_7d !== undefined) { enrichment.appendChild(sectionRow('Avg/day', String(g.avg_per_day_7d))); hasEnrichment = true; }
            }

            if (mh.orphans && mh.orphans.orphan_pct !== undefined) {
                const pct = mh.orphans.orphan_pct;
                const c = pct > 50 ? 'color:#ef4444' : pct > 30 ? 'color:#ffe600' : 'color:#4ade80';
                enrichment.appendChild(sectionRow('Orphans', pct.toFixed(1) + '%', c));
                hasEnrichment = true;
            }
        }
        if (key === 'surplus') {
            const s = data.surplus || {};
            if (s.status && s.status !== 'unknown') {
                const c = s.status === 'idle' ? '#4ade80' : s.status === 'dispatching' ? '#ffe600' : '#888';
                enrichment.appendChild(sectionRow('Scheduler', s.status, 'color:' + c));
                hasEnrichment = true;
            }
            if (s.queue_depth !== undefined) { enrichment.appendChild(sectionRow('Queue', String(s.queue_depth))); hasEnrichment = true; }
            if (s.tasks_completed_24h !== undefined) { enrichment.appendChild(sectionRow('Done/24h', String(s.tasks_completed_24h))); hasEnrichment = true; }
            if (s.tasks_failed_24h) { enrichment.appendChild(sectionRow('Failed/24h', String(s.tasks_failed_24h), 'color:#ef4444')); hasEnrichment = true; }

            const toggle = el('div', 'surplus-detail-toggle');
            toggle.appendChild(el('span', null, 'Details'));
            const arrow = el('span', 'arrow', '\u25B6');
            toggle.appendChild(arrow);
            enrichment.appendChild(toggle);
            hasEnrichment = true;

            const detailContent = el('div', 'surplus-detail-content');
            enrichment.appendChild(detailContent);

            toggle.addEventListener('click', function() {
                const isOpen = detailContent.classList.toggle('open');
                arrow.classList.toggle('open', isOpen);
                if (isOpen && !detailContent.dataset.loaded) {
                    detailContent.dataset.loaded = '1';
                    loadSurplusDetail(detailContent);
                }
            });

            if (surplusWasOpen) {
                detailContent.classList.add('open');
                arrow.classList.add('open');
                detailContent.dataset.loaded = '1';
                loadSurplusDetail(detailContent);
            }
        }
        if (key === 'outreach') {
            const o = data.outreach_stats || {};
            if (o.total !== undefined) { enrichment.appendChild(sectionRow('Sent (7d)', String(o.total))); hasEnrichment = true; }
            if (o.delivery_rate != null) { enrichment.appendChild(sectionRow('Delivery', Math.round(o.delivery_rate * 100) + '%')); hasEnrichment = true; }
            if (o.response_rate != null) { enrichment.appendChild(sectionRow('Response Rate', Math.round(o.response_rate * 100) + '%')); hasEnrichment = true; }
        }
        if (key === 'contingency') {
            const cc = data.cc_sessions || {};
            if (cc.realtime_status) {
                const c = cc.realtime_status === 'NORMAL' ? '#4ade80' : '#ffe600';
                enrichment.appendChild(sectionRow('CC Status', cc.realtime_status, 'color:' + c));
                hasEnrichment = true;
            }
        }

        // Per-call-site enrichment: last run, providers, cost, routing, status
        const siteRuns = [];
        for (const siteId of group.sites) {
            const sd = callSites[siteId] || {};
            if (sd.last_run_at) {
                const age = Math.round((Date.now() - new Date(sd.last_run_at).getTime()) / 1000);
                siteRuns.push({ id: siteId, age, provider: sd.active_provider || sd.last_run_provider });
            }
        }
        if (siteRuns.length > 0) {
            siteRuns.sort((a, b) => a.age - b.age);
            const latest = siteRuns[0];
            const meta = getMeta(latest.id);
            enrichment.appendChild(sectionRow('Last Run', meta.name + ' ' + formatAge(latest.age) + ' ago'));
            hasEnrichment = true;
            const providers = [...new Set(siteRuns.map(r => r.provider).filter(Boolean))];
            if (providers.length > 0) {
                enrichment.appendChild(sectionRow('Providers', providers.slice(0, 3).join(', ')));
                hasEnrichment = true;
            }
        }

        const costBySite = (data.cost || {}).cost_by_call_site || [];
        const siteSet = new Set(group.sites);
        let groupCalls = 0, groupCost = 0;
        for (const entry of costBySite) {
            if (siteSet.has(entry.call_site)) {
                groupCalls += entry.calls || 0;
                groupCost += entry.month_usd || 0;
            }
        }
        if (groupCalls > 0) {
            enrichment.appendChild(sectionRow('Calls', String(groupCalls) + ' \u00B7 $' + groupCost.toFixed(3)));
            hasEnrichment = true;
        }

        let totalProviders = 0, reachableProviders = 0;
        for (const siteId of group.sites) {
            const sd = callSites[siteId] || {};
            const ch = sd.chain_health || [];
            for (const c of ch) {
                if (c.is_cc) continue;
                totalProviders++;
                const ok = c.probe_status ? c.probe_status === 'reachable' : c.state !== 'open';
                if (ok) reachableProviders++;
            }
        }
        if (totalProviders > 0) {
            const rColor = reachableProviders === totalProviders ? 'color:#4ade80' :
                           reachableProviders > 0 ? 'color:#ffe600' : 'color:#ef4444';
            enrichment.appendChild(sectionRow('Routing', reachableProviders + '/' + totalProviders + ' providers ready', rColor));
            hasEnrichment = true;
        }

        const statusCounts = {};
        for (const siteId of group.sites) {
            const st = (callSites[siteId] || {}).status || 'unknown';
            statusCounts[st] = (statusCounts[st] || 0) + 1;
        }
        const healthyCount = (statusCounts.healthy || 0) + (statusCounts.active || 0);
        const idleCount = (statusCounts.idle || 0) + (statusCounts.disabled || 0);
        if (healthyCount < group.sites.length && idleCount > 0) {
            enrichment.appendChild(sectionRow('Sites', healthyCount + ' active, ' + idleCount + ' idle', 'color:#94a3b8'));
            hasEnrichment = true;
        }

        if (hasEnrichment) card.appendChild(enrichment);
        enrichGrid.appendChild(card);
    }
}

// ── Polling ─────────────────────────────────────────────────────────

function tickUpdateTimer() {
    const elapsed = Math.round((Date.now() - lastFetchTime) / 1000);
    document.getElementById('last-update').textContent = elapsed + 's ago';
}

function updateHeaderBadges(data) {
    // Resilience badge
    const hbRes = document.getElementById('hb-resilience');
    if (hbRes) {
        const resObj = data.resilience || {};
        const res = (typeof resObj === 'object' ? resObj.level : resObj) || 'unknown';
        const isOk = res === 'L0' || res === 'normal' || res === 'not configured';
        hbRes.style.display = 'inline-flex';
        hbRes.style.background = isOk ? 'rgba(22,101,52,0.5)' : 'rgba(133,77,14,0.5)';
        hbRes.querySelector('.hb-dot').style.background = isOk ? '#4ade80' : '#facc15';
        hbRes.querySelector('.hb-text').textContent = res;
        hbRes.querySelector('.hb-text').style.color = isOk ? '#4ade80' : '#facc15';
    }
    // CC status badge
    const hbCc = document.getElementById('hb-cc');
    if (hbCc && data.cc_sessions) {
        const ccSt = data.cc_sessions.realtime_status || 'unknown';
        const ccOk = ccSt === 'NORMAL';
        hbCc.style.display = 'inline-flex';
        hbCc.style.background = ccOk ? 'rgba(22,101,52,0.5)' : 'rgba(133,77,14,0.5)';
        hbCc.querySelector('.hb-dot').style.background = ccOk ? '#4ade80' : '#facc15';
        hbCc.querySelector('.hb-text').textContent = 'CC ' + ccSt;
        hbCc.querySelector('.hb-text').style.color = ccOk ? '#4ade80' : '#facc15';
    }
    // Call sites health summary
    const hbSites = document.getElementById('hb-sites');
    if (hbSites && data.call_sites) {
        const sites = Object.values(data.call_sites);
        const healthy = sites.filter(s => s.status === 'healthy' || s.status === 'active').length;
        const total = sites.length;
        const allOk = healthy === total;
        hbSites.style.display = 'inline-flex';
        hbSites.style.background = allOk ? 'rgba(22,101,52,0.4)' : 'rgba(30,41,59,0.6)';
        hbSites.querySelector('.hb-text').textContent = healthy + '/' + total + ' sites';
        hbSites.querySelector('.hb-text').style.color = allOk ? '#4ade80' : 'var(--color-text-secondary)';
    }
}

async function fetchHealth() {
    try {
        const resp = await fetch('/api/genesis/health');
        if (!resp.ok) throw new Error('HTTP ' + resp.status);
        lastData = await resp.json();
        lastFetchTime = Date.now();

        document.getElementById('loading').style.display = 'none';
        updateSidePanel(lastData);
        updateSubsystemGrid(lastData);
        updateHeaderBadges(lastData);
    } catch (e) {
        console.error('Health fetch failed:', e);
        document.getElementById('loading').textContent = 'Connection error \u2014 retrying...';
        document.getElementById('loading').style.display = 'block';
    }
}

// ── Pause / Kill Switch ──────────────────────────────
let pauseState = { paused: false, reason: null, since: null };

async function fetchPauseState() {
    try {
        const resp = await fetch('/api/genesis/pause');
        if (resp.ok) {
            pauseState = await resp.json();
            renderPauseFooter();
        }
    } catch (e) { console.warn('Pause state fetch failed:', e); }
}

function renderPauseFooter() {
    const dot = document.getElementById('pause-dot');
    const label = document.getElementById('pause-label');
    const since = document.getElementById('pause-since');
    const btn = document.getElementById('pause-btn');
    if (!dot || !label || !btn) return;
    if (pauseState.paused) {
        dot.style.background = '#ffa726';
        label.textContent = 'PAUSED';
        label.style.color = '#ffa726';
        since.textContent = pauseState.since ? 'since ' + new Date(pauseState.since).toLocaleTimeString() : '';
        btn.textContent = 'Resume Genesis';
        btn.style.color = '#66bb6a';
        btn.style.borderColor = 'rgba(102,187,106,0.3)';
        btn.style.background = 'rgba(102,187,106,0.12)';
    } else {
        dot.style.background = '#66bb6a';
        label.textContent = 'RUNNING';
        label.style.color = '#66bb6a';
        since.textContent = '';
        btn.textContent = 'Pause Genesis';
        btn.style.color = '#ffa726';
        btn.style.borderColor = 'rgba(255,167,38,0.3)';
        btn.style.background = 'rgba(255,167,38,0.12)';
    }
}

async function togglePause() {
    const newState = !pauseState.paused;
    const pauseMsg = "Pause Genesis?\n\nThis will stop ALL background activity:\n\u2022 Reflections\n\u2022 Surplus tasks\n\u2022 Outreach\n\u2022 Inbox monitoring\n\nConversations still work but without background enrichment.\nYou must manually resume via this button or /pause off in Telegram.";
    const resumeMsg = "Resume Genesis?\n\nAll background activity will restart\n(reflections, surplus tasks, outreach, inbox monitoring).";
    if (!confirm(newState ? pauseMsg : resumeMsg)) return;
    try {
        const resp = await fetch('/api/genesis/pause', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ paused: newState, reason: 'Dashboard toggle' }),
        });
        if (resp.ok) {
            await fetchPauseState();
        } else {
            const err = await resp.json().catch(() => ({}));
            alert('Pause toggle failed: ' + (err.message || 'HTTP ' + resp.status));
        }
    } catch (e) {
        alert('Pause toggle failed: ' + e.message);
    }
}

fetchHealth();
fetchPauseState();
setInterval(fetchHealth, 15000);
setInterval(fetchPauseState, 15000);
setInterval(tickUpdateTimer, 1000);
</script>

<!-- Kill Switch Footer -->
<div id="pause-footer" style="position:fixed;bottom:0;left:0;right:0;z-index:1000;
    background:#1a1a2e;border-top:1px solid #333;padding:0.5rem 1.5rem;
    display:flex;align-items:center;justify-content:space-between;font-size:0.78rem;">
  <div style="display:flex;align-items:center;gap:0.75rem;">
    <span id="pause-dot" style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#66bb6a;"></span>
    <span id="pause-label" style="color:#66bb6a;font-weight:600;">RUNNING</span>
    <span id="pause-since" style="color:#888;"></span>
  </div>
  <button id="pause-btn" onclick="togglePause()"
    style="font-size:0.74rem;padding:0.35rem 0.9rem;border-radius:4px;cursor:pointer;
           font-weight:500;color:#ffa726;border:1px solid rgba(255,167,38,0.3);
           background:rgba(255,167,38,0.12);">Pause Genesis</button>
</div>
<div style="height:50px;"></div>

</body>
</html>
