{% extends "base.html" %}
{% block title %}Live — Drakkar Debug{% endblock %}
{% block head %}<style>#tl-viewport::-webkit-scrollbar { display: none; }</style>{% endblock %}

{% block content %}
<div class="flex items-center gap-3 mb-1">
    <h1 class="text-2xl font-bold text-ink-900">Live Pipeline</h1>
    <span id="ws-badge" class="text-xs px-2 py-0.5 rounded" style="background:#fecaca;color:#991b1b;border:1px solid #fca5a5;">WS: connecting</span>
    <span id="freeze-toggle" class="text-xs px-2 py-0.5 rounded cursor-pointer select-none" style="background:#f5f3ee;color:#6b7280;border:1px solid #ddd9ce;" onclick="toggleFreeze()">Live</span>
    <span class="text-gray-500 text-sm ml-auto">Pool: <span id="v-pool">{{ pool_active }} / {{ pool_max }}</span> slots, <span id="v-waiting" style="color:#b45309;">{{ pool_waiting }}</span> waiting</span>
</div>
<p id="tab-hint" class="text-xs text-gray-400 mb-3" style="line-height:1.4;"></p>

<!-- Tab bar -->
<div class="flex border-b border-cream-200 mb-4" id="tab-bar">
    <button class="tab-btn px-4 py-2 text-sm font-medium" data-tab="arrange" style="color:#6b7280;border-bottom:2px solid transparent;">Arrange</button>
    <button class="tab-btn px-4 py-2 text-sm font-medium" data-tab="execute" style="color:#6b7280;border-bottom:2px solid transparent;">Executors</button>
    <button class="tab-btn px-4 py-2 text-sm font-medium" data-tab="collect" style="color:#6b7280;border-bottom:2px solid transparent;">Collect</button>
</div>

<!-- ==================== ARRANGE TAB ==================== -->
<div class="tab-panel" id="panel-arrange" style="display:none;">
    <p class="text-gray-500 text-sm mb-3">Recent arrange() calls <span id="arrange-total" class="text-teal-600"></span></p>
    <table class="w-full text-sm">
        <thead>
            <tr class="text-left text-gray-400 border-b border-cream-200">
                <th class="py-2 px-3">Partition</th><th class="py-2 px-3">Messages</th><th class="py-2 px-3">Tasks</th>
                <th class="py-2 px-3">Duration</th><th class="py-2 px-3">Time</th><th class="py-2 px-3">Message Labels</th>
            </tr>
        </thead>
        <tbody id="arrange-body"></tbody>
    </table>
</div>

<!-- ==================== EXECUTORS TAB ==================== -->
<div class="tab-panel" id="panel-execute" style="display:none;">
    <div class="w-full bg-cream-200 rounded-full h-3 mb-4">
        {% set pct = (pool_active / pool_max * 100) if pool_max > 0 else 0 %}
        <div id="v-pool-bar" class="h-3 rounded-full transition-all {% if pct > 80 %}bg-red-400{% elif pct > 50 %}bg-amber-400{% else %}bg-emerald-400{% endif %}"
             style="width: {{ pct }}%"></div>
    </div>

    <h2 class="text-lg font-semibold mb-2 text-ink-800">Timeline <span class="text-sm font-normal text-gray-400">(last 10 min, 2 sec delayed)</span></h2>
    <div class="flex items-center gap-3 mb-2">
        <div class="flex items-center gap-1">
            <label class="text-xs text-gray-400">Zoom:</label>
            <button onclick="zoomIn()" class="bg-cream-200 hover:bg-cream-300 px-2 py-0.5 rounded text-sm font-bold">+</button>
            <span id="zoom-label" class="text-xs text-gray-400 w-10 text-center">2x</span>
            <button onclick="zoomOut()" class="bg-cream-200 hover:bg-cream-300 px-2 py-0.5 rounded text-sm font-bold">-</button>
            <button onclick="zoomReset()" class="bg-cream-200 hover:bg-cream-300 px-2 py-0.5 rounded text-xs ml-1">Reset</button>
        </div>
        <button onclick="scrollToEnd()" class="bg-cream-200 hover:bg-cream-300 px-2 py-1 rounded text-xs">Now &rarr;</button>
        <div class="text-xs text-gray-500 ml-auto">
            <span class="inline-block w-3 h-3 rounded mr-1" style="background:#34d399;"></span>completed
            <span class="inline-block w-3 h-3 rounded mr-1 ml-2" style="background:#fbbf24;"></span>running
            <span class="inline-block w-3 h-3 rounded mr-1 ml-2" style="background:#f87171;"></span>failed
        </div>
    </div>
    <div class="bg-white rounded-lg border border-cream-200 p-2 mb-6">
        <div id="tl-viewport" style="position:relative; overflow-x:scroll; overflow-y:hidden; scrollbar-width:none; -ms-overflow-style:none;">
            <div id="tl-inner" style="position:relative; min-height:100px;">
                <div id="tl-axis" style="position:relative; height:18px; border-bottom:1px solid #ddd9ce;"></div>
                <div id="tl-lanes" style="position:relative;"></div>
            </div>
        </div>
        <div id="tl-hover" class="border-t border-cream-200 px-3 py-2 text-xs font-mono text-gray-500" style="height:4.5em; overflow:hidden; background:#f9f8f5;">
            <span class="text-gray-400">hover over a task bar to see details</span>
        </div>
    </div>

    <details open class="mb-4">
        <summary class="text-lg font-semibold cursor-pointer select-none hover:text-ink-900">
            Running <span id="running-count" class="text-sm font-normal text-amber-600">(0)</span>
        </summary>
        <table class="w-full text-sm mt-2">
            <thead><tr class="text-left text-gray-400 border-b border-cream-200">
                <th class="py-2 px-3">Task ID</th><th class="py-2 px-3">Partition</th><th class="py-2 px-3">Duration</th><th class="py-2 px-3">CLI Args</th><th class="py-2 px-3">Stdin</th>
            </tr></thead>
            <tbody id="running-body"></tbody>
        </table>
    </details>
    <details class="mb-4">
        <summary class="text-lg font-semibold cursor-pointer select-none hover:text-ink-900">
            Finished <span id="finished-count" class="text-sm font-normal text-gray-400">(0)</span>
        </summary>
        <table class="w-full text-sm mt-2">
            <thead><tr class="text-left text-gray-400 border-b border-cream-200">
                <th class="py-2 px-3">Task ID</th><th class="py-2 px-3">Partition</th><th class="py-2 px-3">Status</th><th class="py-2 px-3">Duration</th><th class="py-2 px-3">Time</th><th class="py-2 px-3">CLI Args</th><th class="py-2 px-3">Stdin</th>
            </tr></thead>
            <tbody id="finished-body"></tbody>
        </table>
    </details>
