<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Fun-Doc Command Center</title>
    <style>
        :root {
            --bg-darkest: #0a0a0c; --bg-dark: #111114; --bg-medium: #1a1a1f;
            --bg-light: #222228; --bg-hover: #2a2a32;
            --border-dark: #2d2d35; --border-light: #3d3d45;
            --color-magic: #6969FF; --color-rare: #FFFF00; --color-set: #00FF00;
            --color-crafted: #FF6600;
            --text-primary: #d4d4d4; --text-secondary: #8b8b8b; --text-muted: #5a5a5a;
            --accent-gold: #C7B377; --accent-red: #8b2020; --accent-red-light: #c93030;
            --font-main: 'Segoe UI', -apple-system, sans-serif;
            --font-mono: 'Consolas', 'Monaco', monospace;
        }
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: var(--font-main); background: var(--bg-darkest); color: var(--text-primary); min-height: 100vh; display: flex; flex-direction: column; }

        /* Header */
        .header { background: linear-gradient(180deg, var(--bg-medium), var(--bg-dark)); border-bottom: 1px solid var(--accent-red); padding: 0.5rem 1rem; display: flex; align-items: center; gap: 0.75rem; }
        .header-logo { font-size: 1rem; font-weight: 600; color: var(--accent-gold); }
        .header-controls { display: flex; gap: 0.5rem; margin-left: 0.5rem; }
        .header-info { margin-left: auto; color: var(--text-muted); font-size: 0.75rem; font-family: var(--font-mono); }
        .live-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-right: 4px; background: var(--text-muted); }
        .live-dot.connected { background: var(--color-set); animation: pulse 2s infinite; }
        .live-dot.disconnected { background: var(--accent-red-light); }
        @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }

        .main-container { flex: 1; padding: 1rem; overflow-y: auto; max-width: 1400px; margin: 0 auto; width: 100%; }

        /* Buttons */
        .btn { padding: 4px 10px; font-size: 0.7rem; border-radius: 3px; border: 1px solid var(--border-dark); background: var(--bg-medium); color: var(--text-secondary); cursor: pointer; font-family: var(--font-mono); transition: all 0.15s; }
        .btn:hover { border-color: var(--accent-gold); color: var(--accent-gold); }
        .btn.btn-primary { border-color: var(--accent-gold); color: var(--accent-gold); }
        .btn.btn-danger { border-color: var(--accent-red-light); color: var(--accent-red-light); }
        .btn:disabled { opacity: 0.4; cursor: not-allowed; }
        .btn.icon-btn { width: 24px; height: 24px; padding: 0; display: inline-flex; align-items: center; justify-content: center; flex: 0 0 24px; line-height: 1; }
        .btn.icon-btn.is-active { border-color: var(--accent-gold); color: var(--accent-gold); }
        .icon-stop { width: 9px; height: 9px; border-radius: 1px; background: currentColor; display: block; }
        .icon-focus { width: 13px; height: 13px; border: 1px solid currentColor; border-radius: 50%; position: relative; display: block; }
        .icon-focus::before, .icon-focus::after { content: ""; position: absolute; background: currentColor; left: 50%; top: 50%; transform: translate(-50%, -50%); }
        .icon-focus::before { width: 15px; height: 1px; }
        .icon-focus::after { width: 1px; height: 15px; }

        /* Stats cards */
        .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 0.6rem; margin-bottom: 1rem; }
        .stat-card { background: var(--bg-medium); border: 1px solid var(--border-dark); border-radius: 6px; padding: 0.6rem; text-align: center; }
        .stat-value { font-size: 1.6rem; font-weight: 700; color: var(--accent-gold); font-family: var(--font-mono); }
        .stat-value.text-set { color: var(--color-set); } .stat-value.text-rare { color: var(--color-rare); }
        .stat-value.text-crafted { color: var(--color-crafted); } .stat-value.text-magic { color: var(--color-magic); }
        .stat-label { font-size: 0.65rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 1px; }

        /* Progress bar */
        .progress-container { margin-bottom: 1rem; }
        .progress-bar-outer { width: 100%; height: 22px; background: var(--bg-dark); border: 1px solid var(--border-dark); border-radius: 4px; overflow: hidden; position: relative; }
        .progress-bar-inner { height: 100%; background: linear-gradient(90deg, var(--accent-red) 0%, var(--color-crafted) 40%, var(--accent-gold) 70%, var(--color-set) 100%); transition: width 0.5s; border-radius: 3px; }
        .progress-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-family: var(--font-mono); font-size: 0.75rem; font-weight: 600; text-shadow: 0 0 4px rgba(0,0,0,0.8); }

        /* Status banner */
        .status-banner { display: none; background: var(--bg-light); border: 1px solid var(--border-dark); border-radius: 4px; padding: 0.4rem 0.8rem; margin-bottom: 0.75rem; font-size: 0.8rem; font-family: var(--font-mono); color: var(--text-secondary); }
        .status-banner.active { display: flex; flex-direction: column; gap: 0.4rem; }
        .status-banner .banner-row { display: flex; align-items: center; gap: 0.5rem; }
        .status-banner .spinner { width: 14px; height: 14px; border: 2px solid var(--border-light); border-top-color: var(--accent-gold); border-radius: 50%; animation: spin 0.8s linear infinite; flex-shrink: 0; }
        .status-banner .scan-progress-bars { display: none; width: 100%; gap: 0.3rem; flex-direction: column; }
        .status-banner .scan-progress-bars.active { display: flex; }
        .status-banner .scan-bar-label { font-size: 0.65rem; color: var(--text-muted); display: flex; justify-content: space-between; }
        .status-banner .scan-bar-track { width: 100%; height: 8px; background: var(--bg-darkest); border: 1px solid var(--border-dark); border-radius: 3px; overflow: hidden; }
        .status-banner .scan-bar-fill { height: 100%; background: linear-gradient(90deg, var(--color-crafted), var(--accent-gold), var(--color-set)); transition: width 0.2s; border-radius: 2px; }
        .status-banner .scan-bar-fill.inner { background: var(--color-magic); }
        @keyframes spin { to { transform: rotate(360deg); } }

        /* Activity feed */
        .activity-feed { max-height: 200px; overflow-y: auto; background: var(--bg-darkest); border: 1px solid var(--border-dark); border-radius: 4px; padding: 0.4rem; font-family: var(--font-mono); font-size: 0.7rem; line-height: 1.4; }
        .activity-entry { padding: 1px 4px; border-radius: 2px; white-space: pre-wrap; word-break: break-all; }
        .activity-started { color: var(--accent-gold); }
        .activity-tool { color: var(--text-secondary); }
        .activity-text { color: var(--text-primary); }
        .activity-success { color: var(--color-set); }
        .activity-error { color: var(--accent-red-light); }
        .activity-neutral { color: var(--text-muted); }
        .activity-scan { color: var(--color-magic); }

        /* Panels */
        .panel { background: var(--bg-dark); border: 1px solid var(--border-dark); border-radius: 6px; margin-bottom: 0.75rem; }
        .panel-header { padding: 0.5rem 0.8rem; background: linear-gradient(180deg, var(--bg-light), var(--bg-medium)); border-bottom: 1px solid var(--border-dark); border-radius: 6px 6px 0 0; display: flex; align-items: center; justify-content: space-between; }
        .panel-header h2 { font-size: 0.75rem; font-weight: 600; color: var(--accent-gold); text-transform: uppercase; letter-spacing: 1px; }
        .panel-content { padding: 0.6rem; }

        .two-col { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
        .three-col { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.75rem; }
        @media (max-width: 1100px) { .three-col { grid-template-columns: 1fr 1fr; } }
        @media (max-width: 800px) { .two-col, .three-col { grid-template-columns: 1fr; } }

        /* Score bars */
        .score-bar-row { display: flex; align-items: center; margin-bottom: 2px; gap: 5px; }
        .score-bar-label { font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-secondary); width: 42px; text-align: right; }
        .score-bar-track { flex: 1; height: 14px; background: var(--bg-darkest); border-radius: 3px; overflow: hidden; }
        .score-bar-fill { height: 100%; border-radius: 3px; }
        .score-bar-fill.s100 { background: var(--color-set); } .score-bar-fill.s90 { background: #44cc44; }
        .score-bar-fill.s80 { background: var(--accent-gold); } .score-bar-fill.s70 { background: var(--color-rare); }
        .score-bar-fill.s60 { background: var(--color-crafted); } .score-bar-fill.s50 { background: #cc6600; }
        .score-bar-fill.s40 { background: var(--accent-red-light); } .score-bar-fill.s30 { background: var(--accent-red); }
        .score-bar-fill.s20 { background: #661111; } .score-bar-fill.s10 { background: #441111; } .score-bar-fill.s0 { background: #330808; }
        .score-bar-count { font-family: var(--font-mono); font-size: 0.65rem; color: var(--text-muted); width: 32px; }

        /* Tables */
        .data-table { width: 100%; border-collapse: separate; border-spacing: 0; }
        .data-table th, .data-table td { padding: 0.35rem 0.5rem; border-bottom: 1px solid var(--border-dark); text-align: left; font-size: 0.7rem; }
        .data-table th { background: var(--bg-medium); color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; font-size: 0.6rem; position: sticky; top: 0; }
        .data-table tbody tr:hover { background: var(--bg-hover); }
        .mono { font-family: var(--font-mono); font-size: 0.65rem; }

        .badge { display: inline-block; padding: 1px 4px; border-radius: 3px; font-size: 0.6rem; font-weight: 600; font-family: var(--font-mono); }
        .badge-leaf { background: rgba(105,105,255,0.2); color: var(--color-magic); border: 1px solid rgba(105,105,255,0.3); }
        .badge-done { background: rgba(0,255,0,0.15); color: var(--color-set); border: 1px solid rgba(0,255,0,0.25); }
        .badge-fix { background: rgba(255,255,0,0.15); color: var(--color-rare); border: 1px solid rgba(255,255,0,0.25); }
        .badge-new { background: rgba(200,32,32,0.15); color: var(--accent-red-light); border: 1px solid rgba(200,32,32,0.25); }
        .badge-pinned { background: rgba(199,179,119,0.2); color: var(--accent-gold); border: 1px solid rgba(199,179,119,0.3); }

        .score-cell { font-family: var(--font-mono); font-weight: 600; }
        .score-100 { color: var(--color-set); } .score-90 { color: #44cc44; }
        .score-70 { color: var(--color-rare); } .score-50 { color: var(--color-crafted); } .score-low { color: var(--accent-red-light); }

        .func-list-scroll { max-height: 400px; overflow-y: auto; }

        /* Deduction rows */
        .deduction-row { display: flex; align-items: center; padding: 0.25rem 0; border-bottom: 1px solid var(--border-dark); gap: 0.4rem; }
        .deduction-cat { flex: 1; font-family: var(--font-mono); font-size: 0.7rem; }
        .deduction-stat { font-family: var(--font-mono); font-size: 0.65rem; color: var(--text-muted); width: 70px; text-align: right; }
        .deduction-bar { width: 80px; height: 10px; background: var(--bg-darkest); border-radius: 2px; overflow: hidden; }
        .deduction-bar-fill { height: 100%; background: var(--color-crafted); border-radius: 2px; }

        /* Provider cards */
        .provider-card { display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0; border-bottom: 1px solid var(--border-dark); }
        .provider-name { font-family: var(--font-mono); font-size: 0.75rem; color: var(--accent-gold); width: 70px; }
        .provider-stat { font-family: var(--font-mono); font-size: 0.7rem; color: var(--text-secondary); }

        /* Filter */
        .filter-bar { display: flex; gap: 0.4rem; margin-bottom: 0.5rem; flex-wrap: wrap; }
        .filter-input { padding: 0.2rem 0.5rem; font-size: 0.65rem; border-radius: 4px; border: 1px solid var(--border-dark); background: var(--bg-darkest); color: var(--text-primary); font-family: var(--font-mono); width: 160px; }
        .filter-input:focus { outline: none; border-color: var(--accent-gold); }

        .header-select { padding: 3px 6px; font-size: 0.7rem; border-radius: 3px; border: 1px solid var(--border-dark); background: var(--bg-darkest); color: var(--text-primary); font-family: var(--font-mono); cursor: pointer; }
        .header-select:focus { outline: none; border-color: var(--accent-gold); }
        .header-sep { border-left: 1px solid var(--border-dark); height: 18px; margin: 0 3px; }
        .worker-input { padding: 3px 6px; font-size: 0.7rem; border-radius: 3px; border: 1px solid var(--border-dark); background: var(--bg-darkest); color: var(--text-primary); font-family: var(--font-mono); width: 45px; text-align: center; }
        .worker-cards { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
        .worker-card { background: var(--bg-medium); border: 1px solid var(--border-dark); border-radius: 6px; padding: 0.5rem 0.7rem; min-width: 200px; position: relative; }
        .worker-card.running { border-color: var(--color-set); }
        .worker-card.stopping { border-color: var(--color-rare); }
        .worker-card .wc-title { font-family: var(--font-mono); font-size: 0.75rem; color: var(--accent-gold); margin-bottom: 0.15rem; }
        .worker-card .wc-detail { font-family: var(--font-mono); font-size: 0.6rem; color: var(--text-secondary); }
        .worker-card .wc-current { font-family: var(--font-mono); font-size: 0.6rem; color: var(--text-primary); margin-top: 0.15rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 220px; }
        .worker-card .btn-stop { position: absolute; top: 0.4rem; right: 0.4rem; }
        .worker-pane { background: var(--bg-dark); border: 1px solid var(--border-dark); border-radius: 6px; display: flex; flex-direction: column; min-height: 100px; max-height: 180px; }
        .worker-pane.running { border-color: var(--color-set); }
        .worker-pane.timeout { border-color: var(--accent-red-light); box-shadow: 0 0 8px rgba(201, 48, 48, 0.2); }
        .worker-pane.focused { border-color: var(--accent-gold); border-width: 2px; box-shadow: 0 0 8px rgba(199, 179, 119, 0.3); }
        .worker-pane.stopping { border-color: var(--color-rare); }
        .worker-pane.finished { border-color: var(--text-muted); opacity: 0.85; }
        .worker-pane.quota_paused { border-color: #c9a747; box-shadow: 0 0 6px rgba(201, 167, 71, 0.18); }
        .worker-pane.quota_paused .wp-status { color: #c9a747; }
        .sortable-header { cursor: pointer; user-select: none; white-space: nowrap; }
        .sortable-header:hover { color: var(--accent-gold); }
        .sortable-header span { font-size: 0.6rem; color: var(--accent-gold); }
        .focus-banner { display: none; background: var(--bg-medium); border: 1px solid var(--accent-gold); border-radius: 4px; padding: 0.3rem 0.8rem; margin-bottom: 0.75rem; font-family: var(--font-mono); font-size: 0.7rem; color: var(--accent-gold); align-items: center; gap: 0.5rem; }
        .focus-banner.active { display: flex; }
        .focus-banner .focus-func { color: var(--text-primary); font-weight: 600; }
        .focus-banner .focus-addr { color: var(--text-secondary); }
        .worker-pane-header { padding: 0.25rem 0.5rem; background: linear-gradient(180deg, var(--bg-light), var(--bg-medium)); border-bottom: 1px solid var(--border-dark); border-radius: 6px 6px 0 0; display: flex; align-items: center; gap: 0.4rem; font-size: 0.65rem; }
        .worker-pane-header .wp-title { font-family: var(--font-mono); color: var(--text-primary); font-weight: 600; }
        .worker-pane-header .wp-status { font-family: var(--font-mono); color: var(--text-muted); font-size: 0.6rem; }
        .worker-pane-header .wp-status .wp-current { color: var(--accent-gold); }
        .worker-pane-header .wp-alert { font-family: var(--font-mono); font-size: 0.55rem; color: var(--accent-red-light); border: 1px solid rgba(201, 48, 48, 0.4); background: rgba(139, 32, 32, 0.18); border-radius: 3px; padding: 1px 4px; display: none; }
        .worker-pane-header .wp-alert.active { display: inline-block; }
        .worker-pane-header .wp-actions { margin-left: auto; display: flex; gap: 3px; }
        .worker-pane-output { flex: 1; overflow-y: auto; padding: 0.2rem 0.3rem; font-family: var(--font-mono); font-size: 0.6rem; line-height: 1.25; background: var(--bg-darkest); }
        /* Per-function block: three-sided gold bracket (top + left + bottom)
           wrapping the header name, body lines, and footer name. Open right
           preserves the streaming/log feel. margin-bottom acts as the
           between-block breathing space (Q4: blank line between functions). */
        .fn-block { border-top: 1px solid var(--accent-gold); border-left: 1px solid var(--accent-gold); border-bottom: 1px solid var(--accent-gold); border-right: none; border-top-right-radius: 0; border-bottom-right-radius: 0; margin: 0 0 0.5rem 0; padding: 0; }
        .fn-block .fn-name { color: var(--accent-gold); font-weight: 600; padding: 1px 4px 1px 6px; }
        .fn-block .fn-name.fn-header { border-bottom: 1px dashed rgba(199, 179, 119, 0.18); }
        .fn-block .fn-name.fn-footer { border-top: 1px dashed rgba(199, 179, 119, 0.18); }
        .fn-block .fn-body { padding-left: 1rem; }
        .fn-block .fn-body .activity-entry { padding-left: 0; }
        /* Q5=C: bold section heading for the binary boundary in
           continuous globals workers. Visible break, gold accent, mild
           top/bottom padding so it stands out from per-item blocks. */
        .binary-section-heading {
            color: var(--accent-gold);
            font-weight: 700;
            text-transform: uppercase;
            letter-spacing: 0.05em;
            font-size: 0.72rem;
            padding: 0.5rem 0.4rem 0.4rem;
            margin: 0.6rem 0 0.4rem;
            border-top: 2px solid var(--accent-gold);
            border-bottom: 1px dashed rgba(199, 179, 119, 0.35);
            background: rgba(199, 179, 119, 0.05);
        }
        .settings-popout { max-height: 0; overflow: hidden; background: linear-gradient(180deg, var(--bg-medium), var(--bg-dark)); border-bottom: 1px solid transparent; transition: max-height 0.18s ease, border-color 0.18s ease; }
        .settings-popout.active { max-height: 420px; border-bottom-color: var(--border-dark); }
        .settings-popout-inner { max-width: 1400px; margin: 0 auto; width: 100%; padding: 0 1rem; }
        .settings-popout-panel { margin: 0 0 0.75rem; border-top: none; border-radius: 0 0 6px 6px; }
        .settings-popout-panel .panel-header { border-radius: 0; }
        .settings-bar { display: flex; flex-wrap: wrap; gap: 0.6rem 0.9rem; align-items: center; }
        .settings-group { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; }
        .settings-label { font-size: 0.65rem; color: var(--text-secondary); display: flex; align-items: center; gap: 3px; }
        .models-grid { display: grid; grid-template-columns: 64px repeat(3, minmax(110px, 1fr)) minmax(70px, 90px); gap: 4px 6px; align-items: center; width: 100%; }
        .models-grid-header { color: var(--text-muted); font-size: 0.6rem; text-transform: uppercase; letter-spacing: 0.6px; }
        .models-provider { color: var(--accent-gold); font-family: var(--font-mono); font-size: 0.65rem; }
        .settings-divider { width: 1px; align-self: stretch; background: var(--border-dark); }
        .footer { background: var(--bg-dark); border-top: 1px solid var(--border-dark); padding: 0.4rem; text-align: center; color: var(--text-muted); font-size: 0.65rem; }
    </style>
</head>
<body>
    <header class="header">
        <span class="header-logo">Fun-Doc Command Center</span>
        <div class="header-controls">
            <select class="header-select" id="folderSelect" onchange="switchFolder(this.value)" title="Project folder">
                {% for folder in stats.available_folders|default([]) %}
                <option value="{{ folder }}" {% if folder == stats.project_folder %}selected{% endif %}>{{ folder }}</option>
                {% endfor %}
            </select>
            <select class="header-select" id="binarySelect" onchange="switchBinary(this.value)">
                <option value="" disabled>Select Binary</option>
                {% for bin in stats.available_binaries|default([]) %}
                <option value="{{ bin }}" {% if bin == stats.active_binary %}selected{% endif %}>{{ bin }}</option>
                {% endfor %}
            </select>
            <button class="btn btn-primary" onclick="requestRescan(false)">Rescan</button>
            <button class="btn" onclick="requestRescan(true)">Full Rescan</button>
            <span class="header-sep"></span>
            <select class="header-select" id="workerProvider">
                <option value="minimax" selected>MiniMax</option>
                <option value="claude">Claude</option>
                <option value="codex">Codex</option>
                <option value="gemini">Gemini</option>
            </select>
            <input type="number" class="worker-input" id="workerCount" value="10" min="1" max="500" title="Functions to process">
            <label style="font-size: 0.65rem; color: var(--text-secondary); cursor: pointer; display: flex; align-items: center; gap: 3px;" title="Run until stopped or queue empty">
                <input type="checkbox" id="workerContinuous" style="margin: 0;"> <span style="line-height: 1;">Auto</span>
            </label>
            <button class="btn btn-primary" id="btnRunWorker" onclick="startWorker()">Run Worker</button>
            <button class="btn btn-primary" id="btnRunGlobalsWorker" onclick="startGlobalsWorker()" disabled title="Select a binary in the dropdown above to enable">Run Globals Worker</button>
            <button class="btn" id="btnToggleSettings" onclick="toggleSettingsPanel()" aria-expanded="false" aria-controls="settingsPopout">Settings</button>
        </div>
        <span class="header-info">
            <span class="live-dot" id="liveDot"></span>
            <span id="connStatus">Connecting...</span> | {{ stats.project_folder }}
        </span>
    </header>

    <div class="settings-popout" id="settingsPopout">
        <div class="settings-popout-inner">
            <div class="panel settings-popout-panel">
                <div class="panel-header"><h2>Settings</h2></div>
                <div class="panel-content">
                    <div class="settings-bar">
                        <div class="settings-group">
                            <label class="settings-label" title="Functions at or above this score are excluded from the queue (unless pinned)">
                                <span>Good@</span>
                                <input type="number" class="worker-input" id="goodEnoughInput" value="{{ stats.queue_config.good_enough_score|default(80) }}" min="0" max="100" onchange="saveQueueConfig()">
                            </label>
                            <label class="settings-label" title="Surface unscored functions to the worker so they get scored on first contact">
                                <input type="checkbox" id="requireScoredInput" {% if stats.queue_config.require_scored %}checked{% endif %} onchange="saveQueueConfig()" style="margin: 0;">
                                <span>Score Cold</span>
                            </label>
                            <label class="settings-label" title="Write detailed per-tool-call JSONL logs to logs/debug/{date}/. Use analyze_debug.py to inspect patterns.">
                                <input type="checkbox" id="debugModeInput" {% if stats.queue_config.debug_mode %}checked{% endif %} onchange="saveQueueConfig()" style="margin: 0;">
                                <span>Debug Logs</span>
                            </label>
                        </div>
                        <div class="settings-divider"></div>
                        <div class="settings-group">
                            <label class="settings-label" title="When the active provider's complexity gate fires, hand the function off to this provider instead of skipping. Set to Off to disable.">
                                <span>Handoff→</span>
                                <select class="header-select" id="handoffProviderInput" onchange="saveQueueConfig()">
                                    <option value="off" {% if not stats.queue_config.complexity_handoff_provider %}selected{% endif %}>Off</option>
                                    <option value="claude" {% if stats.queue_config.complexity_handoff_provider == 'claude' %}selected{% endif %}>Claude</option>
                                    <option value="codex" {% if stats.queue_config.complexity_handoff_provider == 'codex' %}selected{% endif %}>Codex</option>
                                    <option value="gemini" {% if stats.queue_config.complexity_handoff_provider == 'gemini' %}selected{% endif %}>Gemini</option>
                                </select>
                            </label>
                            <label class="settings-label" title="Maximum handoffs per worker session. 0 = unlimited.">
                                <span>Max</span>
                                <input type="number" class="worker-input" id="handoffMaxInput" value="{{ stats.queue_config.complexity_handoff_max|default(0) }}" min="0" max="999" onchange="saveQueueConfig()" style="width: 48px;">
                            </label>
                        </div>
                        <div class="settings-divider"></div>
                        <div class="settings-group">
                            <label class="settings-label" title="After the worker finishes, a second provider reviews and fixes gaps. Off = no audit pass.">
                                <span>Audit→</span>
                                <select class="header-select" id="auditProviderInput" onchange="saveQueueConfig()">
                                    <option value="off" {% if not stats.queue_config.audit_provider %}selected{% endif %}>Off</option>
                                    <option value="claude" {% if stats.queue_config.audit_provider == 'claude' %}selected{% endif %}>Claude</option>
                                    <option value="codex" {% if stats.queue_config.audit_provider == 'codex' %}selected{% endif %}>Codex</option>
                                    <option value="minimax" {% if stats.queue_config.audit_provider == 'minimax' %}selected{% endif %}>MiniMax</option>
                                    <option value="gemini" {% if stats.queue_config.audit_provider == 'gemini' %}selected{% endif %}>Gemini</option>
                                </select>
                            </label>
                            <label class="settings-label" title="Skip audit if worker's score gain >= this value. Lower = more audits.">
                                <span>MinΔ</span>
                                <input type="number" class="worker-input" id="auditMinDeltaInput" value="{{ stats.queue_config.audit_min_delta|default(5) }}" min="0" max="100" onchange="saveQueueConfig()" style="width: 48px;">
                            </label>
                        </div>
                    </div>
                    <div style="margin-top: 0.75rem;">
                        <div style="font-size: 0.65rem; color: var(--text-secondary); margin-bottom: 0.35rem;" title="Dashboard-owned model config. These values are the default source for provider/model selection. Max Turns sets the per-provider tool-call cap.">Provider Models</div>
                        <div class="models-grid">
                            <div></div>
                            <div class="models-grid-header">Full</div>
                            <div class="models-grid-header">Fix</div>
                            <div class="models-grid-header">Verify</div>
                            <div class="models-grid-header" title="Maximum tool-call turns per provider.">Max Turns</div>
                            {% for provider in ['minimax', 'gemini', 'claude', 'codex'] %}
                            <div class="models-provider">{{ provider }}</div>
                            <input type="text" class="worker-input" id="model-{{ provider }}-FULL" value="{{ stats.queue_config.provider_models.get(provider, {}).get('FULL', '') }}" placeholder="full" onchange="saveQueueConfig()" style="width: 100%; text-align: left;">
                            <input type="text" class="worker-input" id="model-{{ provider }}-FIX" value="{{ stats.queue_config.provider_models.get(provider, {}).get('FIX', '') }}" placeholder="fix" onchange="saveQueueConfig()" style="width: 100%; text-align: left;">
                            <input type="text" class="worker-input" id="model-{{ provider }}-VERIFY" value="{{ stats.queue_config.provider_models.get(provider, {}).get('VERIFY', '') }}" placeholder="verify" onchange="saveQueueConfig()" style="width: 100%; text-align: left;">
                            <input type="number" class="worker-input" id="turns-{{ provider }}" value="{{ stats.queue_config.provider_max_turns.get(provider, 25) }}" min="1" max="999" onchange="saveQueueConfig()" style="width: 100%; text-align: left;">
                            {% endfor %}
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <main class="main-container">
        <!-- Status banner -->
        <div class="status-banner" id="statusBanner">
            <div class="banner-row">
                <div class="spinner"></div>
                <span id="bannerText"></span>
            </div>
            <div class="scan-progress-bars" id="scanProgressBars">
                <div>
                    <div class="scan-bar-label">
                        <span id="scanOuterLabel">Programs</span>
                        <span id="scanOuterCount">0 / 0</span>
                    </div>
                    <div class="scan-bar-track">
                        <div class="scan-bar-fill" id="scanOuterFill" style="width: 0%"></div>
                    </div>
                </div>
                <div>
                    <div class="scan-bar-label">
                        <span id="scanInnerLabel">Functions</span>
                        <span id="scanInnerCount">0 / 0</span>
                    </div>
                    <div class="scan-bar-track">
                        <div class="scan-bar-fill inner" id="scanInnerFill" style="width: 0%"></div>
                    </div>
                </div>
            </div>
        </div>

        <!-- Focus banner — shows which worker/function you're tracking -->
        <div class="focus-banner" id="focusBanner">
            <span>Focused:</span>
            <span class="focus-func" id="focusWorkerName">—</span>
            <span>→</span>
            <span class="focus-func" id="focusFuncName">—</span>
            <span class="focus-addr" id="focusFuncAddr"></span>
            <button class="btn" onclick="navigateToFocusedFunction()" title="Navigate Ghidra to this function">Go to Ghidra</button>
            <label style="margin-left: auto; font-size: 0.6rem; color: var(--text-secondary); cursor: pointer;">
                <input type="checkbox" id="autoFollowCheckbox" onchange="onAutoFollowChanged()"> Auto-follow
            </label>
            <button class="btn" onclick="unfocusWorker()" title="Release focus">Unfocus</button>
        </div>

        <!-- Worker status cards (replaced by output panes, kept for status updates) -->
        <div id="workerPanel" style="display: none;"></div>

        <!-- Stats cards -->
        <div class="stats-grid">
            <div class="stat-card"><div class="stat-value" id="stat-total">{{ stats.total }}</div><div class="stat-label">Total</div></div>
            <div class="stat-card"><div class="stat-value text-set" id="stat-done">{{ stats.done }}</div><div class="stat-label">Done</div></div>
            <div class="stat-card"><div class="stat-value text-rare" id="stat-fixable">{{ stats.fixable }}</div><div class="stat-label">Fixable</div></div>
            <div class="stat-card"><div class="stat-value text-crafted" id="stat-needs-work">{{ stats.needs_work }}</div><div class="stat-label">Needs Work</div></div>
            <div class="stat-card"><div class="stat-value" id="stat-pct">{{ stats.pct }}%</div><div class="stat-label">Progress</div></div>
            <div class="stat-card"><div class="stat-value text-magic" id="stat-today">{{ stats.run_stats.today_runs }}</div><div class="stat-label">Runs Today</div></div>
            <div class="stat-card"><div class="stat-value" id="stat-delta">{{ stats.run_stats.avg_delta }}%</div><div class="stat-label">Avg Delta</div></div>
            <div class="stat-card"><div class="stat-value" id="stat-audited" style="color: var(--color-magic);">{{ stats.audited }}</div><div class="stat-label">Audited</div></div>
            <div class="stat-card"><div class="stat-value" id="stat-escalated" style="color: var(--color-rare);">{{ stats.escalated }}</div><div class="stat-label">Escalated</div></div>
        </div>

        <!-- Progress bar -->
        <div class="progress-container">
            <div class="progress-bar-outer">
                <div class="progress-bar-inner" id="progressInner" style="width: {{ stats.pct }}%"></div>
                <div class="progress-text" id="progressText">{{ stats.done }} / {{ stats.total }} ({{ stats.pct }}%)</div>
            </div>
        </div>

        <!-- Worker Output Panes -->
        <div style="display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.35rem;" id="workerGridHeader">
            <span style="font-size: 0.7rem; color: var(--text-muted); font-family: var(--font-mono);" id="workerGridCount"></span>
            <button class="btn btn-danger" onclick="stopAllWorkers()" id="btnStopAll" style="display: none; margin-left: auto;">Stop All Workers</button>
        </div>
        <div id="workerGrid" style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 0.35rem; margin-bottom: 0.5rem;">
            <!-- Worker panes are dynamically created by JavaScript -->
        </div>

        <!-- Row: Score Distribution + Deduction Breakdown + Run Stats -->
        <div class="three-col" id="analyticsRow">
            <div class="panel">
                <div class="panel-header"><h2>Score Distribution</h2></div>
                <div class="panel-content" id="scoreDistPanel">
                    {% set max_count = stats.buckets.values()|max if stats.buckets.values()|list else 1 %}
                    {% for bucket, css in [("100","s100"),("90-99","s90"),("80-89","s80"),("70-79","s70"),("60-69","s60"),("50-59","s50"),("40-49","s40"),("30-39","s30"),("20-29","s20"),("10-19","s10"),("0-9","s0")] %}
                    {% set count = stats.buckets.get(bucket, 0) %}
                    {% if count > 0 %}
                    <div class="score-bar-row">
                        <span class="score-bar-label">{{ bucket }}</span>
                        <div class="score-bar-track"><div class="score-bar-fill {{ css }}" style="width: {{ (count / max_count * 100)|round }}%"></div></div>
                        <span class="score-bar-count">{{ count }}</span>
                    </div>
                    {% endif %}
                    {% endfor %}
                </div>
            </div>
            <div class="panel">
                <div class="panel-header"><h2>Where Points Are Lost</h2></div>
                <div class="panel-content" id="deductionPanel">
                    {% if stats.deduction_breakdown %}
                    {% set max_pts = stats.deduction_breakdown[0].total_pts if stats.deduction_breakdown else 1 %}
                    {% for d in stats.deduction_breakdown[:10] %}
                    <div class="deduction-row">
                        <span class="deduction-cat">{{ d.category | replace("_", " ") }}</span>
                        <span class="deduction-stat">{{ d.functions }} fns</span>
                        <div class="deduction-bar"><div class="deduction-bar-fill" style="width: {{ (d.total_pts / max_pts * 100)|round }}%"></div></div>
                        <span class="deduction-stat">{{ d.total_pts|round(0)|int }} pts</span>
                    </div>
                    {% endfor %}
                    {% else %}
                    <div style="color: var(--text-muted);">No data. Run a scan first.</div>
                    {% endif %}
                </div>
            </div>
            <div>
                <div class="panel">
                    <div class="panel-header"><h2>Model Performance</h2></div>
                    <div class="panel-content" id="runStatsPanel">
                        <!-- Top-level summary row -->
                        <div style="display: flex; gap: 0.75rem; margin-bottom: 0.4rem; flex-wrap: wrap;">
                            <div><span class="mono" style="color: var(--accent-gold);">{{ stats.run_stats.total_runs }}</span> <span style="color: var(--text-muted); font-size: 0.65rem;">runs</span></div>
                            <div><span class="mono" style="color: var(--color-set);">{{ stats.run_stats.success_rate }}%</span> <span style="color: var(--text-muted); font-size: 0.65rem;">success</span></div>
                            <div><span class="mono" style="color: var(--accent-red-light);">{{ stats.run_stats.regressions }}</span> <span style="color: var(--text-muted); font-size: 0.65rem;">regressed</span></div>
                            <div><span class="mono" style="color: var(--text-secondary);">{{ stats.run_stats.zero_delta }}</span> <span style="color: var(--text-muted); font-size: 0.65rem;">zero-Δ</span></div>
                        </div>

                        <!-- Today vs All-time toggle -->
                        {% if stats.run_stats.today.runs > 0 %}
                        <div style="margin-bottom: 0.4rem; padding: 0.3rem 0.5rem; background: var(--bg-light); border-radius: 4px; font-size: 0.65rem;">
                            <span style="color: var(--accent-gold);">Today:</span>
                            <span class="mono" style="color: var(--text-primary);">{{ stats.run_stats.today.runs }}</span> runs
                            <span style="color: var(--text-muted);">·</span>
                            <span class="mono" style="color: var(--color-set);">{{ stats.run_stats.today.success_rate }}%</span> success
                            <span style="color: var(--text-muted);">·</span>
                            avg <span class="mono" style="color: {% if stats.run_stats.today.avg_delta > 0 %}var(--color-set){% elif stats.run_stats.today.avg_delta < 0 %}var(--accent-red-light){% else %}var(--text-muted){% endif %};">{{ stats.run_stats.today.avg_delta }}%</span> Δ
                        </div>
                        {% endif %}

                        <!-- Per-provider cards (enriched) -->
                        {% for provider, pstats in stats.run_stats.by_provider.items() %}
                        <div class="provider-card" style="flex-wrap: wrap;">
                            <span class="provider-name">{{ provider }}</span>
                            <span class="provider-stat">{{ pstats.runs }} runs</span>
                            <span class="provider-stat" style="color: {% if pstats.avg_delta > 0 %}var(--color-set){% elif pstats.avg_delta < 0 %}var(--accent-red-light){% else %}var(--text-muted){% endif %};">avg {{ pstats.avg_delta }}%</span>
                            <span class="provider-stat" style="color: {% if pstats.success_rate >= 80 %}var(--color-set){% elif pstats.success_rate >= 50 %}var(--accent-gold){% else %}var(--accent-red-light){% endif %};">{{ pstats.success_rate }}% ok</span>
                            {% if pstats.known_tool_runs > 0 %}
                            <span class="provider-stat" style="color: var(--text-muted);">{{ pstats.avg_tools }} tools</span>
                            <span class="provider-stat" style="color: var(--text-muted); font-size: 0.6rem;">{{ pstats.known_tool_runs }} known</span>
                            {% endif %}
                            {% if pstats.unknown_tool_runs > 0 %}
                            <span class="provider-stat" style="color: var(--accent-gold); font-size: 0.6rem;">{{ pstats.unknown_tool_runs }} unknown tc</span>
                            {% endif %}
                            {% if pstats.today_runs > 0 %}
                            <span class="provider-stat" style="color: var(--color-magic); font-size: 0.6rem;">today: {{ pstats.today_runs }} · {{ pstats.today_avg_delta }}%Δ</span>
                            {% endif %}
                        </div>
                        {% endfor %}

                        {% if stats.run_stats.handoffs.total > 0 %}
                        <div style="margin-top: 0.4rem; padding: 0.3rem 0.5rem; background: var(--bg-light); border-radius: 4px; font-size: 0.65rem;">
                            <strong style="color: var(--color-magic);">Handoffs:</strong>
                            <span class="mono" style="color: var(--text-primary);">{{ stats.run_stats.handoffs.total }}</span> total
                            {% for item in stats.run_stats.handoffs.top_pairs[:3] %}
                            <span style="color: var(--text-muted);">· {{ item.pair }} <span class="mono">{{ item.count }}</span></span>
                            {% endfor %}
                        </div>
                        {% endif %}

                        <!-- Failure modes breakdown -->
                        {% if stats.run_stats.failure_modes %}
                        <div style="margin-top: 0.4rem; font-size: 0.65rem;">
                            <strong style="color: var(--accent-red-light);">Failures:</strong>
                            {% for mode, count in stats.run_stats.failure_modes.items() %}
                            <span style="color: var(--text-muted); margin-left: 0.3rem;">{{ mode }} <span class="mono">{{ count }}</span></span>
                            {% endfor %}
                        </div>
                        {% endif %}

                        <!-- Audit effectiveness -->
                        {% if stats.run_stats.audit.ran > 0 or stats.run_stats.audit.skipped_good > 0 %}
                        <div style="margin-top: 0.4rem; padding: 0.3rem 0.5rem; background: var(--bg-light); border-radius: 4px; font-size: 0.65rem;">
                            <strong style="color: var(--color-magic);">Audit:</strong>
                            <span class="mono" style="color: var(--text-primary);">{{ stats.run_stats.audit.ran }}</span> ran
                            <span style="color: var(--text-muted);">·</span>
                            <span class="mono" style="color: var(--color-set);">{{ stats.run_stats.audit.improved }}</span> improved
                            <span style="color: var(--text-muted);">·</span>
                            <span class="mono" style="color: var(--accent-red-light);">{{ stats.run_stats.audit.regressed }}</span> regressed
                            <span style="color: var(--text-muted);">·</span>
                            <span class="mono" style="color: var(--text-muted);">{{ stats.run_stats.audit.no_change }}</span> no change
                            <span style="color: var(--text-muted);">·</span>
                            <span class="mono" style="color: var(--text-muted);">{{ stats.run_stats.audit.skipped_good + stats.run_stats.audit.skipped_delta }}</span> skipped
                        </div>
                        {% endif %}

                        <!-- Stuck functions -->
                        {% if stats.run_stats.stuck_functions %}
                        <div style="margin-top: 0.4rem; font-size: 0.65rem; color: var(--accent-red-light);">
                            <strong>Stuck:</strong>
                            {% for s in stats.run_stats.stuck_functions[:3] %}
                            <div style="color: var(--text-muted);">{{ s.name }} ({{ s.fails }}x)</div>
                            {% endfor %}
                        </div>
                        {% endif %}
                    </div>
                </div>
                <div class="panel">
                    <div class="panel-header" style="display: flex; align-items: center; gap: 0.5rem;">
                        <h2 style="margin: 0;">Function Inventory</h2>
                        <span id="inventoryHeaderState" style="font-size: 0.6rem; color: var(--text-muted); font-family: var(--font-mono);" title="Background scorer state — enabled, paused, or disabled">disabled</span>
                        <button class="btn icon-btn" id="btnInventoryForceOn" onclick="toggleInventoryForceOn()" style="margin-left: auto;" title="Force on — override pause conditions and keep the scorer running (also bypasses doc-worker pause)">⚡</button>
                        <button class="btn icon-btn" id="btnInventoryToggle" onclick="toggleInventoryScorer()" title="Enable scorer">▶</button>
                    </div>
                    <div class="panel-content" id="inventoryPanel" style="max-height: 300px; overflow-y: auto;">
                        <div id="inventoryTotalsLine" style="padding: 0.25rem 0.5rem; font-size: 0.65rem; color: var(--text-muted); font-family: var(--font-mono); border-bottom: 1px solid var(--border-dark);">Loading…</div>
                        <table class="data-table">
                            <thead>
                                <tr>
                                    <th>Binary</th>
                                    <th>Coverage</th>
                                    <th>Scored</th>
                                    <th>Total</th>
                                    <th>Missing</th>
                                    <th>%</th>
                                    <th>Status</th>
                                    <th>Last Scan</th>
                                    <th></th>
                                </tr>
                            </thead>
                            <tbody id="inventoryTableBody">
                                <tr><td colspan="9" style="color: var(--text-muted); text-align: center; padding: 0.6rem;">No inventory data yet — enable the scorer.</td></tr>
                            </tbody>
                        </table>
                    </div>
                </div>
                <div class="panel">
                    <div class="panel-header" style="display: flex; align-items: center; gap: 0.5rem;">
                        <h2 style="margin: 0;">Global  Variables Inventory</h2>
                        <span id="globalInventoryHeaderState" style="font-size: 0.6rem; color: var(--text-muted); font-family: var(--font-mono);" title="Background global-variable auditor — enabled, paused, or disabled">disabled</span>
                        <button class="btn icon-btn" id="btnGlobalInventoryForceOn" onclick="toggleGlobalInventoryForceOn()" style="margin-left: auto;" title="Force on — override pause conditions and keep the scorer running (also bypasses doc-worker pause)">⚡</button>
                        <button class="btn icon-btn" id="btnGlobalInventoryToggle" onclick="toggleGlobalScorer()" title="Enable scorer">▶</button>
                    </div>
                    <div class="panel-content" id="globalInventoryPanel" style="max-height: 300px; overflow-y: auto;">
                        <div id="globalInventoryTotalsLine" style="padding: 0.25rem 0.5rem; font-size: 0.65rem; color: var(--text-muted); font-family: var(--font-mono); border-bottom: 1px solid var(--border-dark);">Loading…</div>
                        <table class="data-table">
                            <thead>
                                <tr>
                                    <th>Binary</th>
                                    <th>Coverage</th>
                                    <th>Documented</th>
                                    <th>Total</th>
                                    <th>Issues</th>
                                    <th>%</th>
                                    <th>Status</th>
                                    <th>Last Scan</th>
                                    <th></th>
                                </tr>
                            </thead>
                            <tbody id="globalInventoryTableBody">
                                <tr><td colspan="9" style="color: var(--text-muted); text-align: center; padding: 0.6rem;">No global-inventory data yet — enable the scorer.</td></tr>
                            </tbody>
                        </table>
                    </div>
                </div>
                <div class="panel">
                    <div class="panel-header"><h2>Cross-Binary Progress</h2></div>
                    <div class="panel-content" id="perBinaryPanel" style="max-height: 300px; overflow-y: auto;">
                        <table class="data-table"><thead><tr><th>Binary</th><th>Progress</th><th>Done</th><th>Total</th><th>Avg</th><th>Fixable</th><th></th></tr></thead><tbody>
                        {% for prog, info in stats.by_program.items()|sort %}
                        {% set pct = ((info.done / info.total * 100) if info.total > 0 else 0)|round(1) %}
                        <tr>
                            <td style="cursor: pointer; color: var(--accent-gold);" onclick="switchBinaryDirect('{{ prog }}')">{{ prog }}</td>
                            <td style="width: 100px;">
                                <div style="width: 100%; height: 10px; background: var(--bg-darkest); border-radius: 2px; overflow: hidden;">
                                    <div style="height: 100%; width: {{ pct }}%; background: {% if pct >= 90 %}var(--color-set){% elif pct >= 50 %}var(--accent-gold){% else %}var(--color-crafted){% endif %}; border-radius: 2px;"></div>
                                </div>
                            </td>
                            <td class="mono">{{ info.done }}</td>
                            <td class="mono">{{ info.total }}</td>
                            <td class="mono" style="color: {% if pct >= 90 %}var(--color-set){% elif pct >= 50 %}var(--accent-gold){% else %}var(--text-muted){% endif %};">{{ pct }}%</td>
                            <td class="mono">{{ info.remaining }}</td>
                            <td><button class="btn" onclick="requestRescanBinary('{{ prog }}')">Rescan</button></td>
                        </tr>
                        {% endfor %}
                        </tbody></table>
                    </div>
                </div>
            </div>
        </div>

        {% set queued = stats.roi_queue|selectattr('pinned')|list %}
        {% set autopicks = stats.roi_queue|rejectattr('pinned')|list %}

        <!-- Refresh status banner -->
        <div class="panel" id="refreshStatusPanel" style="margin-bottom: 0.5rem;">
            <div class="panel-content" style="display: flex; align-items: center; gap: 0.75rem; font-size: 0.7rem; padding: 0.4rem 0.6rem;">
                <span style="color: var(--text-muted);">Score Refresh:</span>
                <span id="refreshLastAt" style="font-family: var(--font-mono); color: var(--text-secondary);">never</span>
                <span id="refreshStaleSkips" style="font-family: var(--font-mono); color: var(--text-muted);"></span>
                <span style="color: var(--border-light);">|</span>
                <span style="color: var(--text-muted);">Escalations:</span>
                <span id="handoffCount" style="font-family: var(--font-mono); color: var(--text-secondary);">0</span>
                <span style="color: var(--border-light);">|</span>
                <span style="color: var(--text-muted);">Audits:</span>
                <span id="auditCount" style="font-family: var(--font-mono); color: var(--text-secondary);" title="ran / skipped (good enough + low delta)">—</span>
                <span style="color: var(--border-light);">|</span>
                <span style="color: var(--text-muted);">Inventory:</span>
                <span id="inventoryWidget" style="font-family: var(--font-mono); color: var(--text-secondary);" title="Background scorer fills missing completeness scores. Toggle in settings.">disabled</span>
                <button class="btn" onclick="drainDone()" id="btnDrainDone" title="Batch-score every pinned function and auto-dequeue any that are already above good_enough" style="margin-left: auto;">Drain Done</button>
                <button class="btn" onclick="manualRefresh()" id="btnRefresh" title="Batch-rescore the top 50 ROI candidates for the active binary">Refresh Top 50</button>
            </div>
        </div>

        <!-- Work Queue (explicitly queued only) -->
        <div class="panel">
            <div class="panel-header">
                <h2>Work Queue <span id="queueCount" style="color: var(--text-muted); font-weight: normal;">({{ queued|length }})</span></h2>
                <span style="color: var(--text-muted); font-size: 0.65rem;">
                    What you've explicitly queued. Workers pull from here in queue order, then auto-dequeue once a function crosses good@.
                </span>
            </div>
            <div class="panel-content">
                <div class="func-list-scroll" style="max-height: 240px;" id="queuePanel">
                    <table class="data-table">
                        <thead><tr><th>Actions</th><th>Function</th><th>Addr</th><th>Score</th><th>Deps</th><th>Fixable</th><th>ROI</th><th>Type</th><th>Last</th></tr></thead>
                        <tbody id="queueBody">
                        {% if queued %}
                        {% for t in queued %}
                        <tr>
                            <td><button class="btn btn-primary" onclick="dequeueFunc('{{ t.key }}')">Dequeue</button></td>
                            <td><span class="badge badge-pinned">QUEUED</span> {{ t.name }}</td>
                            <td class="mono">0x{{ t.address }}</td>
                            <td class="score-cell {% if t.score >= 90 %}score-90{% elif t.score >= 70 %}score-70{% elif t.score >= 50 %}score-50{% else %}score-low{% endif %}">{{ t.score }}%</td>
                            <td class="mono">{% if t.deps_remaining is defined and t.deps_remaining == 0 and not t.is_leaf %}<span style="color: var(--color-set);">Ready</span>{% elif t.is_leaf %}<span class="badge badge-leaf">Leaf</span>{% elif t.deps_remaining is defined and t.deps_remaining > 0 %}<span style="color: var(--color-rare);">{{ t.deps_remaining }} dep{{ 's' if t.deps_remaining > 1 }}</span>{% else %}—{% endif %}</td>
                            <td class="mono">{{ t.fixable }}</td>
                            <td class="mono" style="color: var(--accent-gold);">{{ t.roi }}</td>
                            <td>{% if t.is_leaf %}<span class="badge badge-leaf">Leaf</span>{% elif t.call_graph_layer is not none %}<span style="color: var(--text-secondary); font-family: var(--font-mono); font-size: 0.65rem;">L{{ t.call_graph_layer }}</span>{% elif t.call_graph_layer is none and not t.is_leaf %}<span style="color: var(--text-muted); font-family: var(--font-mono); font-size: 0.65rem;">Cyc</span>{% endif %}</td>
                            <td style="color: var(--text-muted); font-size: 0.6rem;">{{ t.last_result or '' }}</td>
                        </tr>
                        {% endfor %}
                        {% else %}
                        <tr><td colspan="9" style="text-align: center; color: var(--text-muted); padding: 1rem;">Nothing queued. Workers will fall through to Auto-Pick Preview below when started.</td></tr>
                        {% endif %}
                        </tbody>
                    </table>
                </div>
            </div>
        </div>

        <!-- Auto-Pick Preview (what the worker would grab if queue is empty) -->
        <div class="panel">
            <div class="panel-header">
                <h2>Auto-Pick Preview <span style="color: var(--text-muted); font-weight: normal;">(top {{ [autopicks|length, 30]|min }})</span></h2>
                <span style="color: var(--text-muted); font-size: 0.65rem;">
                    Bottom-up order: leaves first, then callers whose callees are documented. Ready% = fraction of callees at &ge;80%.
                </span>
            </div>
            <div class="panel-content">
                <div class="func-list-scroll" style="max-height: 300px;" id="autoPickPanel">
                    <table class="data-table">
                        <thead><tr><th>Actions</th><th>Function</th><th>Addr</th><th>Score</th><th>Deps</th><th>Fixable</th><th>ROI</th><th>Type</th><th>Last</th></tr></thead>
                        <tbody id="autoPickBody">
                        {% for t in autopicks[:30] %}
                        <tr>
                            <td><button class="btn" onclick="queueFunc('{{ t.key }}')">Queue</button></td>
                            <td>{% if t.needs_scoring %}<span class="badge badge-new">COLD</span> {% endif %}{{ t.name }}</td>
                            <td class="mono">0x{{ t.address }}</td>
                            <td class="score-cell {% if t.score >= 90 %}score-90{% elif t.score >= 70 %}score-70{% elif t.score >= 50 %}score-50{% else %}score-low{% endif %}">{{ t.score }}%</td>
                            <td class="mono">{% if t.deps_remaining is defined and t.deps_remaining == 0 and not t.is_leaf %}<span style="color: var(--color-set);">Ready</span>{% elif t.is_leaf %}<span class="badge badge-leaf">Leaf</span>{% elif t.deps_remaining is defined and t.deps_remaining > 0 %}<span style="color: var(--color-rare);">{{ t.deps_remaining }} dep{{ 's' if t.deps_remaining > 1 }}</span>{% else %}—{% endif %}</td>
                            <td class="mono">{{ t.fixable }}</td>
                            <td class="mono" style="color: var(--accent-gold);">{{ t.roi }}</td>
                            <td>{% if t.is_leaf %}<span class="badge badge-leaf">Leaf</span>{% elif t.call_graph_layer is not none %}<span style="color: var(--text-secondary); font-family: var(--font-mono); font-size: 0.65rem;">L{{ t.call_graph_layer }}</span>{% elif t.call_graph_layer is none and not t.is_leaf %}<span style="color: var(--text-muted); font-family: var(--font-mono); font-size: 0.65rem;">Cyc</span>{% endif %}</td>
                            <td style="color: var(--text-muted); font-size: 0.6rem;">{{ t.last_result or '' }}</td>
                        </tr>
                        {% endfor %}
                        </tbody>
                    </table>
                </div>
            </div>
        </div>

        <!-- View tabs: List View / Call Graph Layers -->
        <div style="display: flex; gap: 0.5rem; margin-bottom: 0.5rem;">
            <button class="btn active" id="tab-list" onclick="switchView('list')">List View</button>
            <button class="btn" id="tab-graph" onclick="switchView('graph')">Call Graph Layers</button>
        </div>

        <!-- Call Graph Layers panel (hidden by default, shown when tab-graph is active) -->
        <div class="panel" id="callGraphPanel" style="display: none;">
            <div class="panel-header">
                <h2>Call Graph Layers</h2>
                <span style="color: var(--text-muted); font-size: 0.65rem;">
                    Bottom-up: Layer 0 = leaves (no callees). Higher layers = callers of previous layers. Green = complete, gold = ready to process.
                </span>
            </div>
            <div class="panel-content" id="callGraphContent">
                <div style="text-align: center; color: var(--text-muted); padding: 1rem;">Loading call graph layers...</div>
            </div>
        </div>

        <!-- All Functions -->
        <div class="panel" id="allFuncsPanel">
            <div class="panel-header">
                <h2>All Functions</h2>
                <span style="color: var(--text-muted); font-size: 0.65rem;" id="allFuncsCount">
                    {{ stats.all_functions|length }} functions
                </span>
            </div>
            <div class="panel-content">
                <div class="filter-bar">
                    <button class="btn active" id="filter-all" onclick="filterFuncs('all', this)">All</button>
                    <button class="btn" id="filter-needs_work" onclick="filterFuncs('needs_work', this)">Needs Work</button>
                    <button class="btn" id="filter-fixable" onclick="filterFuncs('fixable', this)">Fixable</button>
                    <button class="btn" id="filter-done" onclick="filterFuncs('done', this)">Done</button>
                    <select class="header-select" id="layerFilterSelect" onchange="onLayerDropdownChanged()" title="Filter by call-graph layer">
                        <option value="">All Layers</option>
                    </select>
                    <input class="filter-input" type="text" id="searchInput" placeholder="Search name or address..." oninput="onSearchInput()">
                </div>
                <div class="func-list-scroll">
                    <table class="data-table"><thead><tr>
                        <th>Actions</th>
                        <th class="sortable-header" onclick="toggleSort('name')">Function <span id="sort-ind-name"></span></th>
                        <th class="sortable-header" onclick="toggleSort('address')">Addr <span id="sort-ind-address"></span></th>
                        <th class="sortable-header" onclick="toggleSort('score')">Score <span id="sort-ind-score"></span></th>
                        <th class="sortable-header" onclick="toggleSort('deps')">Deps <span id="sort-ind-deps"></span></th>
                        <th class="sortable-header" onclick="toggleSort('fixable')">Fixable <span id="sort-ind-fixable"></span></th>
                        <th class="sortable-header" onclick="toggleSort('tools')" title="Tool calls used in the most recent primary run for this function. Audit pass not included.">Tools <span id="sort-ind-tools"></span></th>
                        <th class="sortable-header" onclick="toggleSort('layer')">Layer <span id="sort-ind-layer"></span></th>
                        <th title="Name provenance: scan (default Ghidra label or first scan), manual (worker renamed), propagation (cross-version hash propagation -- skipped from selector unless pinned or high-confidence), pdb (symbol server import), archive (re-kb gate). See #204.">Src</th>
                        <th class="sortable-header" onclick="toggleSort('status')">Status <span id="sort-ind-status"></span></th>
                    </tr></thead>
                    <tbody id="allFuncsBody">
                    {% for f in stats.all_functions %}
                    <tr class="func-row" data-score="{{ f.score }}" data-name="{{ f.name|lower }}" data-key="{{ f.key }}">
                        <td>
                            {% if f.pinned %}<button class="btn btn-primary" onclick="dequeueFunc('{{ f.key }}')">Dequeue</button>
                            {% else %}<button class="btn" onclick="queueFunc('{{ f.key }}')">Queue</button>{% endif %}
                        </td>
                        <td>{% if f.pinned %}<span class="badge badge-pinned">QUEUED</span> {% endif %}{{ f.name }}</td>
                        <td class="mono">0x{{ f.address }}</td>
                        {% if f.unscored %}
                        <td class="score-cell" style="color: var(--text-muted);">&mdash;</td>
                        {% else %}
                        <td class="score-cell {% if f.score >= 100 %}score-100{% elif f.score >= 90 %}score-90{% elif f.score >= 70 %}score-70{% elif f.score >= 50 %}score-50{% else %}score-low{% endif %}">{{ f.score }}%</td>
                        {% endif %}
                        <td class="mono">{% if f.is_leaf %}<span class="badge badge-leaf">Leaf</span>{% elif f.callers == 0 %}<span style="color: var(--color-set);">Ready</span>{% else %}<span style="color: var(--color-rare);">{{ f.callers }} dep{{ 's' if f.callers > 1 }}</span>{% endif %}</td>
                        <td class="mono">{{ f.fixable }}</td>
                        <td class="mono" style="color: var(--text-secondary);">{% if f.tool_calls is none or f.tool_calls < 0 %}<span style="color: var(--text-muted);" title="Provider didn't expose a tool-call count">&mdash;</span>{% else %}{{ f.tool_calls }}{% endif %}</td>
                        <td class="mono" style="font-size: 0.6rem; color: var(--text-secondary);">{% if f.is_leaf %}Leaf{% else %}—{% endif %}</td>
                        <td class="mono" style="font-size: 0.6rem;">
                            {% set ns = f.name_source or 'scan' %}
                            {% if f.skip_reason == 'propagation' %}<span style="color: var(--color-rare);" title="Skipped by selector: name came from cross-version hash propagation, no archive backing. Source binary: {{ f.name_source_binary or 'unknown' }}. Pin to override.">propagation</span>
                            {% elif ns == 'propagation' %}<span style="color: var(--color-set);" title="Name from propagation; high-confidence ({{ f.name_confidence }}), admitted by selector. Source: {{ f.name_source_binary or 'unknown' }}">propagation</span>
                            {% elif ns == 'manual' %}<span style="color: var(--color-set);" title="Renamed by worker">manual</span>
                            {% elif ns == 'pdb' %}<span style="color: var(--color-set);" title="From PDB import">pdb</span>
                            {% elif ns == 'archive' %}<span style="color: var(--color-set);" title="From re-kb archive">archive</span>
                            {% else %}<span style="color: var(--text-muted);" title="Default Ghidra label or set during scan">scan</span>{% endif %}
                        </td>
                        <td>
                        {% if f.unscored %}<span class="badge badge-new" title="Never scored — click Queue to score on demand">UNSCORED</span>
                        {% elif f.score >= (stats.queue_config.good_enough_score|default(80)) %}<span class="badge badge-done">DONE</span>
                        {% elif f.score >= 70 %}<span class="badge badge-fix">FIX</span>
                        {% else %}<span class="badge badge-new">NEW</span>{% endif %}
                        </td>
                    </tr>
                    {% endfor %}
                    </tbody></table>
                </div>
            </div>
        </div>
    </main>

    <footer class="footer">Fun-Doc Command Center | Last scan: {{ stats.last_scan or 'never' }}</footer>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.7.5/socket.io.min.js"></script>
    <script>
        const socket = io();
        let refreshTimer = null;

        // --- Connection status ---
        socket.on("connect", () => {
            document.getElementById("liveDot").className = "live-dot connected";
            document.getElementById("connStatus").textContent = "Live";
            socket.emit('request_worker_status');
        });
        socket.on("disconnect", () => {
            document.getElementById("liveDot").className = "live-dot disconnected";
            document.getElementById("connStatus").textContent = "Disconnected";
        });

        // --- State changed: debounced refresh of stats ---
        socket.on("state_changed", () => {
            if (refreshTimer) clearTimeout(refreshTimer);
            refreshTimer = setTimeout(refreshStats, 500);
        });
        socket.on("queue_changed", () => {
            if (refreshTimer) clearTimeout(refreshTimer);
            refreshTimer = setTimeout(refreshStats, 300);
        });

        // ---- Inventory scorer (background completeness scoring) ----
        let _inventoryRefreshTimer = null;
        let _inventoryEnabled = false;

        function refreshInventory() {
            fetch('/api/inventory/status').then(r => r.json()).then(data => {
                if (!data || data.error) return;
                _renderInventoryWidget(data.scorer || {}, data.totals || {});
                _renderInventoryHeader(data.scorer || {});
                _renderInventoryTable(data.binaries || [], (data.scorer || {}).blacklisted || []);
            }).catch(() => {});
        }

        function _renderInventoryWidget(scorer, totals) {
            const el = document.getElementById('inventoryWidget');
            if (!el) return;
            _inventoryEnabled = !!scorer.enabled;
            if (!scorer.enabled) {
                el.textContent = 'disabled';
                el.style.color = 'var(--text-muted)';
                return;
            }
            const scored = totals.scored || 0;
            const total = totals.total_documentable || 0;
            const done = totals.binaries_complete || 0;
            const all = totals.binaries_total || 0;
            const reason = scorer.paused_reason;
            const target = scorer.current_target ? scorer.current_target.split('/').pop() : null;
            let line;
            if (reason) {
                line = `${scored.toLocaleString()}/${total.toLocaleString()} • ${done}/${all} done • paused (${escapeHtml(reason)})`;
            } else if (target) {
                line = `${scored.toLocaleString()}/${total.toLocaleString()} • ${done}/${all} done • scanning ${escapeHtml(target)}`;
            } else {
                line = `${scored.toLocaleString()}/${total.toLocaleString()} • ${done}/${all} done`;
            }
            el.innerHTML = line;
            el.style.color = reason ? 'var(--text-muted)' : 'var(--accent-gold)';
        }

        function _renderInventoryHeader(scorer) {
            const el = document.getElementById('inventoryHeaderState');
            const btn = document.getElementById('btnInventoryToggle');
            const force = document.getElementById('btnInventoryForceOn');
            if (!el || !btn) return;
            if (!scorer.enabled) {
                el.textContent = 'disabled';
                el.style.color = 'var(--text-muted)';
                btn.textContent = '▶'; // ▶
                btn.title = 'Enable scorer';
            } else if (scorer.paused_reason) {
                el.textContent = `paused (${scorer.paused_reason})`;
                el.style.color = 'var(--text-muted)';
                btn.textContent = '⏸'; // ⏸
                btn.title = 'Disable scorer';
            } else if (scorer.current_target) {
                el.textContent = 'running';
                el.style.color = 'var(--accent-gold)';
                btn.textContent = '⏸';
                btn.title = 'Disable scorer';
            } else {
                el.textContent = 'enabled';
                el.style.color = 'var(--text-secondary)';
                btn.textContent = '⏸';
                btn.title = 'Disable scorer';
            }
            if (force) {
                force.classList.toggle('is-active', !!scorer.force_on);
                force.title = scorer.force_on
                    ? 'Force-on engaged — overriding pauses. Click to disengage.'
                    : 'Force on — override pause conditions and keep the scorer running (also bypasses doc-worker pause)';
            }
        }

        function _renderInventoryTable(binaries, blacklist) {
            const body = document.getElementById('inventoryTableBody');
            const totalsLine = document.getElementById('inventoryTotalsLine');
            if (!body) return;
            if (!binaries.length) {
                body.innerHTML = '<tr><td colspan="9" style="color: var(--text-muted); text-align: center; padding: 0.6rem;">No inventory data yet — enable the scorer.</td></tr>';
                if (totalsLine) totalsLine.textContent = '';
                return;
            }
            const blackset = new Set(blacklist || []);
            let totalScored = 0;
            let totalDoc = 0;
            let totalMiss = 0;
            let totalDone = 0;
            const rows = binaries.map(r => {
                totalScored += r.scored || 0;
                totalDoc += r.total_documentable || 0;
                totalMiss += r.missing || 0;
                if (r.status === 'complete') totalDone++;
                const pct = r.percent || 0;
                const barColor = pct >= 99 ? 'var(--color-set)' : pct >= 50 ? 'var(--accent-gold)' : 'var(--color-crafted)';
                const statusColor = r.status === 'complete' ? 'var(--color-set)'
                    : r.status === 'blacklisted' ? 'var(--accent-red-light)'
                    : r.status === 'in_progress' ? 'var(--accent-gold)'
                    : 'var(--text-muted)';
                const last = r.last_scan ? r.last_scan.replace('T', ' ').slice(0, 19) : '—';
                const isBlack = blackset.has(r.path);
                // Per-binary Reset surfaces only when this row's total looks
                // wedged (< 100 documentable functions) — that's the heuristic
                // for "scorer probably mis-stamped this binary." Larger counts
                // are assumed legitimate; user can use Retry if blacklisted.
                // Naming note: outer scope already has `let totalDoc` as the
                // table's running counter, so we use `rowTotal` here to avoid
                // a temporal-dead-zone collision that silently broke render.
                const rowTotal = r.total_documentable || 0;
                const showReset = rowTotal < 100;
                const actions = [];
                if (isBlack) actions.push(`<button class="btn" onclick="clearInventoryBlacklist('${escapeHtml(r.path)}')" title="Clear session blacklist for this binary">Retry</button>`);
                if (showReset) actions.push(`<button class="btn icon-btn" onclick="resetInventoryBinary('${escapeHtml(r.path)}', '${escapeHtml(r.name)}')" title="Reset this binary's inventory record (only ${rowTotal} functions counted — looks wedged)">↻</button>`);
                const actionBtn = actions.join(' ');
                return `<tr>
                    <td style="cursor: pointer; color: var(--accent-gold);" onclick="switchBinaryDirect('${escapeHtml(r.name)}')" title="${escapeHtml(r.path)}">${escapeHtml(r.name)}</td>
                    <td style="width: 100px;"><div style="width: 100%; height: 8px; background: var(--bg-darkest); border-radius: 2px; overflow: hidden;"><div style="height: 100%; width: ${pct}%; background: ${barColor}; border-radius: 2px;"></div></div></td>
                    <td class="mono">${(r.scored || 0).toLocaleString()}</td>
                    <td class="mono">${(r.total_documentable || 0).toLocaleString()}</td>
                    <td class="mono" style="color: ${(r.missing || 0) > 0 ? 'var(--accent-red-light)' : 'var(--text-muted)'};">${(r.missing || 0).toLocaleString()}</td>
                    <td class="mono">${pct}%</td>
                    <td class="mono" style="color: ${statusColor};">${escapeHtml(r.status || '')}</td>
                    <td class="mono" style="color: var(--text-muted); font-size: 0.6rem;">${escapeHtml(last)}</td>
                    <td>${actionBtn}</td>
                </tr>`;
            }).join('');
            body.innerHTML = rows;
            if (totalsLine) {
                const overall = totalDoc > 0 ? Math.round(1000 * totalScored / totalDoc) / 10 : 0;
                totalsLine.textContent = `${totalScored.toLocaleString()} / ${totalDoc.toLocaleString()} scored (${overall}%) • ${totalMiss.toLocaleString()} missing • ${totalDone} / ${binaries.length} binaries complete`;
            }
        }

        function toggleInventoryScorer() {
            const next = !_inventoryEnabled;
            fetch('/api/inventory/toggle', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({enabled: next}),
            }).then(r => r.json()).then(() => refreshInventory());
        }

        function clearInventoryBlacklist(path) {
            fetch('/api/inventory/clear_blacklist', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({path: path}),
            }).then(r => r.json()).then(() => refreshInventory());
        }

        function toggleInventoryForceOn() {
            const btn = document.getElementById('btnInventoryForceOn');
            const next = !(btn && btn.classList.contains('is-active'));
            fetch('/api/inventory/force_on', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({force_on: next}),
            }).then(r => r.json()).then(() => refreshInventory());
        }

        function resetInventoryBinary(path, name) {
            if (!confirm(`Reset inventory record for ${name}? The scorer will re-walk it on the next loop.`)) return;
            fetch('/api/inventory/reset', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({path: path}),
            }).then(r => r.json()).then(() => refreshInventory());
        }

        socket.on('inventory_status', () => {
            if (_inventoryRefreshTimer) clearTimeout(_inventoryRefreshTimer);
            _inventoryRefreshTimer = setTimeout(refreshInventory, 300);
        });
        socket.on('inventory_reset', () => refreshInventory());

        // ---- Global Inventory (v5.7.0) ----
        let _globalInventoryRefreshTimer = null;
        let _globalInventoryEnabled = false;

        function refreshGlobalInventory() {
            fetch('/api/global_inventory/status').then(r => r.json()).then(data => {
                if (!data || data.error) return;
                _renderGlobalInventoryHeader(data.scorer || {});
                _renderGlobalInventoryTable(data.binaries || [], data.totals || {}, (data.scorer || {}).blacklisted || []);
            }).catch(() => {});
        }

        function _renderGlobalInventoryHeader(scorer) {
            const el = document.getElementById('globalInventoryHeaderState');
            const btn = document.getElementById('btnGlobalInventoryToggle');
            const force = document.getElementById('btnGlobalInventoryForceOn');
            if (!el || !btn) return;
            _globalInventoryEnabled = !!scorer.enabled;
            if (!scorer.enabled) {
                el.textContent = 'disabled';
                el.style.color = 'var(--text-muted)';
                btn.textContent = '▶';
                btn.title = 'Enable scorer';
            } else if (scorer.paused_reason) {
                el.textContent = `paused (${scorer.paused_reason})`;
                el.style.color = 'var(--text-muted)';
                btn.textContent = '⏸';
                btn.title = 'Disable scorer';
            } else if (scorer.current_target) {
                el.textContent = 'running';
                el.style.color = 'var(--accent-gold)';
                btn.textContent = '⏸';
                btn.title = 'Disable scorer';
            } else {
                el.textContent = 'enabled';
                el.style.color = 'var(--text-secondary)';
                btn.textContent = '⏸';
                btn.title = 'Disable scorer';
            }
            if (force) {
                force.classList.toggle('is-active', !!scorer.force_on);
                force.title = scorer.force_on
                    ? 'Force-on engaged — overriding pauses. Click to disengage.'
                    : 'Force on — override pause conditions and keep the scorer running (also bypasses doc-worker pause)';
            }
        }

        function _renderGlobalInventoryTable(binaries, totals, blacklist) {
            const body = document.getElementById('globalInventoryTableBody');
            const totalsLine = document.getElementById('globalInventoryTotalsLine');
            if (!body) return;
            if (!binaries.length) {
                body.innerHTML = '<tr><td colspan="9" style="color: var(--text-muted); text-align: center; padding: 0.6rem;">No global-inventory data yet — enable the scorer.</td></tr>';
                if (totalsLine) totalsLine.textContent = '';
                return;
            }
            const blackset = new Set(blacklist || []);
            const rows = binaries.map(r => {
                const pct = r.percent || 0;
                const barColor = pct >= 99 ? 'var(--color-set)' : pct >= 50 ? 'var(--accent-gold)' : 'var(--color-crafted)';
                const statusColor = r.status === 'complete' ? 'var(--color-set)'
                    : r.status === 'blacklisted' ? 'var(--accent-red-light)'
                    : r.status === 'in_progress' ? 'var(--accent-gold)'
                    : 'var(--text-muted)';
                const last = r.last_scan ? r.last_scan.replace('T', ' ').slice(0, 19) : '—';
                const isBlack = blackset.has(r.path);
                // Per-binary Reset is always available on the global
                // inventory — refreshing a binary's globals on demand is
                // a routine operation (not just a wedge-recovery
                // escape hatch), since the discovery surface (undefined
                // xref'd data) shifts as the user fixes things in
                // Ghidra. The old `< 100` threshold was a leftover
                // anti-footgun heuristic from the function-inventory
                // copy; for globals the user wants explicit per-binary
                // refresh control regardless of count.
                const rowTotal = r.total_documentable || 0;
                const actions = [];
                if (isBlack) actions.push(`<button class="btn" onclick="clearGlobalInventoryBlacklist('${escapeHtml(r.path)}')" title="Clear session blacklist for this binary">Retry</button>`);
                actions.push(`<button class="btn icon-btn" onclick="resetGlobalInventoryBinary('${escapeHtml(r.path)}', '${escapeHtml(r.name)}')" title="Reset this binary's global-inventory record so the scorer re-walks it on the next loop (${rowTotal} globals currently counted)">↻</button>`);
                const actionBtn = actions.join(' ');
                return `<tr>
                    <td style="cursor: pointer; color: var(--accent-gold);" title="${escapeHtml(r.path)}">${escapeHtml(r.name)}</td>
                    <td style="width: 100px;"><div style="width: 100%; height: 8px; background: var(--bg-darkest); border-radius: 2px; overflow: hidden;"><div style="height: 100%; width: ${pct}%; background: ${barColor}; border-radius: 2px;"></div></div></td>
                    <td class="mono">${(r.fully_documented || 0).toLocaleString()}</td>
                    <td class="mono">${(r.total_documentable || 0).toLocaleString()}</td>
                    <td class="mono" style="color: ${(r.with_issues || 0) > 0 ? 'var(--accent-red-light)' : 'var(--text-muted)'};">${(r.with_issues || 0).toLocaleString()}</td>
                    <td class="mono">${pct}%</td>
                    <td class="mono" style="color: ${statusColor};">${escapeHtml(r.status || '')}</td>
                    <td class="mono" style="color: var(--text-muted); font-size: 0.6rem;">${escapeHtml(last)}</td>
                    <td>${actionBtn}</td>
                </tr>`;
            }).join('');
            body.innerHTML = rows;
            if (totalsLine) {
                const overall = totals.total_documentable > 0 ? Math.round(1000 * totals.fully_documented / totals.total_documentable) / 10 : 0;
                totalsLine.textContent = `${(totals.fully_documented || 0).toLocaleString()} / ${(totals.total_documentable || 0).toLocaleString()} fully documented (${overall}%) • ${(totals.with_issues || 0).toLocaleString()} globals with issues • ${totals.binaries_complete || 0} / ${binaries.length} binaries complete`;
            }
        }

        function toggleGlobalScorer() {
            const next = !_globalInventoryEnabled;
            fetch('/api/global_inventory/toggle', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({enabled: next}),
            }).then(r => r.json()).then(() => refreshGlobalInventory());
        }

        function clearGlobalInventoryBlacklist(path) {
            fetch('/api/global_inventory/clear_blacklist', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({path: path}),
            }).then(r => r.json()).then(() => refreshGlobalInventory());
        }

        function toggleGlobalInventoryForceOn() {
            const btn = document.getElementById('btnGlobalInventoryForceOn');
            const next = !(btn && btn.classList.contains('is-active'));
            fetch('/api/global_inventory/force_on', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({force_on: next}),
            }).then(r => r.json()).then(() => refreshGlobalInventory());
        }

        function resetGlobalInventoryBinary(path, name) {
            if (!confirm(`Reset global-inventory record for ${name}? The scorer will re-walk it on the next loop.`)) return;
            fetch('/api/global_inventory/reset', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({path: path}),
            }).then(r => r.json()).then(() => refreshGlobalInventory());
        }

        socket.on('global_inventory_status', () => {
            if (_globalInventoryRefreshTimer) clearTimeout(_globalInventoryRefreshTimer);
            _globalInventoryRefreshTimer = setTimeout(refreshGlobalInventory, 300);
        });
        socket.on('global_inventory_reset', () => refreshGlobalInventory());

        // ---- Quota-pause wake countdown ----
        function _formatQuotaCountdown(isoStr) {
            try {
                const until = new Date(isoStr);
                const now = new Date();
                let secs = Math.max(0, Math.floor((until - now) / 1000));
                if (secs <= 0) return 'resuming…';
                const h = Math.floor(secs / 3600); secs -= h * 3600;
                const m = Math.floor(secs / 60); secs -= m * 60;
                if (h > 0) return `${h}h ${m}m`;
                if (m > 0) return `${m}m ${secs}s`;
                return `${secs}s`;
            } catch { return ''; }
        }

        // Repaint paused workers every 30s so the countdown visibly ticks
        // down without waiting for a full worker_status push. We rely on
        // the cached __lastWorkerStatus the worker handler stashes.
        setInterval(() => {
            const workers = window.__lastWorkerStatus || [];
            workers.forEach(w => {
                if (w.status !== 'quota_paused' || !w.paused_until) return;
                const status = document.getElementById(`wp-status-${w.id}`);
                if (!status) return;
                const remaining = _formatQuotaCountdown(w.paused_until);
                const reason = w.paused_reason ? ` — ${escapeHtml(w.paused_reason).slice(0,60)}` : '';
                status.innerHTML = `<span class="wp-current">paused</span> until ${escapeHtml(w.paused_until.replace('T',' ').slice(0,19))} (${remaining})${reason}`;
            });
        }, 30000);

        // Backend may push provider_pauses on install/clear; we don't render
        // a separate panel (Q7 = no banner) but the worker_status path picks
        // up the new paused_until field naturally on the next status emit.
        socket.on('provider_pauses', () => {});

        function scoreCellClass(s) {
            if (s >= 100) return 'score-100';
            if (s >= 90) return 'score-90';
            if (s >= 70) return 'score-70';
            if (s >= 50) return 'score-50';
            return 'score-low';
        }
        function escapeHtml(s) {
            return String(s == null ? '' : s)
                .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
                .replace(/"/g, '&quot;').replace(/'/g, '&#39;');
        }

        function _row(t, queued) {
            const actionBtn = queued
                ? `<button class="btn btn-primary" onclick="dequeueFunc('${escapeHtml(t.key)}')">Dequeue</button>`
                : `<button class="btn" onclick="queueFunc('${escapeHtml(t.key)}')">Queue</button>`;
            const queuedBadge = queued ? '<span class="badge badge-pinned">QUEUED</span> ' : '';
            const coldBadge = t.needs_scoring ? '<span class="badge badge-new">COLD</span> ' : '';
            let layerLabel = '';
            if (t.is_leaf) {
                layerLabel = '<span class="badge badge-leaf">Leaf</span>';
            } else if (t.call_graph_layer != null) {
                layerLabel = `<span style="color: var(--text-secondary); font-family: var(--font-mono); font-size: 0.65rem;">L${t.call_graph_layer}</span>`;
            } else if (!t.is_leaf) {
                layerLabel = '<span style="color: var(--text-muted); font-family: var(--font-mono); font-size: 0.65rem;">Cyc</span>';
            }
            let depsCell;
            if (t.is_leaf) {
                depsCell = '<span class="badge badge-leaf">Leaf</span>';
            } else if (t.deps_remaining === 0) {
                depsCell = '<span style="color: var(--color-set);">Ready</span>';
            } else if (t.deps_remaining != null && t.deps_remaining > 0) {
                depsCell = `<span style="color: var(--color-rare);">${t.deps_remaining} dep${t.deps_remaining > 1 ? 's' : ''}</span>`;
            } else {
                depsCell = '—';
            }
            return `<tr>
                <td>${actionBtn}</td>
                <td>${queuedBadge}${coldBadge}${escapeHtml(t.name)}</td>
                <td class="mono">0x${escapeHtml(t.address)}</td>
                <td class="score-cell ${scoreCellClass(t.score)}">${t.score}%</td>
                <td class="mono">${depsCell}</td>
                <td class="mono">${t.fixable}</td>
                <td class="mono" style="color: var(--accent-gold);">${t.roi}</td>
                <td>${layerLabel}</td>
                <td style="color: var(--text-muted); font-size: 0.6rem;">${escapeHtml(t.last_result || '')}</td>
            </tr>`;
        }

        function repaintRefreshStatus(meta, runStats) {
            if (!meta) return;
            const lastEl = document.getElementById('refreshLastAt');
            const staleEl = document.getElementById('refreshStaleSkips');
            if (lastEl) {
                if (meta.last_refresh_at) {
                    const ts = new Date(meta.last_refresh_at);
                    const ageSec = Math.max(0, (Date.now() - ts.getTime()) / 1000);
                    let ageStr;
                    if (ageSec < 60) ageStr = `${Math.round(ageSec)}s ago`;
                    else if (ageSec < 3600) ageStr = `${Math.round(ageSec / 60)}m ago`;
                    else ageStr = `${Math.round(ageSec / 3600)}h ago`;
                    lastEl.textContent = `${ageStr} — ${meta.last_refresh_count || 0} scored, ${meta.last_refresh_stale || 0} drifted`;
                } else {
                    lastEl.textContent = 'never';
                }
            }
            if (staleEl) {
                const skips = meta.stale_skips_since_refresh || 0;
                if (skips > 0) {
                    staleEl.textContent = `${skips} stale skip${skips === 1 ? '' : 's'} since refresh`;
                    staleEl.style.color = skips >= 3 ? 'var(--accent-red-light)' : 'var(--color-rare)';
                } else {
                    staleEl.textContent = '';
                }
            }
            const handoffEl = document.getElementById('handoffCount');
            if (handoffEl) {
                const n = meta.handoffs_this_session || 0;
                handoffEl.textContent = String(n);
                handoffEl.style.color = n > 0 ? 'var(--color-rare)' : 'var(--text-secondary)';
            }
            const auditEl = document.getElementById('auditCount');
            if (auditEl && runStats && runStats.audit) {
                const a = runStats.audit;
                const ran = a.today_ran || 0;
                const skipped = (a.today_skipped_good || 0) + (a.today_skipped_delta || 0);
                if (ran > 0 || skipped > 0) {
                    const improved = a.today_improved || 0;
                    auditEl.textContent = `${ran} ran${improved ? ` (${improved} ↑)` : ''} / ${skipped} skipped`;
                    auditEl.style.color = ran > 0 ? 'var(--color-rare)' : 'var(--text-secondary)';
                } else {
                    auditEl.textContent = '—';
                    auditEl.style.color = 'var(--text-secondary)';
                }
            }
        }

        async function drainDone() {
            const btn = document.getElementById('btnDrainDone');
            btn.disabled = true;
            btn.textContent = 'Draining...';
            try {
                const r = await fetch('/api/queue/drain_done', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: '{}',
                });
                const data = await r.json();
                if (data.error) {
                    showBanner(`Drain error: ${data.error}`);
                } else {
                    showBanner(
                        `Drained: checked ${data.checked}, dequeued ${data.dequeued}, ` +
                        `${data.still_queued} still queued${data.errors ? ', ' + data.errors + ' errors' : ''}`
                    );
                }
                setTimeout(hideBanner, 5000);
            } catch (e) {
                showBanner(`Drain failed: ${e}`);
                setTimeout(hideBanner, 5000);
            } finally {
                btn.disabled = false;
                btn.textContent = 'Drain Done';
            }
        }

        async function manualRefresh() {
            const btn = document.getElementById('btnRefresh');
            const binary = document.getElementById('binarySelect').value || null;
            btn.disabled = true;
            btn.textContent = 'Refreshing...';
            try {
                await fetch('/api/queue/refresh', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({binary, count: 50}),
                });
            } finally {
                setTimeout(() => {
                    btn.disabled = false;
                    btn.textContent = 'Refresh Top 50';
                }, 3000);
            }
        }

        function repaintQueue(roi_queue) {
            if (!roi_queue) return;
            const queued = roi_queue.filter(t => t.pinned);
            const autopicks = roi_queue.filter(t => !t.pinned).slice(0, 30);

            const queueBody = document.getElementById('queueBody');
            if (queueBody) {
                if (queued.length === 0) {
                    queueBody.innerHTML = '<tr><td colspan="9" style="text-align: center; color: var(--text-muted); padding: 1rem;">Nothing queued. Workers will fall through to Auto-Pick Preview below when started.</td></tr>';
                } else {
                    queueBody.innerHTML = queued.map(t => _row(t, true)).join('');
                }
            }
            const queueCount = document.getElementById('queueCount');
            if (queueCount) queueCount.textContent = `(${queued.length})`;

            const autoBody = document.getElementById('autoPickBody');
            if (autoBody) {
                autoBody.innerHTML = autopicks.map(t => _row(t, false)).join('');
            }
        }

        function refreshStats() {
            fetch("/api/stats").then(r => r.json()).then(stats => {
                // Stat cards
                document.getElementById("stat-total").textContent = stats.total;
                document.getElementById("stat-done").textContent = stats.done;
                document.getElementById("stat-fixable").textContent = stats.fixable;
                document.getElementById("stat-needs-work").textContent = stats.needs_work;
                document.getElementById("stat-pct").textContent = stats.pct + "%";
                document.getElementById("stat-today").textContent = stats.run_stats.today_runs;
                document.getElementById("stat-delta").textContent = stats.run_stats.avg_delta + "%";
                document.getElementById("stat-audited").textContent = stats.audited;
                document.getElementById("stat-escalated").textContent = stats.escalated;
                document.getElementById("progressInner").style.width = stats.pct + "%";
                document.getElementById("progressText").textContent = `${stats.done} / ${stats.total} (${stats.pct}%)`;

                if (stats.queue_config) {
                    const ge = document.getElementById('goodEnoughInput');
                    const rs = document.getElementById('requireScoredInput');
                    const ho = document.getElementById('handoffProviderInput');
                    const hm = document.getElementById('handoffMaxInput');
                    const dm = document.getElementById('debugModeInput');
                    const providerMaxTurns = stats.queue_config.provider_max_turns || {};
                    const providerModels = stats.queue_config.provider_models || {};
                    if (ge && document.activeElement !== ge) ge.value = stats.queue_config.good_enough_score;
                    if (rs) rs.checked = !!stats.queue_config.require_scored;
                    if (ho && document.activeElement !== ho) {
                        ho.value = stats.queue_config.complexity_handoff_provider || 'off';
                    }
                    if (hm && document.activeElement !== hm) {
                        hm.value = stats.queue_config.complexity_handoff_max || 0;
                    }
                    if (dm) dm.checked = !!stats.queue_config.debug_mode;
                    ['minimax', 'gemini', 'claude', 'codex'].forEach(provider => {
                        const input = document.getElementById(`turns-${provider}`);
                        if (!input || document.activeElement === input) return;
                        input.value = providerMaxTurns[provider] || 25;
                    });
                    ['minimax', 'gemini', 'claude', 'codex'].forEach(provider => {
                        ['FULL', 'FIX', 'VERIFY'].forEach(mode => {
                            const input = document.getElementById(`model-${provider}-${mode}`);
                            if (!input || document.activeElement === input) return;
                            input.value = ((providerModels[provider] || {})[mode]) || '';
                        });
                    });
                }
                if (stats.queue_meta) repaintRefreshStatus(stats.queue_meta, stats.run_stats);
                if (stats.roi_queue) repaintQueue(stats.roi_queue);

                // Score distribution
                const distPanel = document.getElementById("scoreDistPanel");
                if (distPanel && stats.buckets) {
                    const bucketOrder = [["100","s100"],["90-99","s90"],["80-89","s80"],["70-79","s70"],["60-69","s60"],["50-59","s50"],["40-49","s40"],["30-39","s30"],["20-29","s20"],["10-19","s10"],["0-9","s0"]];
                    const maxCount = Math.max(1, ...Object.values(stats.buckets));
                    distPanel.innerHTML = bucketOrder.map(([bucket, css]) => {
                        const count = stats.buckets[bucket] || 0;
                        if (count === 0) return '';
                        const pct = Math.round(count / maxCount * 100);
                        return `<div class="score-bar-row"><span class="score-bar-label">${bucket}</span><div class="score-bar-track"><div class="score-bar-fill ${css}" style="width:${pct}%"></div></div><span class="score-bar-count">${count}</span></div>`;
                    }).join('');
                }

                // Deduction breakdown
                const dedPanel = document.getElementById("deductionPanel");
                if (dedPanel && stats.deduction_breakdown && stats.deduction_breakdown.length > 0) {
                    const maxPts = stats.deduction_breakdown[0].total_pts || 1;
                    dedPanel.innerHTML = stats.deduction_breakdown.slice(0, 10).map(d => {
                        const pct = Math.round(d.total_pts / maxPts * 100);
                        return `<div class="deduction-row"><span class="deduction-cat">${d.category.replace(/_/g, ' ')}</span><span class="deduction-stat">${d.functions} fns</span><div class="deduction-bar"><div class="deduction-bar-fill" style="width:${pct}%"></div></div><span class="deduction-stat">${Math.round(d.total_pts)} pts</span></div>`;
                    }).join('');
                }
                // Refresh layer dropdown counts
                populateLayerDropdown();
            }).catch(() => {});
        }

        // --- Worker output panes ---
        const workerPanes = {};  // worker_id -> {element, output}

        function getOrCreatePane(workerId, provider, binary) {
            if (workerPanes[workerId]) return workerPanes[workerId];
            const grid = document.getElementById('workerGrid');
            const pane = document.createElement('div');
            pane.className = 'worker-pane running';
            pane.id = `wp-${workerId}`;
            pane.innerHTML = `
                <div class="worker-pane-header">
                    <span class="wp-title">${provider || '?'} #${workerId}</span>
                    <span class="wp-status" id="wp-status-${workerId}">${binary || ''}</span>
                    <span class="wp-alert" id="wp-alert-${workerId}"></span>
                    <div class="wp-actions">
                        <button class="btn icon-btn" onclick="focusWorker('${workerId}')" id="wp-focus-${workerId}" title="Focus worker" aria-label="Focus worker" aria-pressed="false"><span class="icon-focus" aria-hidden="true"></span></button>
                        <button class="btn btn-danger icon-btn" onclick="stopWorker('${workerId}')" id="wp-stop-${workerId}" title="Stop worker" aria-label="Stop worker"><span class="icon-stop" aria-hidden="true"></span></button>
                        <button class="btn" onclick="dismissPane('${workerId}')" style="display:none" id="wp-dismiss-${workerId}">X</button>
                    </div>
                </div>
                <div class="wp-config" id="wp-config-${workerId}" style="display:none; padding:0.15rem 0.5rem; font-family: var(--font-mono); font-size: 0.58rem; color: var(--text-secondary); border-bottom: 1px solid var(--border-dark); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"></div>
                <div class="worker-pane-output" id="wp-out-${workerId}"></div>
            `;
            grid.appendChild(pane);
            workerPanes[workerId] = {element: pane, output: pane.querySelector('.worker-pane-output')};
            return workerPanes[workerId];
        }

        // Render the per-worker frozen-config sub-line. Called from the
        // worker_status handler whenever a worker's config_snapshot field
        // updates. Sub-line is hidden when the worker has no snapshot
        // (legacy/CLI workers — pre-snapshot behavior).
        function renderWorkerConfig(workerId, snap) {
            const el = document.getElementById(`wp-config-${workerId}`);
            if (!el) return;
            if (!snap || typeof snap !== 'object') {
                el.style.display = 'none';
                el.textContent = '';
                return;
            }
            const provs = snap.providers || {};
            const primary = snap.primary_provider;
            const primaryEntry = provs[primary] || {};
            const tokens = [];
            // Model from FULL mode for the primary provider — what the worker
            // actually uses unless it switches to FIX/VERIFY mid-run.
            const fullModel = (primaryEntry.models || {}).FULL;
            if (fullModel) tokens.push(fullModel);
            if (primaryEntry.max_turns !== undefined) tokens.push(`t:${primaryEntry.max_turns}`);
            // Audit policy. "off" if null/undefined, otherwise show provider + min-delta.
            if (snap.audit_provider) {
                tokens.push(`audit:${snap.audit_provider} Δ${snap.audit_min_delta}`);
            } else {
                tokens.push('audit:off');
            }
            // Escalation target.
            if (snap.complexity_handoff_provider) {
                tokens.push(`esc:${snap.complexity_handoff_provider}`);
            } else {
                tokens.push('esc:off');
            }
            // good_enough is a numeric threshold; show as good@N.
            if (snap.good_enough_score !== undefined) tokens.push(`good@${snap.good_enough_score}`);
            el.textContent = tokens.join(' · ');
            el.style.display = '';
        }

        // Cap the number of body lines per function block so a verbose run
        // (e.g., 200+ tool calls) can't accumulate unbounded DOM nodes inside
        // one .fn-block.body. Older entries get trimmed once the cap is hit.
        // Copilot review feedback on PR #168 flagged the prior pruning logic
        // as only counting top-level blocks — one block could blow up alone.
        const FN_BLOCK_BODY_MAX_LINES = 300;

        function appendToPane(workerId, text, type) {
            const pane = workerPanes[workerId];
            if (!pane) return;
            // Q4: drop blank/whitespace-only lines so within-block output is
            // continuous with no gaps. Between-block spacing comes from the
            // .fn-block margin-bottom.
            if (!text || !String(text).trim()) return;
            const el = document.createElement('div');
            el.className = 'activity-entry activity-' + type;
            el.textContent = text;
            // Route into the currently-open function block's body if one
            // exists; otherwise drop into the bare pane (legacy lines that
            // arrive outside a function — e.g. worker startup lines).
            const target = (pane.currentFnBlock && pane.currentFnBlock.body) || pane.output;
            target.appendChild(el);
            // Per-block trim (within the active fn-block.body).
            if (pane.currentFnBlock && pane.currentFnBlock.body
                    && pane.currentFnBlock.body.children.length > FN_BLOCK_BODY_MAX_LINES) {
                pane.currentFnBlock.body.removeChild(pane.currentFnBlock.body.firstChild);
            }
            // Top-level trim (bare pane lines + total fn-blocks).
            if (pane.output.children.length > 200) pane.output.removeChild(pane.output.firstChild);
            pane.output.scrollTop = pane.output.scrollHeight;
        }

        // ---- Function-block lifecycle helpers (Q1-Q5 design) ----

        function _openFnBlock(pane, name, address) {
            // If a previous block is still open (no Complete: was ever
            // emitted — e.g. worker died mid-function, then was restored
            // and started a new one), close it as interrupted so we never
            // have two adjacent open blocks.
            if (pane.currentFnBlock) {
                _closeFnBlock(pane, /* name */ null, /* addr */ null, true /* interrupted */);
            }
            const block = document.createElement('div');
            block.className = 'fn-block';
            const header = document.createElement('div');
            header.className = 'fn-name fn-header';
            header.textContent = `${name} @ 0x${address}`;
            block.appendChild(header);
            const body = document.createElement('div');
            body.className = 'fn-body';
            block.appendChild(body);
            pane.output.appendChild(block);
            pane.currentFnBlock = { block, body };
            pane.output.scrollTop = pane.output.scrollHeight;
        }

        function _closeFnBlock(pane, name, address, interrupted) {
            if (!pane || !pane.currentFnBlock) return;
            const footer = document.createElement('div');
            footer.className = 'fn-name fn-footer';
            const nm = name || pane.currentFuncName || '?';
            const ad = address || pane.currentFuncAddr || '?';
            footer.textContent = interrupted
                ? `${nm} @ 0x${ad} (interrupted)`
                : `${nm} @ 0x${ad}`;
            pane.currentFnBlock.block.appendChild(footer);
            pane.currentFnBlock = null;
        }

        function dismissPane(workerId) {
            const pane = workerPanes[workerId];
            if (pane) { pane.element.remove(); delete workerPanes[workerId]; }
        }

        function setPaneAlert(workerId, alert) {
            const pane = workerPanes[workerId];
            const badge = document.getElementById(`wp-alert-${workerId}`);
            if (!pane || !badge) return;
            if (alert && alert.type === 'provider_timeout') {
                badge.textContent = `TIMEOUT${alert.timeout_secs ? ' ' + alert.timeout_secs + 's' : ''}`;
                badge.title = alert.message || 'Provider timeout';
                badge.classList.add('active');
                pane.element.classList.add('timeout');
            } else {
                badge.textContent = '';
                badge.title = '';
                badge.classList.remove('active');
                pane.element.classList.remove('timeout');
            }
        }

        // Human-readable label for a structured exit_reason. Returns null
        // when the worker exited normally with work done; the caller falls
        // back to the raw status string in that case.
        function exitReasonLabel(exitReason) {
            if (!exitReason) return null;
            if (exitReason === 'no_eligible_candidates') return 'no eligible candidates';
            // Unknown reason: render as-is so we don't silently swallow a
            // new exit code the backend may add later.
            return exitReason;
        }

        function markPaneFinished(workerId, reason, exitReason) {
            const pane = workerPanes[workerId];
            if (!pane) return;
            pane.element.className = 'worker-pane finished';
            const status = document.getElementById(`wp-status-${workerId}`);
            const label = exitReasonLabel(exitReason);
            if (status) {
                status.textContent = label
                    ? `${reason || 'finished'} — ${label}`
                    : (reason || 'finished');
            }
            const stopBtn = document.getElementById(`wp-stop-${workerId}`);
            const dismissBtn = document.getElementById(`wp-dismiss-${workerId}`);
            if (stopBtn) stopBtn.style.display = 'none';
            if (dismissBtn) dismissBtn.style.display = '';
        }

        // Scan events (go to banner, not panes)
        socket.on("scan_started", d => {
            showBanner(`Scan started (${d.mode || 'incremental'}${d.folder ? ': ' + d.folder : ''})`);
            showScanBars();
            setScanBars({outerLabel: 'Programs', outerCur: 0, outerTot: 0, innerLabel: 'Functions', innerCur: 0, innerTot: 0});
        });
        socket.on("scan_progress", d => {
            const idx = (d.index || 0);
            const total = d.total || 0;
            const scored = d.scored || 0;
            const progTotal = d.program_total || 0;
            const phase = d.phase || 'scoring';
            const failed = d.failed_batches || 0;
            const pct = progTotal > 0 ? Math.round(scored / progTotal * 100) : 0;
            const failSuffix = failed > 0 ? ` (${failed} failed batches)` : '';
            updateBanner(`Scanning [${idx + 1}/${total}] ${d.program}${phase === 'scoring' && progTotal > 0 ? ` — ${pct}%` : ` (${phase})`}${failSuffix}`);
            setScanBars({
                outerLabel: `Program ${idx + 1} of ${total}`,
                outerCur: idx,  // before the current program completes
                outerTot: total,
                innerLabel: `${d.program} — ${phase}${failSuffix}`,
                innerCur: scored,
                innerTot: progTotal,
            });
        });
        socket.on("scan_complete", d => {
            updateBanner(`Scan complete: ${d.total || 0} functions, ${d.done || 0} done`);
            hideScanBars();
            setTimeout(hideBanner, 3000);
            refreshStats();
        });
        socket.on("scan_error", d => {
            showBanner(`Scan error: ${d.error}`);
            hideScanBars();
            setTimeout(hideBanner, 5000);
        });
        socket.on("scan_acknowledged", d => {
            // Immediate confirmation that the server received the rescan request
            showBanner(`Scan request accepted${d.program ? ' for ' + d.program : ''}${d.refresh ? ' (full refresh)' : ''}...`);
        });

        // Worker lifecycle events
        socket.on("worker_started", d => {
            getOrCreatePane(d.worker_id, d.provider, d.binary || '');
            appendToPane(
                d.worker_id,
                d.restored
                    ? `Worker restored after restart: ${d.provider} ${d.continuous ? 'Auto' : 'x' + d.count}`
                    : `Worker started: ${d.provider} ${d.continuous ? 'Auto' : 'x' + d.count}`,
                'scan'
            );
            // Show Stop All button when any worker is running
            const stopAllBtn = document.getElementById('btnStopAll');
            if (stopAllBtn) stopAllBtn.style.display = '';
        });
        socket.on("worker_stopped", d => {
            const pane = workerPanes[d.worker_id];
            // Close any block left open by mid-function abandonment (Q2 C).
            if (pane) _closeFnBlock(pane, null, null, true /* interrupted */);
            const exitLabel = exitReasonLabel(d.exit_reason);
            const completed = d.progress?.completed || 0;
            const summary = exitLabel
                ? `Worker ${d.reason}: ${completed} completed — ${exitLabel}`
                : `Worker ${d.reason}: ${completed} completed`;
            appendToPane(d.worker_id, summary, d.reason === 'finished' ? 'success' : 'neutral');
            markPaneFinished(d.worker_id, d.reason, d.exit_reason);
            if (_focusedWorkerId === d.worker_id) unfocusWorker();
            refreshStats();
        });

        // Per-function events — route to worker pane by worker_id
        socket.on("function_started", d => {
            if (d.worker_id) getOrCreatePane(d.worker_id, null, null);
            const pane = workerPanes[d.worker_id];
            if (pane) {
                _openFnBlock(pane, d.name, d.address);
                pane.currentFuncName = d.name;
                pane.currentFuncAddr = d.address;
            }
            // Update focus tracking if this worker is focused
            updateFocusedFunction(d.worker_id, d.name, d.address);
        });
        socket.on("function_mode", d => appendToPane(d.worker_id, `  ${d.mode} | ${d.model} | score: ${d.score}%`, 'neutral'));
        socket.on("tool_result", d => {
            // Single-line render matching the console: [mcp] tool → status (Nb, Nms)
            const sizeStr = (d.bytes != null) ? `${d.bytes}b` : '';
            const durStr = (d.duration_ms != null) ? `${d.duration_ms}ms` : '';
            const meta = [sizeStr, durStr].filter(Boolean).join(', ');
            const errPart = d.error ? ` - ${d.error}` : '';
            const metaPart = meta ? ` (${meta})` : '';
            const klass = (d.status === 'failed' || d.status === 'error') ? 'error' : 'tool';
            appendToPane(d.worker_id, `  [mcp] ${d.tool} → ${d.status}${errPart}${metaPart}`, klass);
        });
        socket.on("provider_timeout", d => {
            if (d.worker_id) {
                getOrCreatePane(d.worker_id, d.provider, null);
                appendToPane(d.worker_id, `  [watchdog] ${d.message || 'provider timeout'} — killed stalled session`, 'error');
                setPaneAlert(d.worker_id, d);
            }
            showBanner(`Provider timeout: ${d.message || 'stalled provider session killed'}`);
            setTimeout(hideBanner, 5000);
        });
        socket.on("model_text", d => { if (d.text) appendToPane(d.worker_id, d.text.substring(0, 300), 'text'); });
        socket.on("score_update", d => {
            const delta = (d.score_after||0) - (d.score_before||0);
            const sign = delta >= 0 ? '+' : '';
            appendToPane(d.worker_id, `  Score: ${d.score_before}% -> ${d.score_after}% (${sign}${delta}%) | ${d.result}`, delta > 0 ? 'success' : 'neutral');
        });
        socket.on("function_complete", d => {
            const pane = workerPanes[d.worker_id];
            let line;
            if (d.result === 'skipped') {
                line = `SKIP${d.skip_type ? ' (' + d.skip_type + ')' : ''}: ${d.reason || 'no reason given'}`;
            } else {
                line = `Complete: ${d.result}`;
            }
            const cls = d.result === 'completed' ? 'success' : (d.result === 'skipped' ? 'neutral' : 'error');
            appendToPane(d.worker_id, line, cls);
            // Close the block with the post-rescore name (Q3) — d.name is set
            // by the server-side enrichment; falls back to the start-name
            // tracked on the pane if the event omitted it (skip paths).
            if (pane) {
                _closeFnBlock(pane, d.name, d.address, false);
                pane.currentFuncName = null;
                pane.currentFuncAddr = null;
            }
            refreshStats();
        });

        // --- Globals worker per-item lifecycle (Q1=A reuses fn-block;
        //     Q2=B uses dedicated event names so events.jsonl stays
        //     truthful; Q3=B reuses updateFocusedFunction for the title;
        //     Q4=B fires only when real provider work happened).
        socket.on("global_started", d => {
            if (d.worker_id) getOrCreatePane(d.worker_id, null, null);
            const pane = workerPanes[d.worker_id];
            if (pane) {
                // Prefix the per-item header with the *full* program path
                // so the user can disambiguate between multiple versions
                // of the same DLL (e.g., /Vanilla/1.13d/D2Game.dll vs
                // /Mods/PD2-S12/D2Game.dll — addresses can look "wrong"
                // to the user when the version they're viewing in Ghidra
                // doesn't match the version the worker is processing).
                // Falls back to filename, then to no prefix.
                const pathPart = d.program_path || d.program || '';
                const labelName = pathPart ? `[${pathPart}] ${d.name}` : d.name;
                _openFnBlock(pane, labelName, d.address);
                pane.currentFuncName = d.name;
                pane.currentFuncAddr = d.address;
            }
            updateFocusedFunction(d.worker_id, d.name, d.address);
        });
        socket.on("global_complete", d => {
            const pane = workerPanes[d.worker_id];
            const fixedPart = (typeof d.fixed_count === 'number' && d.fixed_count > 0)
                ? ` (fixed ${d.fixed_count})` : '';
            const line = `Complete: ${d.result}${fixedPart}`;
            const cls = (d.result === 'completed' || d.result === 'improved') ? 'success'
                      : (d.result === 'no_change' ? 'neutral' : 'error');
            appendToPane(d.worker_id, line, cls);
            if (pane) {
                // Mirror the start-block format so the footer reads the
                // same way (full program path when available).
                const pathPart = d.program_path || d.program || '';
                const footerName = pathPart ? `[${pathPart}] ${d.name}` : d.name;
                _closeFnBlock(pane, footerName, d.address, false);
                pane.currentFuncName = null;
                pane.currentFuncAddr = null;
            }
        });
        // Q5=C: bold section heading between binaries in continuous mode.
        socket.on("globals_binary_advanced", d => {
            const pane = workerPanes[d.worker_id];
            if (!pane || !pane.output) return;
            // Close any open per-item block before the heading so the
            // heading doesn't get nested inside a previous binary's block.
            if (pane.currentFnBlock) {
                _closeFnBlock(pane, null, null, false);
            }
            const heading = document.createElement('div');
            heading.className = 'binary-section-heading';
            const ts = (d.timestamp || '').slice(11, 19) || '';
            heading.textContent = ts
                ? `══ ${d.binary_name || d.binary || '(unknown)'} ══  ${ts}`
                : `══ ${d.binary_name || d.binary || '(unknown)'} ══`;
            pane.output.appendChild(heading);
            pane.output.scrollTop = pane.output.scrollHeight;
        });

        // --- Banner ---
        function showBanner(text) { const b = document.getElementById("statusBanner"); b.classList.add("active"); document.getElementById("bannerText").textContent = text; }
        function updateBanner(text) { document.getElementById("bannerText").textContent = text; }
        function hideBanner() {
            document.getElementById("statusBanner").classList.remove("active");
            hideScanBars();
        }
        function showScanBars() {
            const el = document.getElementById('scanProgressBars');
            if (el) el.classList.add('active');
        }
        function hideScanBars() {
            const el = document.getElementById('scanProgressBars');
            if (el) el.classList.remove('active');
        }
        function setScanBars({outerLabel, outerCur, outerTot, innerLabel, innerCur, innerTot}) {
            const setText = (id, t) => { const e = document.getElementById(id); if (e) e.textContent = t; };
            const setWidth = (id, pct) => { const e = document.getElementById(id); if (e) e.style.width = pct + '%'; };
            if (outerLabel !== undefined) setText('scanOuterLabel', outerLabel);
            if (outerCur !== undefined && outerTot !== undefined) {
                setText('scanOuterCount', `${outerCur} / ${outerTot}`);
                setWidth('scanOuterFill', outerTot > 0 ? Math.min(100, (outerCur / outerTot) * 100) : 0);
            }
            if (innerLabel !== undefined) setText('scanInnerLabel', innerLabel);
            if (innerCur !== undefined && innerTot !== undefined) {
                setText('scanInnerCount', `${innerCur} / ${innerTot}`);
                setWidth('scanInnerFill', innerTot > 0 ? Math.min(100, (innerCur / innerTot) * 100) : 0);
            }
        }

        // --- Scan controls ---
        function requestRescan(refresh) {
            // If a binary is selected in the dropdown, scope the rescan to that binary
            const activeBinary = document.getElementById("binarySelect").value || null;
            socket.emit("request_rescan", {refresh: refresh, program: activeBinary});
            const scope = activeBinary || "all binaries";
            showBanner(refresh ? `Full rescan (${scope})...` : `Rescan (${scope})...`);
        }
        function requestRescanBinary(prog) { socket.emit("request_rescan", {program: prog}); showBanner(`Rescanning ${prog}...`); }

        // --- Queue controls (no page reload) ---
        // Binary selector
        async function switchBinary(binary) {
            await fetch('/api/context/binary', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({binary: binary || null})});
            location.reload();
        }
        async function switchFolder(folder) {
            await fetch('/api/context/folder', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({folder})});
            // Clear binary filter when switching folders
            await fetch('/api/context/binary', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({binary: null})});
            location.reload();
        }
        function switchBinaryDirect(prog) {
            document.getElementById('binarySelect').value = prog;
            switchBinary(prog);
        }

        async function queueFunc(key) {
            const r = await fetch('/api/queue/pin', {
                method:'POST',
                headers:{'Content-Type':'application/json'},
                body:JSON.stringify({key}),
            });
            try {
                const data = await r.json();
                if (data.status === 'already_done') {
                    showBanner(`Already at ${data.score}% (>= good@${data.good_enough}) — auto-dequeued`);
                    setTimeout(hideBanner, 4000);
                } else if (data.score !== undefined && data.was_unscored) {
                    showBanner(`Queued ${key.split('::').pop()} — scored at ${data.score}%`);
                    setTimeout(hideBanner, 3000);
                }
            } catch (_) { /* non-JSON response, fall through */ }
            refreshAllFunctions();
            refreshStats();
        }
        async function dequeueFunc(key) {
            await fetch('/api/queue/unpin', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({key})});
            refreshAllFunctions();
        }
        // Aliases for any legacy onclick handlers still in DOM from server-rendered initial page
        const pinFunc = queueFunc;
        const unpinFunc = dequeueFunc;

        async function saveQueueConfig() {
            const good = parseInt(document.getElementById('goodEnoughInput').value);
            const cold = document.getElementById('requireScoredInput').checked;
            const handoffSel = document.getElementById('handoffProviderInput');
            const handoff = handoffSel ? handoffSel.value : 'off';
            const handoffMaxEl = document.getElementById('handoffMaxInput');
            const handoffMax = handoffMaxEl ? (parseInt(handoffMaxEl.value) || 0) : 0;
            const debugEl = document.getElementById('debugModeInput');
            const debug = debugEl ? debugEl.checked : false;
            const auditSel = document.getElementById('auditProviderInput');
            const audit = auditSel ? auditSel.value : 'off';
            const auditDeltaEl = document.getElementById('auditMinDeltaInput');
            const auditDelta = auditDeltaEl ? (parseInt(auditDeltaEl.value) || 5) : 5;
            const providerMaxTurns = collectProviderMaxTurns();
            const providerModels = collectProviderModels();
            const newConfig = {
                good_enough_score: good,
                require_scored: cold,
                complexity_handoff_provider: handoff === 'off' ? null : handoff,
                complexity_handoff_max: handoffMax,
                debug_mode: debug,
                audit_provider: audit === 'off' ? null : audit,
                audit_min_delta: auditDelta,
                provider_max_turns: providerMaxTurns,
                provider_models: providerModels,
            };
            await fetch('/api/queue/config', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(newConfig),
            });
            // Toast informing the user that running workers keep their
            // frozen snapshots — the new config applies to NEXT workers.
            // Only fires when at least one running worker's snapshot
            // diverges from what was just saved; if every running worker
            // already matches (or there are no running workers), the save
            // was a no-op for everyone and the toast would just be noise.
            const drifted = countDriftedWorkers(newConfig);
            if (drifted > 0) {
                showBanner(
                    `Config saved. ${drifted} running worker${drifted === 1 ? '' : 's'} ` +
                    `will keep ${drifted === 1 ? 'its' : 'their'} current settings until ` +
                    `restarted. New workers will use the updated config.`
                );
                setTimeout(hideBanner, 6000);
            }
            // queue_changed event will trigger a refreshStats which repaints the queue
        }

        // Compare each running worker's frozen snapshot against the config
        // the user just saved. Returns the count of workers whose snapshot
        // differs from the new config in any of the snapshotted fields.
        // The cached worker_status payload (stamped by the worker_status
        // socket handler) is the source of truth for current snapshots.
        function countDriftedWorkers(newConfig) {
            const workers = window.__lastWorkerStatus || [];
            let count = 0;
            for (const w of workers) {
                if (!w || (w.status !== 'running' && w.status !== 'starting')) continue;
                const snap = w.config_snapshot;
                if (!snap) continue;  // legacy worker, no snapshot to drift
                if (snapshotDiffersFromConfig(snap, newConfig)) count++;
            }
            return count;
        }

        function snapshotDiffersFromConfig(snap, cfg) {
            if (snap.good_enough_score !== cfg.good_enough_score) return true;
            if ((snap.audit_provider || null) !== (cfg.audit_provider || null)) return true;
            if (snap.audit_min_delta !== cfg.audit_min_delta) return true;
            if ((snap.complexity_handoff_provider || null) !==
                (cfg.complexity_handoff_provider || null)) return true;
            if (snap.complexity_handoff_max !== cfg.complexity_handoff_max) return true;
            // Per-provider drift: if the snapshot captured a provider that
            // appears in the new config with a different max_turns or model
            // slice, that's drift the user might want to know about.
            const snapProvs = snap.providers || {};
            for (const provName of Object.keys(snapProvs)) {
                const snapEntry = snapProvs[provName] || {};
                const cfgTurns = (cfg.provider_max_turns || {})[provName];
                if (cfgTurns !== undefined && cfgTurns !== snapEntry.max_turns) return true;
                const cfgModels = (cfg.provider_models || {})[provName] || {};
                const snapModels = snapEntry.models || {};
                for (const mode of ['FULL', 'FIX', 'VERIFY']) {
                    if ((cfgModels[mode] || '') !== (snapModels[mode] || '')) return true;
                }
            }
            return false;
        }

        function collectProviderMaxTurns() {
            const providerMaxTurns = {};
            ['minimax', 'gemini', 'claude', 'codex'].forEach(provider => {
                const input = document.getElementById(`turns-${provider}`);
                providerMaxTurns[provider] = input ? (parseInt(input.value) || 25) : 25;
            });
            return providerMaxTurns;
        }

        // --- All Functions: server-side search + filter ---
        let _searchTimer = null;
        let _currentFilter = 'all';
        function onSearchInput() {
            _currentLayerFilter = null;  // clear layer filter on manual search
            if (_searchTimer) clearTimeout(_searchTimer);
            _searchTimer = setTimeout(runServerSearch, 250);
        }
        function refreshAllFunctions() {
            if (_searchTimer) clearTimeout(_searchTimer);
            _searchTimer = setTimeout(runServerSearch, 100);
        }
        function runServerSearch() {
            const q = document.getElementById('searchInput').value.trim();
            const sort = _getSortValue();
            const program = document.getElementById('binarySelect').value || '';
            const params = new URLSearchParams();
            if (q) params.set('q', q);
            if (program) params.set('program', program);
            params.set('sort', sort);
            params.set('limit', '5000');
            // Layer filter from Call Graph Layers click
            if (_currentLayerFilter !== null) {
                params.set('layer', _currentLayerFilter);
            }
            fetch('/api/functions/search?' + params.toString())
                .then(r => r.json())
                .then(data => repaintAllFunctions(data.results || [], data.total || 0));
            // Clear the layer filter after use so normal searches work
            // (it persists until the user types a new search or clicks a filter button)
        }
        function repaintAllFunctions(rows, totalMatch) {
            const tbody = document.getElementById('allFuncsBody');
            if (!tbody) return;
            const goodEnough = parseInt(document.getElementById('goodEnoughInput').value) || 80;
            const filtered = rows.filter(f => {
                if (_currentFilter === 'needs_work') return f.score < 70;
                if (_currentFilter === 'fixable') return f.score >= 70 && f.score < goodEnough;
                if (_currentFilter === 'done') return f.score >= goodEnough;
                return true;
            });
            tbody.innerHTML = filtered.map(f => {
                const queueBtn = f.pinned
                    ? `<button class="btn btn-primary" onclick="dequeueFunc('${escapeHtml(f.key)}')">Dequeue</button>`
                    : `<button class="btn" onclick="queueFunc('${escapeHtml(f.key)}')">Queue</button>`;
                const queuedBadge = f.pinned ? '<span class="badge badge-pinned">QUEUED</span> ' : '';
                // Unscored entries: state has never had analyze_function_completeness
                // run, so score=0 means "unknown" not "0% done". Render with a dash
                // and an UNSCORED badge so the user doesn't think it's a real target.
                let scoreCell, statusBadge;
                if (f.unscored) {
                    scoreCell = `<td class="score-cell" style="color: var(--text-muted);">—</td>`;
                    statusBadge = '<span class="badge badge-new" title="Never scored — click Queue to score on demand">UNSCORED</span>';
                } else {
                    scoreCell = `<td class="score-cell ${scoreCellClass(f.score)}">${f.score}%</td>`;
                    if (f.score >= goodEnough) statusBadge = '<span class="badge badge-done">DONE</span>';
                    else if (f.score >= 70) statusBadge = '<span class="badge badge-fix">FIX</span>';
                    else statusBadge = '<span class="badge badge-new">NEW</span>';
                }
                // Deps remaining column
                let depsLabel;
                if (f.is_leaf) {
                    depsLabel = '<span class="badge badge-leaf">Leaf</span>';
                } else if (f.deps_remaining === 0) {
                    depsLabel = '<span style="color: var(--color-set); font-family: var(--font-mono); font-size: 0.65rem;">Ready</span>';
                } else if (f.deps_remaining != null) {
                    depsLabel = `<span style="color: var(--color-rare); font-family: var(--font-mono); font-size: 0.65rem;">${f.deps_remaining} dep${f.deps_remaining > 1 ? 's' : ''}</span>`;
                } else {
                    depsLabel = '';
                }
                // Layer column
                let layerLabel;
                if (f.is_leaf) {
                    layerLabel = 'Leaf';
                } else if (f.call_graph_layer != null) {
                    layerLabel = `L${f.call_graph_layer}`;
                } else {
                    layerLabel = 'Cyc';
                }
                const toolCallsCell = (f.tool_calls == null || f.tool_calls < 0)
                    ? `<td class="mono" style="color: var(--text-muted);" title="Provider didn't expose a tool-call count">&mdash;</td>`
                    : `<td class="mono" style="color: var(--text-secondary);">${f.tool_calls}</td>`;
                // Source column (#204) — name-provenance. Highlights propagation
                // rows that the selector will skip.
                const ns = f.name_source || 'scan';
                let sourceCell;
                if (f.skip_reason === 'propagation') {
                    const srcBin = escapeHtml(f.name_source_binary || 'unknown');
                    sourceCell = `<td class="mono" style="font-size: 0.6rem;"><span style="color: var(--color-rare);" title="Skipped by selector: name came from cross-version hash propagation, no archive backing. Source binary: ${srcBin}. Pin to override.">propagation</span></td>`;
                } else if (ns === 'propagation') {
                    const srcBin = escapeHtml(f.name_source_binary || 'unknown');
                    const conf = f.name_confidence != null ? f.name_confidence : '?';
                    sourceCell = `<td class="mono" style="font-size: 0.6rem;"><span style="color: var(--color-set);" title="Name from propagation; high-confidence (${conf}), admitted by selector. Source: ${srcBin}">propagation</span></td>`;
                } else if (ns === 'manual') {
                    sourceCell = `<td class="mono" style="font-size: 0.6rem;"><span style="color: var(--color-set);" title="Renamed by worker">manual</span></td>`;
                } else if (ns === 'pdb') {
                    sourceCell = `<td class="mono" style="font-size: 0.6rem;"><span style="color: var(--color-set);" title="From PDB import">pdb</span></td>`;
                } else if (ns === 'archive') {
                    sourceCell = `<td class="mono" style="font-size: 0.6rem;"><span style="color: var(--color-set);" title="From re-kb archive">archive</span></td>`;
                } else {
                    sourceCell = `<td class="mono" style="font-size: 0.6rem;"><span style="color: var(--text-muted);" title="Default Ghidra label or set during scan">scan</span></td>`;
                }
                return `<tr class="func-row" data-score="${f.score}" data-name="${escapeHtml((f.name||'').toLowerCase())}" data-key="${escapeHtml(f.key)}">
                    <td>${queueBtn}</td>
                    <td>${queuedBadge}${escapeHtml(f.name)}</td>
                    <td class="mono">0x${escapeHtml(f.address)}</td>
                    ${scoreCell}
                    <td>${depsLabel}</td>
                    <td class="mono">${f.fixable}</td>
                    ${toolCallsCell}
                    <td class="mono" style="font-size: 0.6rem; color: var(--text-secondary);">${layerLabel}</td>
                    ${sourceCell}
                    <td>${statusBadge}</td>
                </tr>`;
            }).join('');
            const counter = document.getElementById('allFuncsCount');
            if (counter) counter.textContent = `showing ${filtered.length} of ${totalMatch} matches`;
        }

        // --- Worker controls ---
        function startWorker() {
            const provider = document.getElementById('workerProvider').value;
            const count = parseInt(document.getElementById('workerCount').value) || 10;
            const continuous = document.getElementById('workerContinuous').checked;
            const binary = document.getElementById('binarySelect').value || null;
            socket.emit('request_start_worker', {provider, count, binary, continuous});
            const btn = document.getElementById('btnRunWorker');
            btn.disabled = true;
            setTimeout(() => { btn.disabled = false; }, 2000);
        }
        // Globals worker — Q1/Q9: requires a selected binary; the button is
        // disabled when none is chosen so the user can't accidentally launch
        // a worker that has no target.
        function startGlobalsWorker() {
            const provider = document.getElementById('workerProvider').value;
            const count = parseInt(document.getElementById('workerCount').value) || 10;
            const continuous = document.getElementById('workerContinuous').checked;
            const binary = document.getElementById('binarySelect').value || null;
            if (!binary) return;  // defensive — button should be disabled
            socket.emit('request_start_globals_worker', {provider, count, binary, continuous});
            const btn = document.getElementById('btnRunGlobalsWorker');
            btn.disabled = true;
            // Re-enable after 2s; _refreshGlobalsWorkerButton will re-disable
            // if the binary selector got cleared in the meantime.
            setTimeout(_refreshGlobalsWorkerButton, 2000);
        }
        function _refreshGlobalsWorkerButton() {
            const btn = document.getElementById('btnRunGlobalsWorker');
            const sel = document.getElementById('binarySelect');
            if (!btn || !sel) return;
            const has = !!sel.value;
            btn.disabled = !has;
            btn.title = has
                ? `Run a globals doc worker on ${sel.value}`
                : 'Select a binary in the dropdown above to enable';
        }
        document.addEventListener('DOMContentLoaded', () => {
            _refreshGlobalsWorkerButton();
            const sel = document.getElementById('binarySelect');
            if (sel) sel.addEventListener('change', _refreshGlobalsWorkerButton);
        });
        function toggleSettingsPanel(forceOpen) {
            const popout = document.getElementById('settingsPopout');
            const button = document.getElementById('btnToggleSettings');
            if (!popout || !button) return;
            const open = forceOpen === undefined ? !popout.classList.contains('active') : !!forceOpen;
            popout.classList.toggle('active', open);
            button.classList.toggle('btn-primary', open);
            button.setAttribute('aria-expanded', open ? 'true' : 'false');
        }
        function collectProviderModels() {
            const providers = ['minimax', 'gemini', 'claude', 'codex'];
            const modes = ['FULL', 'FIX', 'VERIFY'];
            const providerModels = {};
            providers.forEach(provider => {
                modes.forEach(mode => {
                    const input = document.getElementById(`model-${provider}-${mode}`);
                    const value = input ? input.value.trim() : '';
                    if (!value) return;
                    if (!providerModels[provider]) providerModels[provider] = {};
                    providerModels[provider][mode] = value;
                });
            });
            return providerModels;
        }
        function stopWorker(wid) { socket.emit('request_stop_worker', {worker_id: wid}); }
        function stopAllWorkers() {
            // Stop every worker that has a visible pane
            Object.keys(workerPanes).forEach(wid => {
                const pane = workerPanes[wid];
                if (pane && !pane.element.classList.contains('finished')) {
                    socket.emit('request_stop_worker', {worker_id: wid});
                }
            });
        }

        socket.on('worker_started_ack', d => { /* pane created by worker_started event */ });
        socket.on('worker_error', d => { showBanner(`Worker error: ${d.error}`); setTimeout(hideBanner, 5000); });
        socket.on('worker_stop_ack', d => { /* stop handled by worker_status update */ });
        socket.on('worker_started', d => addActivity(`Worker ${d.worker_id}: ${d.provider} ${d.restored ? 'restored ' : ''}${d.continuous ? 'Auto' : 'x' + d.count}`, 'scan'));
        socket.on('worker_progress', d => {
            if (d.current) addActivity(`Worker ${d.worker_id}: ${d.current.name} (${d.completed+1}/${d.total})`, 'scan');
        });
        socket.on('worker_stopped', d => {
            addActivity(`Worker ${d.worker_id} ${d.reason}`, d.reason === 'finished' ? 'success' : 'neutral');
            refreshStats();
        });
        socket.on('provider_timeout', d => addActivity(`Worker ${d.worker_id}: timeout ${d.provider || 'provider'} (${d.timeout_secs || '?'}s)`, 'error'));
        socket.on('worker_status', workers => {
            // Update pane borders and status text based on worker status.
            // On page refresh the local workerPanes map starts empty but the
            // server still knows about the running workers — use its
            // authoritative provider/model/binary info to rebuild panes with
            // the correct title (e.g. "codex #abc123" instead of "? #abc123"
            // that we'd otherwise get from the function_started fallback).
            workers.forEach(w => {
                let pane = workerPanes[w.id];
                if (!pane) {
                    pane = getOrCreatePane(w.id, w.provider, w.binary || '');
                    // If the worker has already finished/stopped by the time
                    // we reconnect, restore the finished-state styling so the
                    // user can dismiss it.
                    if (w.status === 'finished' || w.status === 'stopped') {
                        markPaneFinished(w.id, w.status, w.exit_reason);
                    }
                }
                // Keep title in sync in case provider/model were still
                // unknown when the pane was first created (e.g. created
                // from a function_started fallback with null provider).
                const titleEl = pane.element.querySelector('.wp-title');
                if (titleEl && w.provider) {
                    const titleText = `${w.provider} #${w.id}`;
                    if (titleEl.textContent !== titleText) {
                        titleEl.textContent = titleText;
                    }
                }
                const cls = w.status === 'running' ? 'running'
                    : w.status === 'stopping' ? 'stopping'
                    : w.status === 'quota_paused' ? 'quota_paused'
                    : 'finished';
                // Q2 C: detect transition INTO quota_paused so we close any
                // function block left open by the worker yielding mid-loop —
                // never leave a header without a matching footer.
                if (
                    w.status === 'quota_paused'
                    && pane.lastStatus !== 'quota_paused'
                    && pane.currentFnBlock
                ) {
                    _closeFnBlock(pane, null, null, true /* interrupted */);
                }
                pane.lastStatus = w.status;
                pane.element.className = `worker-pane ${cls}`;
                setPaneAlert(w.id, w.last_alert);
                const done = w.progress.completed + w.progress.skipped + w.progress.failed;
                const status = document.getElementById(`wp-status-${w.id}`);
                if (status) {
                    if (w.status === 'quota_paused' && w.paused_until) {
                        // Quota wall: replace the status line with a wake-time
                        // countdown so the operator can see when work resumes.
                        const remaining = _formatQuotaCountdown(w.paused_until);
                        const reason = w.paused_reason ? ` — ${escapeHtml(w.paused_reason).slice(0, 60)}` : '';
                        status.innerHTML = `<span class="wp-current">paused</span> until ${escapeHtml(w.paused_until.replace('T',' ').slice(0,19))} (${remaining})${reason}`;
                    } else {
                        const countStr = w.continuous ? 'auto' : `${done}/${w.count}`;
                        const cur = w.progress.current ? w.progress.current.name : '';
                        const skipStr = w.progress.skipped > 0 ? ` ${w.progress.skipped}skip` : '';
                        const timeoutStr = w.timeout_count > 0 ? ` | ${w.timeout_count} timeout${w.timeout_count > 1 ? 's' : ''}` : '';
                        const restoredStr = w.restored ? 'restored | ' : '';
                        const prefix = `${restoredStr}${countStr} | ${w.progress.completed}ok ${w.progress.failed}fail${skipStr}${timeoutStr}`;
                        status.innerHTML = escapeHtml(prefix) + (cur ? ' | <span class="wp-current">' + escapeHtml(cur) + '</span>' : '');
                    }
                }
                // Snapshot sub-line. Frozen at worker start, never changes
                // for this worker. Hidden when no snapshot present.
                renderWorkerConfig(w.id, w.config_snapshot);
            });
            // Cache the latest worker_status payload so the config-save
            // toast (Q5) can compare current snapshots against the new
            // queue config without an extra round-trip.
            window.__lastWorkerStatus = workers;
            // Show/hide Stop All button based on active workers
            const activeCount = workers.filter(w => w.status === 'running' || w.status === 'stopping').length;
            const stopAllBtn = document.getElementById('btnStopAll');
            const gridCount = document.getElementById('workerGridCount');
            if (stopAllBtn) stopAllBtn.style.display = activeCount > 0 ? '' : 'none';
            if (gridCount) gridCount.textContent = activeCount > 0 ? `${activeCount} active worker${activeCount > 1 ? 's' : ''}` : '';
        });

        // --- Column header sort ---
        // Maps column names to their API sort values for asc/desc
        const _sortColumns = {
            name:    { asc: 'name',      desc: 'name_desc'  },
            address: { asc: 'address',   desc: 'address_desc' },
            score:   { asc: 'score',     desc: 'score_desc' },
            deps:    { asc: 'deps_asc',  desc: 'deps_desc'  },
            fixable: { asc: 'fixable',   desc: 'fixable_desc' },
            layer:   { asc: 'layer',     desc: 'layer_desc' },
            status:  { asc: 'status',    desc: 'status_desc' },
        };
        let _currentSortCol = null;   // which column is active
        let _currentSortDir = null;   // 'asc' or 'desc'

        function toggleSort(col) {
            if (_currentSortCol === col) {
                // Same column: asc → desc → off
                if (_currentSortDir === 'asc') {
                    _currentSortDir = 'desc';
                } else {
                    // Was desc (or anything else) → clear
                    _currentSortCol = null;
                    _currentSortDir = null;
                }
            } else {
                // New column: start with asc
                _currentSortCol = col;
                _currentSortDir = 'asc';
            }
            // Update indicators
            for (const c of Object.keys(_sortColumns)) {
                const ind = document.getElementById(`sort-ind-${c}`);
                if (!ind) continue;
                if (c === _currentSortCol && _currentSortDir === 'asc') {
                    ind.textContent = '▲';
                } else if (c === _currentSortCol && _currentSortDir === 'desc') {
                    ind.textContent = '▼';
                } else {
                    ind.textContent = '';
                }
            }
            runServerSearch();
        }

        function _getSortValue() {
            if (!_currentSortCol || !_currentSortDir) return 'score'; // default
            const col = _sortColumns[_currentSortCol];
            return col ? (col[_currentSortDir] || 'score') : 'score';
        }

        // --- Layer filter ---
        let _currentLayerFilter = null;

        function filterByLayer(layerId, layerLabel) {
            // Switch to list view
            switchView('list');
            // Set the dropdown to match
            const sel = document.getElementById('layerFilterSelect');
            if (sel) sel.value = layerId;
            // Store the layer filter and re-run search
            _currentLayerFilter = layerId || null;
            runServerSearch();
        }

        function onLayerDropdownChanged() {
            const sel = document.getElementById('layerFilterSelect');
            const val = sel ? sel.value : '';
            _currentLayerFilter = val || null;
            // Switch to list view if on graph tab
            const graphPanel = document.getElementById('callGraphPanel');
            if (graphPanel && graphPanel.style.display !== 'none') {
                switchView('list');
            }
            runServerSearch();
        }

        async function populateLayerDropdown() {
            const sel = document.getElementById('layerFilterSelect');
            if (!sel) return;
            try {
                const resp = await fetch('/api/call_graph_layers');
                const data = await resp.json();
                if (!data.layers) return;
                // Preserve current selection
                const current = sel.value;
                // Clear all options except the first ("All Layers")
                while (sel.options.length > 1) sel.remove(1);
                // Add layer options
                for (const layer of data.layers) {
                    const opt = document.createElement('option');
                    opt.value = layer.label === 'Leaves' ? '0' : layer.label === 'Cyclic' ? 'cyclic' : String(layer.depth);
                    const remaining = layer.total - layer.done;
                    opt.textContent = `${layer.label} (${remaining} remaining)`;
                    sel.appendChild(opt);
                }
                // Restore selection
                if (current) sel.value = current;
            } catch (e) {
                // silently fail — dropdown just won't have options
            }
        }

        // Populate on page load
        populateLayerDropdown();

        // --- Worker Focus ---
        let _focusedWorkerId = null;
        let _focusedFuncAddr = null;
        let _focusedFuncName = null;
        let _focusedWorkerName = null;

        function setFocusButtonState(workerId, focused) {
            const btn = document.getElementById(`wp-focus-${workerId}`);
            if (!btn) return;
            btn.classList.toggle('btn-primary', focused);
            btn.setAttribute('aria-pressed', focused ? 'true' : 'false');
            btn.setAttribute('aria-label', focused ? 'Unfocus worker' : 'Focus worker');
            btn.setAttribute('title', focused ? 'Unfocus worker' : 'Focus worker');
        }

        function focusWorker(workerId) {
            // Toggle off if already focused
            if (_focusedWorkerId === workerId) {
                unfocusWorker();
                return;
            }
            // Clear previous focus
            if (_focusedWorkerId) {
                const prevPane = workerPanes[_focusedWorkerId];
                if (prevPane) prevPane.element.classList.remove('focused');
                setFocusButtonState(_focusedWorkerId, false);
            }
            _focusedWorkerId = workerId;
            // Highlight the pane
            const pane = workerPanes[workerId];
            if (pane) pane.element.classList.add('focused');
            setFocusButtonState(workerId, true);
            // Update banner with worker name + current function
            const titleEl = pane?.element.querySelector('.wp-title');
            _focusedWorkerName = titleEl ? titleEl.textContent : workerId;
            document.getElementById('focusWorkerName').textContent = _focusedWorkerName;
            // Read the current function from the pane (set by function_started events)
            if (pane && pane.currentFuncAddr) {
                _focusedFuncAddr = pane.currentFuncAddr;
                _focusedFuncName = pane.currentFuncName || '—';
                document.getElementById('focusFuncName').textContent = _focusedFuncName;
                document.getElementById('focusFuncAddr').textContent = `@ 0x${_focusedFuncAddr}`;
            }
            // Show the banner
            document.getElementById('focusBanner').classList.add('active');
            // Navigate to whatever function this worker is currently on
            navigateToFocusedFunction();
        }

        function unfocusWorker() {
            if (_focusedWorkerId) {
                const pane = workerPanes[_focusedWorkerId];
                if (pane) pane.element.classList.remove('focused');
                setFocusButtonState(_focusedWorkerId, false);
            }
            _focusedWorkerId = null;
            _focusedFuncAddr = null;
            _focusedFuncName = null;
            document.getElementById('focusBanner').classList.remove('active');
            document.getElementById('autoFollowCheckbox').checked = false;
        }

        function updateFocusedFunction(workerId, name, address) {
            if (workerId !== _focusedWorkerId) return;
            _focusedFuncName = name;
            _focusedFuncAddr = address;
            document.getElementById('focusFuncName').textContent = name || '—';
            document.getElementById('focusFuncAddr').textContent = address ? `@ 0x${address}` : '';
            // Auto-follow: navigate Ghidra if checkbox is on
            if (document.getElementById('autoFollowCheckbox').checked) {
                navigateToFocusedFunction();
            }
        }

        function navigateToFocusedFunction() {
            if (!_focusedFuncAddr) return;
            fetch('/api/navigate', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ address: _focusedFuncAddr })
            }).catch(() => {});
        }

        function onAutoFollowChanged() {
            // If just turned on, navigate immediately
            if (document.getElementById('autoFollowCheckbox').checked && _focusedFuncAddr) {
                navigateToFocusedFunction();
            }
        }

        // --- Filter ---
        function filterFuncs(mode, btn) {
            document.querySelectorAll('.filter-bar .btn').forEach(b => b.classList.remove('active'));
            if (btn) btn.classList.add('active');
            _currentFilter = mode;
            _currentLayerFilter = null;  // clear layer filter on manual filter click
            runServerSearch();
        }

        // --- Call Graph Layers tab ---
        function switchView(view) {
            document.getElementById('tab-list').classList.toggle('active', view === 'list');
            document.getElementById('tab-graph').classList.toggle('active', view === 'graph');
            const allFuncsPanel = document.getElementById('allFuncsPanel');
            const graphPanel = document.getElementById('callGraphPanel');
            if (allFuncsPanel) allFuncsPanel.style.display = view === 'list' ? '' : 'none';
            if (graphPanel) graphPanel.style.display = view === 'graph' ? '' : 'none';
            if (view === 'graph') loadCallGraphLayers();
        }

        async function loadCallGraphLayers() {
            const el = document.getElementById('callGraphContent');
            if (!el) return;
            try {
                const resp = await fetch('/api/call_graph_layers');
                const data = await resp.json();
                if (!data.layers || data.layers.length === 0) {
                    el.innerHTML = '<div style="text-align: center; color: var(--text-muted); padding: 1rem;">No call graph data. Run a scan with --refresh to populate callee data.</div>';
                    return;
                }
                const summary = `<div style="font-size: 0.65rem; color: var(--text-muted); margin-bottom: 0.5rem; font-family: var(--font-mono);">`
                    + `${data.total_functions} functions &middot; ${data.assigned} in clean layers &middot; ${data.cyclic} in call cycles &middot; max depth ${data.max_depth}`
                    + `</div>`;
                const bars = data.layers.map(layer => {
                    const pct = layer.total > 0 ? Math.round(layer.done / layer.total * 100) : 0;
                    const barClass = pct >= 100 ? 's100' : pct >= 80 ? 's80' : pct >= 50 ? 's50' : pct >= 20 ? 's20' : 's0';
                    const readyStyle = layer.ready > 0 ? 'color: var(--color-set); font-weight: 600;' : 'color: var(--text-muted);';
                    // Layer ID for filtering: "0" for leaves, "1"-"14" for layers, "cyclic" for cyclic
                    const layerId = layer.label === 'Leaves' ? '0' : layer.label === 'Cyclic' ? 'cyclic' : String(layer.depth);
                    return `<div class="score-bar-row" style="margin-bottom: 4px; cursor: pointer;" onclick="filterByLayer('${layerId}', '${layer.label}')" title="Click to view ${layer.label} functions in List View">
                        <span class="score-bar-label" style="min-width: 70px; font-size: 0.7rem;">${layer.label}</span>
                        <div class="score-bar-track" style="flex: 1;">
                            <div class="score-bar-fill ${barClass}" style="width: ${pct}%;"></div>
                        </div>
                        <span style="min-width: 180px; font-family: var(--font-mono); font-size: 0.65rem; text-align: right; padding-left: 8px;">
                            ${layer.done}/${layer.total} (${pct}%)
                            <span style="${readyStyle}">&nbsp;&bull; ${layer.ready} ready</span>
                        </span>
                    </div>`;
                }).join('');
                el.innerHTML = summary + bars;
            } catch (e) {
                el.innerHTML = `<div style="color: var(--accent-red-light); padding: 0.5rem;">Error loading call graph: ${e.message}</div>`;
            }
        }

        document.addEventListener('keydown', (event) => {
            if (event.key === 'Escape') toggleSettingsPanel(false);
        });

        // Initial inventory load on page open. The widget + table populate
        // even when the scorer is disabled (showing persisted last-scan data
        // from any prior session). Live updates flow via the
        // inventory_status WebSocket event.
        refreshInventory();
        setInterval(refreshInventory, 30000);
        refreshGlobalInventory();
        setInterval(refreshGlobalInventory, 30000);
    </script>
</body>
</html>
