// Detail-page client behaviors. Loaded in layout.templ with `defer` so the // DOM is parsed before any listeners fire. Three jobs: // // 1. Auto-advance: when a substep-* SSE event lands with state=running, // open the parent step panel and collapse any previously-running step // that's now completed. Keeps the operator's attention on the thing // that's currently moving without manual clicks. // 2. In-step search: filter `.log-line` rows inside the current step by // substring match. Client-side only — the log pane's `
` ancestor // scopes the filter naturally. // 3. Permalink scroll + highlight: when the URL carries `#L{run}-{stage}-{ord}` // on load, scroll that log line into view; anchor clicks update // `location.hash` without a reload. (function () { 'use strict'; // --- 1. auto-advance on substep SSE --------------------------------- document.body.addEventListener('htmx:sseMessage', function (ev) { var name = ev.detail && ev.detail.type; if (!name || name.indexOf('substep-') !== 0) { return; } // After htmx has applied the swap, check which step the just-updated // substep belongs to. We scan *after* the swap so we see the new // class ("substep-running" / "substep-passed") rather than the old. setTimeout(function () { autoAdvance(); }, 0); }); function autoAdvance() { var steps = document.querySelectorAll('.step[data-stage]'); var runningStep = null; steps.forEach(function (step) { if (step.querySelector('.substep-running')) { runningStep = step; } }); if (!runningStep) { return; } // Open the running step; collapse any other open step that no longer // has a running substep. The default-open step picked server-side // stays open if nothing is running yet. steps.forEach(function (step) { if (step === runningStep) { if (!step.open) { step.open = true; } return; } if (step.open && !step.querySelector('.substep-running')) { // Leave the "currently-failed" step open even when we // auto-advance — operator still wants to see what broke. if (step.classList.contains('step-failed')) { return; } step.open = false; } }); } // --- 2. in-step search ---------------------------------------------- document.body.addEventListener('input', function (ev) { var el = ev.target; if (!el.classList || !el.classList.contains('log-search')) { return; } var step = el.closest('.step'); if (!step) { return; } var query = el.value.trim().toLowerCase(); step.querySelectorAll('.log-line').forEach(function (line) { if (!query) { line.style.display = ''; line.classList.remove('log-hit'); return; } var text = (line.textContent || '').toLowerCase(); if (text.indexOf(query) === -1) { line.style.display = 'none'; line.classList.remove('log-hit'); } else { line.style.display = ''; line.classList.add('log-hit'); } }); }); // --- 3. permalink scroll + highlight on load ------------------------ function scrollToHash() { var hash = (location.hash || '').replace(/^#/, ''); if (!hash) { return; } var target = document.getElementById(hash); if (!target) { return; } // Open the enclosing step so the target is actually visible. var step = target.closest('.step'); if (step && !step.open) { step.open = true; } target.scrollIntoView({ block: 'center' }); } window.addEventListener('load', scrollToHash); window.addEventListener('hashchange', scrollToHash); // Anchor clicks update location.hash without triggering navigation; // the hashchange listener above handles the scroll + highlight. document.body.addEventListener('click', function (ev) { var a = ev.target.closest && ev.target.closest('.log-anchor'); if (!a) { return; } ev.preventDefault(); var href = a.getAttribute('href') || ''; if (href.indexOf('#') === 0) { history.replaceState(null, '', href); scrollToHash(); } }); })();