</div>

<!-- ==================== COLLECT TAB ==================== -->
<div class="tab-panel" id="panel-collect" style="display:none;">
    <p class="text-gray-500 text-sm mb-3">Recent collect() completions <span id="collect-total" class="text-purple-600"></span></p>
    <table class="w-full text-sm">
        <thead>
            <tr class="text-left text-gray-400 border-b border-cream-200">
                <th class="py-2 px-3">Task ID</th><th class="py-2 px-3">Partition</th>
                <th class="py-2 px-3">Duration</th><th class="py-2 px-3">Output Messages</th><th class="py-2 px-3">Time</th>
            </tr>
        </thead>
        <tbody id="collect-body"></tbody>
    </table>
</div>

<script>
// === Tab management ===
var tabHints = {
    arrange: 'Arrange is called after message polling for each partition. It receives a batch of messages and creates executor Tasks with CLI params to run.',
    execute: 'Executors run CPU-bound Tasks as subprocesses with prepared CLI args, capturing stdout/stderr. Limited by the pool semaphore.',
    collect: 'Collect is called with each executor result and defines output messages for the target topic and database rows to persist.',
};

function switchTab(name) {
    document.querySelectorAll('.tab-panel').forEach(function(p) { p.style.display = 'none'; });
    document.querySelectorAll('.tab-btn').forEach(function(b) {
        b.style.color = '#6b7280'; b.style.borderBottom = '2px solid transparent';
    });
    var panel = document.getElementById('panel-' + name);
    var btn = document.querySelector('[data-tab="' + name + '"]');
    if (panel) panel.style.display = 'block';
    if (btn) { btn.style.color = '#1a1a1a'; btn.style.borderBottom = '2px solid #0d9488'; }
    var hint = document.getElementById('tab-hint');
    if (hint) hint.textContent = tabHints[name] || '';
    var hash = name;
    history.replaceState(null, '', '#' + hash);
}

document.querySelectorAll('.tab-btn').forEach(function(btn) {
    btn.addEventListener('click', function() { switchTab(btn.dataset.tab); });
});

// === Shared state ===
var MAX_LANES = {{ pool_max }};
var TIMELINE_MINUTES = 10;
var TIMELINE_DELAY = 2; // seconds behind real time for smooth rendering
var LANE_H = 22, LANE_GAP = 2, BASE_PX_PER_SEC = 8;
var zoomLevel = 2;
var hoveredTask = null;
var frozen = false;

function fmtStdin(t) {
    if (!t.stdin_size) return '-';
    return t.stdin_lines + ' lines, ' + formatSize(t.stdin_size);
}
function formatSize(bytes) {
    if (bytes < 1024) return bytes + ' B';
    if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
    return (bytes / 1048576).toFixed(1) + ' MB';
}

