Skip to content

Commit 97eee98

Browse files
committed
Refining the refresh feature of the CLI's Realtime log
1 parent 023ab1a commit 97eee98

1 file changed

Lines changed: 73 additions & 16 deletions

File tree

th_cli/test_run/log_viewer.html

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -451,18 +451,18 @@
451451
<div id="refreshBanner" class="refresh-banner hidden">
452452
<span class="banner-icon">&#8505;</span>
453453
<span class="banner-msg">
454-
New execution detected: <strong id="newRunTitle"></strong>
454+
New execution started: <strong id="newRunTitle"></strong>
455455
</span>
456-
<button class="btn-reload" onclick="window.location.reload()">Reload page</button>
457-
<button class="btn-dismiss" onclick="dismissBanner()">Dismiss</button>
456+
<button class="btn-reload" onclick="switchToNewExecution()">Switch to new execution</button>
457+
<button class="btn-dismiss" onclick="dismissBanner()">Stay here</button>
458458
</div>
459459

460460
<!-- ── Main area ───────────────────────────────────────────────────── -->
461461
<div class="main-content">
462462

463463
<!-- Tree sidebar -->
464464
<div class="tree-panel">
465-
<div class="tree-panel-header">&#128462; Execution Tree</div>
465+
<div class="tree-panel-header">&#9776; Execution Tree</div>
466466
<div class="tree-empty" id="treeEmpty">
467467
<div>
468468
<div style="font-size:28px;margin-bottom:8px;">&#9203;</div>
@@ -529,17 +529,51 @@
529529
let wasDisconnected = false;
530530
let currentRunTitle = '{test_run_title}'; // initial title injected by server
531531

532+
// When true, all incoming SSE events from a new execution are ignored so the
533+
// current page content (old execution) stays untouched until the user chooses.
534+
let newExecutionPending = false;
535+
536+
// Interval handle for /api/status polling after a clean stream end.
537+
let statusPollInterval = null;
538+
532539
// Maps stepId → log-entry index at the moment the step became 'executing'.
533-
// Used by scrollToStep() to jump without server-side stream markers.
534540
const stepLogIndex = {{}};
535541

536542
// Maps step title → ordered list of stepIds built from the tree snapshot.
537-
// Used to resolve "Executing Test Step: <title>" log entries to their DOM anchor.
538543
let stepTitleToIds = {{}};
539544

540545
const BATCH_INTERVAL_MS = 50;
541546
const MAX_BATCH_SIZE = 50;
542547

548+
// ── /api/status polling ──────────────────────────────────────────
549+
// Used after a clean stream end so we never auto-connect a new SSE session
550+
// (which would overwrite the page before the user has seen the banner).
551+
function startStatusPolling() {{
552+
if (statusPollInterval) return;
553+
const indicator = document.getElementById('statusIndicator');
554+
const statusTxt = document.getElementById('statusText');
555+
statusTxt.textContent = 'Stream Ended — watching for next execution…';
556+
statusPollInterval = setInterval(() => {{
557+
fetch('/api/status')
558+
.then(r => r.json())
559+
.then(s => {{
560+
if (s.run_title && s.run_title !== currentRunTitle) {{
561+
stopStatusPolling();
562+
newExecutionPending = true;
563+
showRefreshBanner(s.run_title);
564+
}}
565+
}})
566+
.catch(() => {{}}); // server not up yet — keep polling
567+
}}, 3000);
568+
}}
569+
570+
function stopStatusPolling() {{
571+
if (statusPollInterval) {{
572+
clearInterval(statusPollInterval);
573+
statusPollInterval = null;
574+
}}
575+
}}
576+
543577
// ── SSE connection ───────────────────────────────────────────────
544578
function connectToLogStream() {{
545579
const indicator = document.getElementById('statusIndicator');
@@ -552,33 +586,40 @@
552586
statusTxt.textContent = 'Connected';
553587

554588
if (wasDisconnected) {{
555-
// Check whether a new execution has started.
589+
// Preemptively block incoming events until the status check resolves.
590+
// This prevents tree_init / log events from rewriting the page in the
591+
// microseconds before the async fetch completes.
592+
newExecutionPending = true;
556593
fetch('/api/status')
557594
.then(r => r.json())
558595
.then(s => {{
559596
if (s.run_title && s.run_title !== currentRunTitle) {{
560597
showRefreshBanner(s.run_title);
598+
// newExecutionPending stays true — banner is the gate
599+
}} else {{
600+
// Same execution reconnected (e.g. network hiccup) — resume normally
601+
newExecutionPending = false;
561602
}}
562603
}})
563-
.catch(() => {{}});
604+
.catch(() => {{ newExecutionPending = false; }});
564605
}}
565606
wasDisconnected = false;
566607
}});
567608