function toggleFreeze() {
    if (!frozen) {
        frozen = true;
        var btn = document.getElementById('freeze-toggle');
        if (btn) { btn.textContent = 'Frozen'; btn.style.background = '#dbeafe'; btn.style.color = '#1e40af'; btn.style.border = '1px solid #93c5fd'; }
    } else {
        // unfreeze — reload to catch up with current live state
        location.reload();
    }
}

var allTasks = {};
var recentArranges = [];
var recentCollects = [];
var MAX_HISTORY = {{ max_ui_rows }};
var arrangeTotal = 0, collectTotal = 0;
// === Helpers ===
function scrollToEnd() { tlFollowing = true; }
function fmtTime(ts) { return new Date(ts * 1000).toISOString().substring(11, 19); }
function fmtTimeMs(ts) { return ts ? new Date(ts * 1000).toISOString().substring(11, 23) : ''; }
function barColor(s) { return s === 'completed' ? '#34d399' : s === 'failed' ? '#f87171' : '#fbbf24'; }
function statusCss(s) { return s === 'completed' ? 'color:#059669' : s === 'failed' ? 'color:#dc2626' : 'color:#d97706'; }
function durColor(d) { return d > 1 ? '#dc2626' : d > 0.1 ? '#d97706' : '#059669'; }
function baseTaskId(tid) { return tid ? tid.split(':r')[0] : tid; }
function zoomIn() { zoomLevel = Math.min(zoomLevel * 2, 64); document.getElementById('zoom-label').textContent = zoomLevel + 'x'; rebuildAxis(); }
function zoomOut() { zoomLevel = Math.max(zoomLevel / 2, 0.25); document.getElementById('zoom-label').textContent = zoomLevel + 'x'; rebuildAxis(); }
function zoomReset() { zoomLevel = 2; document.getElementById('zoom-label').textContent = '2x'; rebuildAxis(); }

// === Timeline ===
function showTaskDetail(t) {
    hoveredTask = t;
    var el = document.getElementById('tl-hover');
    el.textContent = '';
    var dur = t.duration ? t.duration.toFixed(3) + 's' : 'running';
    var line1 = [
        {l:'Task', v:t.task_id}, {l:'Slot', v:t.slot != null ? '#'+t.slot : '-'}, {l:'PID', v:t.pid||'-'}, {l:'P', v:t.partition},
        {l:'Status', v:t.status, s:statusCss(t.status)}, {l:'Duration', v:dur},
        {l:'Start', v:fmtTimeMs(t.start_ts)}, {l:'End', v:t.end_ts ? fmtTimeMs(t.end_ts) : 'running'},
    ];
    for (var i = 0; i < line1.length; i++) {
        var p = line1[i];
        var lbl = document.createElement('span');
        lbl.style.cssText = 'color:#9ca3af;margin-right:2px;'; lbl.textContent = p.l + ':';
        el.appendChild(lbl);
        var val = document.createElement('span');
        if (p.s) val.style.cssText = p.s;
        val.style.marginRight = '12px'; val.textContent = p.v;
        el.appendChild(val);
    }
    el.appendChild(document.createElement('br'));
    var cliLbl = document.createElement('span');
    cliLbl.style.cssText = 'color:#9ca3af;margin-right:2px;'; cliLbl.textContent = 'CLI:';
    el.appendChild(cliLbl);
    var cliVal = document.createElement('span');
    cliVal.style.cssText = 'color:#6b7280;margin-right:12px;'; cliVal.textContent = t.args || '-';
    el.appendChild(cliVal);
    if (t.stdin_size) {
        var stdinLbl = document.createElement('span');
        stdinLbl.style.cssText = 'color:#9ca3af;margin-right:2px;'; stdinLbl.textContent = 'Stdin:';
        el.appendChild(stdinLbl);
        var stdinVal = document.createElement('span');
        stdinVal.style.color = '#6b7280'; stdinVal.textContent = fmtStdin(t);
        el.appendChild(stdinVal);
    }
}
function clearTaskDetail() {
    hoveredTask = null;
    var el = document.getElementById('tl-hover');
    el.textContent = '';
    var hint = document.createElement('span');
    hint.style.color = '#9ca3af'; hint.textContent = 'hover over a task bar to see details';
    el.appendChild(hint);
}

// === Timeline — persistent DOM elements, no rebuild ===
// Bars are created once and never destroyed (except on age-out).
// rAF loop updates: canvas width, running bar widths, scroll position.
// Axis ticks are rebuilt only on zoom change (cheap).

var tlAnchor = 0;       // fixed time reference for pixel positions
var tlFollowing = true;  // auto-follow the right edge
var taskBars = {};       // task_id -> {el: DOM element, task: task object}
var lastZoom = 0;        // detect zoom changes to rebuild axis
var laneStripesBuilt = false;

function tlPx(ts) {
    // convert a unix timestamp to pixel position relative to anchor
    return (ts - tlAnchor) * BASE_PX_PER_SEC * zoomLevel;
}
function tlEdgePx() {
    return (Date.now() / 1000 - TIMELINE_DELAY - tlAnchor) * BASE_PX_PER_SEC * zoomLevel;
}

function ensureAnchor() {
    var now = Date.now() / 1000 - TIMELINE_DELAY;
    var newAnchor = Math.floor((now - TIMELINE_MINUTES * 60) / 30) * 30;
    if (tlAnchor === 0 || newAnchor > tlAnchor + 30) {
        tlAnchor = newAnchor;
        return true; // anchor changed — need full axis rebuild
    }
    return false;
}

function buildLaneStripes() {
    var lanesDiv = document.getElementById('tl-lanes');
    if (!lanesDiv || laneStripesBuilt) return;
    lanesDiv.style.height = (MAX_LANES * (LANE_H + LANE_GAP)) + 'px';
    for (var i = 0; i < MAX_LANES; i++) {
        var y = i * (LANE_H + LANE_GAP);
        if (i % 2 === 0) {
            var stripe = document.createElement('div');
            stripe.style.cssText = 'position:absolute;left:0;width:100%;background:rgba(0,0,0,0.02);pointer-events:none;';
            stripe.style.top = y + 'px'; stripe.style.height = LANE_H + 'px';
            lanesDiv.appendChild(stripe);
        }
        var label = document.createElement('div');
        label.style.cssText = 'position:absolute;left:2px;font-size:10px;color:#9ca3af;font-family:monospace;pointer-events:none;z-index:1;';
        label.style.top = y + 'px'; label.style.lineHeight = LANE_H + 'px';
        label.textContent = '#' + i;
        lanesDiv.appendChild(label);
    }
    laneStripesBuilt = true;
}

function rebuildAxis() {
    var axis = document.getElementById('tl-axis');
    if (!axis) return;
    axis.replaceChildren();
    var pxPerSec = BASE_PX_PER_SEC * zoomLevel;
    var now = Date.now() / 1000 - TIMELINE_DELAY;
    var canvasW = (now - tlAnchor) * pxPerSec;
    axis.style.width = Math.ceil(canvasW) + 'px';

    var tick = 30;
    if (pxPerSec >= 16) tick = 10; if (pxPerSec >= 32) tick = 5;
    if (pxPerSec < 4) tick = 60; if (pxPerSec < 1) tick = 120;
    for (var t = Math.ceil(tlAnchor / tick) * tick; t <= now; t += tick) {
        var x = (t - tlAnchor) * pxPerSec;
        var line = document.createElement('div');
        line.style.cssText = 'position:absolute;top:0;width:1px;height:100%;background:#e5e7eb;';
        line.style.left = x + 'px'; axis.appendChild(line);
        var lbl = document.createElement('div');
        lbl.style.cssText = 'position:absolute;top:2px;font-size:9px;color:#9ca3af;font-family:monospace;transform:translateX(-50%);';
        lbl.style.left = x + 'px'; lbl.textContent = fmtTime(t);
        axis.appendChild(lbl);
    }
    lastZoom = zoomLevel;
}

// Create or update a single task bar DOM element
function ensureBar(task) {
    var id = task.task_id;
    if (!task.start_ts) return;

    var lane = task.slot != null ? task.slot : 0;
    task.lane = lane;

    var entry = taskBars[id];
    if (!entry) {
        // create new bar
        var el = document.createElement('a');
        el.href = '/task/' + encodeURIComponent(id);
        el.style.cssText = 'position:absolute;border-radius:3px;cursor:pointer;box-shadow:0 1px 2px rgba(0,0,0,0.1);border:1px solid rgba(0,0,0,0.15);z-index:2;';
        el.style.height = LANE_H + 'px';
        el.style.top = (lane * (LANE_H + LANE_GAP)) + 'px';
        (function(t) {
            el.addEventListener('mouseenter', function() { showTaskDetail(t); });
            el.addEventListener('mouseleave', clearTaskDetail);
        })(task);
        var lanesDiv = document.getElementById('tl-lanes');
        if (lanesDiv) lanesDiv.appendChild(el);
        entry = {el: el, task: task};
        taskBars[id] = entry;
    }

    // update color (may change on completion/failure/retry)
    entry.el.style.backgroundColor = barColor(task.status);
    entry.task = task;
}

// Remove bars for tasks no longer in allTasks
function pruneOldBars() {
    var ids = Object.keys(taskBars);
    for (var i = 0; i < ids.length; i++) {
        if (!allTasks[ids[i]]) {
            var el = taskBars[ids[i]].el;
            if (el.parentNode) el.parentNode.removeChild(el);
            delete taskBars[ids[i]];
        }
    }
}