568609
eventSource.addEventListener('log', function(e) {{
610+
if (newExecutionPending) return;
569611
logBatchQueue.push(JSON.parse(e.data));
570612
scheduleBatchProcessing();
571613
}});
572614

573615
eventSource.addEventListener('tree_init', function(e) {{
616+
if (newExecutionPending) return;
574617
renderTree(JSON.parse(e.data));
575618
}});
576619

577620
eventSource.addEventListener('tree_update', function(e) {{
621+
if (newExecutionPending) return;
578622
const update = JSON.parse(e.data);
579-
// Record the current log-entry count when a step starts executing.
580-
// scrollToStep() uses this index to find the approximate position in the
581-
// DOM without needing server-injected markers in the log stream.
582623
if (update.node_type === 'step' && update.state === 'executing') {{
583624
const stepId = `step-${{update.suite_idx}}-${{update.case_idx}}-${{update.step_idx}}`;
584625
stepLogIndex[stepId] = logCount;
@@ -595,13 +636,18 @@
595636
}});
596637

597638
eventSource.addEventListener('end', function(e) {{
639+
// Execution finished cleanly. Close the SSE connection and switch to
640+
// polling so we never auto-flow new-execution data into the current page.
598641
indicator.className = 'status-indicator disconnected';
599-
statusTxt.textContent = 'Stream Ended';
600642
eventSource.close();
601643
processBatchImmediately();
644+
wasDisconnected = true;
645+
startStatusPolling();
602646
}});
603647

604648
eventSource.onerror = function(e) {{
649+
// Unexpected connection drop (e.g. network hiccup mid-execution).
650+
// Reconnect normally; the connected handler will verify the title.
605651
indicator.className = 'status-indicator disconnected';
606652
statusTxt.textContent = 'Disconnected';
607653
wasDisconnected = true;
@@ -622,8 +668,21 @@
622668
document.getElementById('refreshBanner').classList.remove('hidden');
623669
}}
624670

671+
function switchToNewExecution() {{
672+
window.location.reload();
673+
}}
674+
625675
function dismissBanner() {{
676+
// User wants to stay on the current execution's data.
677+
// Stop polling and close any open SSE connection so nothing overwrites the page.
678+
stopStatusPolling();
679+
newExecutionPending = false;
680+
if (eventSource && eventSource.readyState !== EventSource.CLOSED) {{
681+
eventSource.close();
682+
}}
626683
document.getElementById('refreshBanner').classList.add('hidden');
684+
document.getElementById('statusIndicator').className = 'status-indicator disconnected';
685+
document.getElementById('statusText').textContent = 'Stream Ended';
627686
}}
628687

629688
// ── Tree rendering ───────────────────────────────────────────────
@@ -728,8 +787,8 @@
728787
729788
// Toggle arrow (for collapsible nodes)
730789
const toggle = document.createElement('span');
731-
toggle.className = 'tree-toggle';
732-
toggle.textContent = hasChildren ? '▶' : ''; // ▶
790+
toggle.className = hasChildren ? 'tree-toggle expanded' : 'tree-toggle';
791+
toggle.textContent = hasChildren ? '▶' : '';
733792
if (!hasChildren) toggle.style.visibility = 'hidden';
734793
735794
// State icon
@@ -800,8 +859,6 @@
800859
// Highlight executing node, remove from others at the same level
801860
if (state === 'executing') {{
802861
el.classList.add('executing');
803-
// Scroll the tree so this node is visible
804-
el.scrollIntoView({{ block: 'nearest', behavior: 'smooth' }});
805862
}} else {{
806863
el.classList.remove('executing');
807864
}}

0 commit comments

Comments
 (0)