// rAF loop: update bar positions/widths, canvas size, scroll — 60fps, no DOM rebuild
function animateTimeline() {
    if (frozen) { requestAnimationFrame(animateTimeline); return; }

    var vp = document.getElementById('tl-viewport');
    var inner = document.getElementById('tl-inner');
    if (!vp || !inner) { requestAnimationFrame(animateTimeline); return; }

    ensureAnchor();

    var now = Date.now() / 1000 - TIMELINE_DELAY;
    var pxPerSec = BASE_PX_PER_SEC * zoomLevel;
    var edge = tlEdgePx();
    inner.style.width = Math.ceil(edge) + 'px';

    // update all bar positions and widths
    var cutoffTs = tlAnchor; // don't show bars before anchor
    var ids = Object.keys(taskBars);
    for (var i = 0; i < ids.length; i++) {
        var entry = taskBars[ids[i]];
        var t = entry.task;
        var el = entry.el;
        var endTs = t.end_ts || now;
        var bx = (t.start_ts - tlAnchor) * pxPerSec;
        var bw = Math.max(2, (endTs - t.start_ts) * pxPerSec);

        // hide bars completely outside the visible canvas
        if (bx + bw < 0 || t.start_ts < cutoffTs) {
            el.style.display = 'none';
        } else {
            el.style.display = '';
            el.style.left = bx + 'px';
            el.style.width = bw + 'px';
        }
    }

    // scroll
    var maxScroll = Math.max(0, edge - vp.clientWidth);
    if (vp.scrollLeft > maxScroll) vp.scrollLeft = maxScroll;
    if (tlFollowing) vp.scrollLeft = maxScroll;

    requestAnimationFrame(animateTimeline);
}

// Initial population: create bars for all tasks loaded from API
function initBars() {
    buildLaneStripes();
    var ids = Object.keys(allTasks);
    for (var i = 0; i < ids.length; i++) {
        ensureBar(allTasks[ids[i]]);
    }
}

// detect user scrolling away from the right edge
(function() {
    var vp = document.getElementById('tl-viewport');
    if (!vp) return;
    var userScrolling = false;
    vp.addEventListener('wheel', function() { userScrolling = true; });
    vp.addEventListener('touchstart', function() { userScrolling = true; });
    vp.addEventListener('scroll', function() {
        if (!userScrolling) return;
        userScrolling = false;
        // if user scrolled to the right edge, re-enable following
        var maxScroll = Math.max(0, tlEdgePx() - vp.clientWidth);
        tlFollowing = Math.abs(vp.scrollLeft - maxScroll) < 5;
    });
})();

// === Tab renderers ===
function renderArranges() {
    var el = document.getElementById('arrange-total');
    if (el) el.textContent = '(total: ' + arrangeTotal + ')';
    var body = document.getElementById('arrange-body');
    if (!body) return;
    body.replaceChildren();
    for (var i = 0; i < recentArranges.length; i++) {
        var a = recentArranges[i];
        var tr = document.createElement('tr'); tr.className = 'border-b border-cream-100 hover:bg-cream-100';
        var td1 = document.createElement('td'); td1.className = 'py-2 px-3 font-mono';
        var link = document.createElement('a'); link.href = '/partitions/' + a.partition;
        link.className = 'text-teal-600 hover:underline'; link.textContent = 'P' + a.partition;
        td1.appendChild(link);
        var td2 = document.createElement('td'); td2.className = 'py-2 px-3 font-mono'; td2.textContent = a.message_count;
        var td3 = document.createElement('td'); td3.className = 'py-2 px-3 font-mono'; td3.textContent = a.task_count;
        var td4 = document.createElement('td'); td4.className = 'py-2 px-3 font-mono';
        td4.style.color = durColor(a.duration); td4.textContent = a.duration.toFixed(3) + 's';
        var td5 = document.createElement('td'); td5.className = 'py-2 px-3 text-gray-400 text-xs'; td5.textContent = fmtTimeMs(a.ts);
        var td6 = document.createElement('td'); td6.className = 'py-2 px-3 font-mono text-xs text-gray-500 max-w-md truncate expandable cursor-pointer';
        td6.title = 'Click to expand';
        td6.setAttribute('data-full', a.labels.join('\n'));
        td6.textContent = a.labels.slice(0, 3).join(', ') + (a.labels.length > 3 ? ' ... (' + a.labels.length + ')' : '');
        tr.append(td1, td2, td3, td4, td5, td6); body.appendChild(tr);
    }
    if (!recentArranges.length) {
        var etr = document.createElement('tr'); var etd = document.createElement('td');
        etd.colSpan = 6; etd.className = 'py-3 text-center text-gray-400'; etd.textContent = 'No arrange() calls yet';
        etr.appendChild(etd); body.appendChild(etr);
    }
}

function renderCollects() {
    var el = document.getElementById('collect-total');
    if (el) el.textContent = '(total: ' + collectTotal + ')';
    var body = document.getElementById('collect-body');
    if (!body) return;
    body.replaceChildren();
    for (var i = 0; i < recentCollects.length; i++) {
        var c = recentCollects[i];
        var tr = document.createElement('tr'); tr.className = 'border-b border-cream-100 hover:bg-cream-100';
        var td1 = document.createElement('td'); td1.className = 'py-2 px-3 font-mono text-xs';
        var a1 = document.createElement('a'); a1.href = '/task/' + encodeURIComponent(baseTaskId(c.task_id));
        a1.className = 'text-purple-600 hover:underline'; a1.textContent = c.task_id; td1.appendChild(a1);
        var td2 = document.createElement('td'); td2.className = 'py-2 px-3 font-mono'; td2.textContent = 'P' + c.partition;
        var td3 = document.createElement('td'); td3.className = 'py-2 px-3 font-mono';
        td3.style.color = durColor(c.duration); td3.textContent = c.duration.toFixed(3) + 's';
        var td4 = document.createElement('td'); td4.className = 'py-2 px-3 font-mono'; td4.textContent = c.output_count;
        var td5 = document.createElement('td'); td5.className = 'py-2 px-3 text-gray-400 text-xs'; td5.textContent = fmtTimeMs(c.ts);
        tr.append(td1, td2, td3, td4, td5); body.appendChild(tr);
    }
    if (!recentCollects.length) {
        var etr = document.createElement('tr'); var etd = document.createElement('td');
        etd.colSpan = 5; etd.className = 'py-3 text-center text-gray-400'; etd.textContent = 'No collect() calls yet';
        etr.appendChild(etd); body.appendChild(etr);
    }
}

function renderTaskLists() {
    var now = Date.now() / 1000;
    var running = [], finished = [];
    var keys = Object.keys(allTasks);
    for (var i = 0; i < keys.length; i++) {
        var t = allTasks[keys[i]];
        if (t.status === 'running') running.push(t);
        else if (t.status === 'completed' || t.status === 'failed') finished.push(t);
    }
    finished.sort(function(a, b) { return (b.end_ts || 0) - (a.end_ts || 0); });

    var rc = document.getElementById('running-count');
    var rb = document.getElementById('running-body');
    if (rc) rc.textContent = '(' + running.length + ')';
    if (rb) {
        rb.replaceChildren();
        for (var ri = 0; ri < running.length; ri++) {
            var rt = running[ri];
            var rtr = document.createElement('tr'); rtr.className = 'border-b border-cream-100 hover:bg-cream-100';
            var rtd1 = document.createElement('td'); rtd1.className = 'py-2 px-3 font-mono';
            var ra1 = document.createElement('a'); ra1.href = '/task/' + encodeURIComponent(baseTaskId(rt.task_id));
            ra1.className = 'text-amber-600 hover:underline'; ra1.textContent = rt.task_id; rtd1.appendChild(ra1);
            var rtd2 = document.createElement('td'); rtd2.className = 'py-2 px-3 font-mono'; rtd2.textContent = rt.partition;
            var rtd3 = document.createElement('td'); rtd3.className = 'py-2 px-3 font-mono'; rtd3.textContent = (now - rt.start_ts).toFixed(1) + 's';
            var rtd4 = document.createElement('td'); rtd4.className = 'py-2 px-3 font-mono text-xs text-gray-500 max-w-md truncate expandable cursor-pointer';
            rtd4.title = 'Click to expand'; rtd4.textContent = rt.args || '';
            var rtd5 = document.createElement('td'); rtd5.className = 'py-2 px-3 font-mono text-xs text-gray-400'; rtd5.textContent = fmtStdin(rt);
            rtr.append(rtd1, rtd2, rtd3, rtd4, rtd5); rb.appendChild(rtr);
        }
        if (!running.length) {
            var etr = document.createElement('tr'); var etd = document.createElement('td');
            etd.colSpan = 5; etd.className = 'py-3 text-center text-gray-400'; etd.textContent = 'No tasks running';
            etr.appendChild(etd); rb.appendChild(etr);
        }
    }

    var fc = document.getElementById('finished-count');
    var fb = document.getElementById('finished-body');
    if (fc) fc.textContent = '(' + finished.length + ')';
    if (fb) {
        fb.replaceChildren();
        for (var fi = 0; fi < finished.slice(0, MAX_HISTORY).length; fi++) {
            var ft = finished[fi];
            var ftr = document.createElement('tr'); ftr.className = 'border-b border-cream-100 hover:bg-cream-100';
            var ftd1 = document.createElement('td'); ftd1.className = 'py-2 px-3 font-mono text-xs';
            var fa1 = document.createElement('a'); fa1.href = '/task/' + encodeURIComponent(baseTaskId(ft.task_id));
            fa1.className = 'hover:underline'; fa1.style.color = ft.status === 'completed' ? '#059669' : '#dc2626';
            fa1.textContent = ft.task_id; ftd1.appendChild(fa1);
            var ftd2 = document.createElement('td'); ftd2.className = 'py-2 px-3 font-mono'; ftd2.textContent = ft.partition;
            var ftd3 = document.createElement('td'); ftd3.className = 'py-2 px-3';
            ftd3.style.color = ft.status === 'completed' ? '#059669' : '#dc2626'; ftd3.textContent = ft.status;
            var ftd4 = document.createElement('td'); ftd4.className = 'py-2 px-3 font-mono'; ftd4.textContent = ft.duration ? ft.duration.toFixed(2) + 's' : '';
            var ftd5 = document.createElement('td'); ftd5.className = 'py-2 px-3 text-gray-400 text-xs'; ftd5.textContent = ft.end_ts ? fmtTimeMs(ft.end_ts) : '';
            var ftd6 = document.createElement('td'); ftd6.className = 'py-2 px-3 font-mono text-xs text-gray-500 max-w-md truncate expandable cursor-pointer';
            ftd6.title = 'Click to expand'; ftd6.textContent = ft.args || '';
            var ftd7 = document.createElement('td'); ftd7.className = 'py-2 px-3 font-mono text-xs text-gray-400'; ftd7.textContent = fmtStdin(ft);
            ftr.append(ftd1, ftd2, ftd3, ftd4, ftd5, ftd6, ftd7); fb.appendChild(ftr);
        }
        if (!finished.length) {
            var etr2 = document.createElement('tr'); var etd2 = document.createElement('td');
            etd2.colSpan = 7; etd2.className = 'py-3 text-center text-gray-400'; etd2.textContent = 'No finished tasks';
            etr2.appendChild(etd2); fb.appendChild(etr2);
        }
    }
}

// === Data loading ===
async function syncFromAPI() {
    // API sync merges data — it never deletes WS-created tasks.
    // Only adds tasks we don't know about yet (e.g. from before page load).
    // Removes only tasks that aged out of the timeline window.
    try {
        var r = await fetch('/api/recent-tasks?minutes=' + TIMELINE_MINUTES);
        var data = await r.json();
        var tasks = data.tasks || [];
        for (var i = 0; i < tasks.length; i++) {
            var apiTask = tasks[i];
            var existing = allTasks[apiTask.task_id];
            if (!existing) {
                // new task from API (pre-page-load history) — add it
                allTasks[apiTask.task_id] = apiTask;
            } else if (existing.status === 'running' && apiTask.end_ts) {
                // WS had it as running, API knows it finished — update end state
                existing.end_ts = apiTask.end_ts;
                existing.status = apiTask.status;
                existing.duration = apiTask.duration;
                existing.pid = apiTask.pid || existing.pid;
            }
            // otherwise keep the WS-maintained version (it has fresher data)
        }
        // trim tasks that aged out of the visible timeline window
        var cutoff = Date.now() / 1000 - TIMELINE_MINUTES * 60;
        var keys = Object.keys(allTasks);
        for (var k = 0; k < keys.length; k++) {
            var tk = allTasks[keys[k]];
            var endOrStart = tk.end_ts || tk.start_ts;
            if (endOrStart && endOrStart < cutoff) { delete allTasks[keys[k]]; }
        }
    } catch(e) {}
}

async function loadArrangeHistory() {
    try {
        var r = await fetch('/api/events?event_types=arranged&limit=' + MAX_HISTORY);
        var events = await r.json();
        for (var i = 0; i < events.length; i++) {
            var e = events[i];
            var meta = {};
            try { meta = JSON.parse(e.metadata || '{}'); } catch(x) {}
            recentArranges.push({
                partition: e.partition,
                message_count: meta.message_count || (meta.offsets ? meta.offsets.length : 0),
                task_count: meta.task_count || 0, duration: e.duration || 0,
                ts: e.ts, labels: meta.message_labels || [],
            });
        }
        arrangeTotal = recentArranges.length;
    } catch(ex) {}
}

async function loadCollectHistory() {
    try {
        var r = await fetch('/api/events?event_types=collect_completed&limit=' + MAX_HISTORY);
        var events = await r.json();
        for (var i = 0; i < events.length; i++) {
            var e = events[i];
            var outputCount = 0;
            try { outputCount = JSON.parse(e.metadata || '{}').output_message_count || 0; } catch(x) {}
            recentCollects.push({
                task_id: e.task_id, partition: e.partition,
                duration: e.duration || 0, output_count: outputCount, ts: e.ts,
            });
        }
        collectTotal = recentCollects.length;
    } catch(ex) {}
}

// === WebSocket ===
function connectWS() {
    var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
    var ws = new WebSocket(proto + '//' + location.host + '/ws');
    ws.onopen = function() {
        var b = document.getElementById('ws-badge');
        if (b) { b.textContent = 'WS: connected'; b.style.background = '#d1fae5'; b.style.color = '#065f46'; b.style.border = '1px solid #6ee7b7'; }
    };
    ws.onclose = function() {
        var b = document.getElementById('ws-badge');
        if (b) { b.textContent = 'WS: disconnected'; b.style.background = '#fecaca'; b.style.color = '#991b1b'; b.style.border = '1px solid #fca5a5'; }
        setTimeout(connectWS, 3000);
    };
    ws.onmessage = function(msg) {
        if (frozen) return;
        try {
            var e = JSON.parse(msg.data);
            if (e.event === 'task_started') {
                // if this task_id already exists and finished (retry), keep the old bar
                var prev = allTasks[e.task_id];
                if (prev && prev.end_ts) {
                    var retryKey = e.task_id + ':r' + e.ts;
                    allTasks[retryKey] = prev;
                    prev.task_id = retryKey;
                    // move the old bar's DOM entry to the new key
                    if (taskBars[e.task_id]) {
                        taskBars[retryKey] = taskBars[e.task_id];
                        taskBars[retryKey].task = prev;
                        delete taskBars[e.task_id];
                    }
                }
                allTasks[e.task_id] = {
                    task_id: e.task_id, partition: e.partition, pid: e.pid,
                    start_ts: e.ts, end_ts: null, status: 'running', args: e.args, duration: null,
                    slot: e.slot != null ? e.slot : null,
                    stdin_lines: e.stdin_lines || 0, stdin_size: e.stdin_size || 0,
                };
                ensureBar(allTasks[e.task_id]);
            } else if (e.event === 'task_completed' || e.event === 'task_failed') {
                var t = allTasks[e.task_id];
                if (t) {
                    t.end_ts = e.ts; t.status = e.event === 'task_completed' ? 'completed' : 'failed';
                    t.duration = e.duration; t.pid = e.pid || t.pid;
                    ensureBar(t); // update color
                }
            } else if (e.event === 'arranged') {
                arrangeTotal++;
                recentArranges.unshift({
                    partition: e.partition, message_count: e.message_count || 0,
                    task_count: e.task_count || 0, duration: e.duration || 0,
                    ts: e.ts, labels: e.message_labels || [],
                });
                if (recentArranges.length > MAX_HISTORY) recentArranges.length = MAX_HISTORY;
                renderArranges();
            } else if (e.event === 'collect_completed') {
                collectTotal++;
                var outputCount = 0;
                try { outputCount = JSON.parse(e.metadata || '{}').output_message_count || 0; } catch(x) {}
                recentCollects.unshift({
                    task_id: e.task_id, partition: e.partition,
                    duration: e.duration || 0, output_count: outputCount, ts: e.ts,
                });
                if (recentCollects.length > MAX_HISTORY) recentCollects.length = MAX_HISTORY;
                renderCollects();
            }
            if (e.pool_active !== undefined) {
                var pe = document.getElementById('v-pool');
                if (pe) pe.textContent = e.pool_active + ' / ' + MAX_LANES;
                var we = document.getElementById('v-waiting');
                if (we) we.textContent = e.pool_waiting || 0;
            }
        } catch(err) {}
    };
}

// === Boot ===
function initFromHash() {
    var hash = location.hash.slice(1);
    switchTab(hash || 'execute');
}

(async function() {
    ensureAnchor();
    await Promise.all([syncFromAPI(), loadArrangeHistory(), loadCollectHistory()]);
    initFromHash();
    initBars();
    rebuildAxis();
    renderArranges();
    renderCollects();
    connectWS();
    // rAF loop: updates bar positions/widths + scroll at 60fps
    requestAnimationFrame(animateTimeline);
    // task list tables (running/finished) — update every 500ms
    setInterval(function() { if (!frozen) renderTaskLists(); }, 500);
    // prune old bars + API sync + axis ticks every 5s
    setInterval(function() {
        if (frozen) return;
        rebuildAxis();
        syncFromAPI().then(function() {
            var ids = Object.keys(allTasks);
            for (var i = 0; i < ids.length; i++) {
                if (!taskBars[ids[i]]) ensureBar(allTasks[ids[i]]);
            }
            pruneOldBars();
        });
    }, 5000);
})();

window.addEventListener('hashchange', initFromHash);

document.addEventListener('keydown', function(ev) {
    if (ev.key === ' ' && ev.target === document.body) {
        ev.preventDefault();
        toggleFreeze();
    }
});
</script>
{% endblock %}
