Back to feed

sledtools/pika branch #28

pika-git-2

Fix branch review CI summary refresh after reruns

Target branch: master

Merge Commit: 07092b7753383f61bc7e1ed3fe12fd48d14efd44

branch: merged tutorial: ready ci: success
Open CI Details

Continuous Integration

CI: success

Compact status on the review page, with full logs on the CI page.

Open CI Details

Latest run #34 success

2 passed

head fb181927f31f8d57d49be0af08faadb85a52bbdc · queued 2026-03-24 13:46:32 · 2 lane(s)

queued 7s · ran 23s

check-notifications · success check-agent-contracts · success

Summary

This branch fixes a stale CI summary problem on branch review pages. When a user triggers a lane rerun or recovery action from the branch CI page, the detail page (which may be open in another tab) had no way to learn about the change and continued showing outdated summary information. The fix introduces a cross-tab signaling mechanism using localStorage: the branch CI page writes a signal on every rerun/recovery action, and the detail page listens for storage events to force-restart its Server-Sent Events (SSE) stream, pulling fresh CI summary data. The SSE stream management in the detail page is also hardened with deduplication guards and proper cleanup of the EventSource reference.

Tutorial Steps

Define a shared localStorage key and announce signals from branch_ci.html

Intent: Establish a cross-tab communication channel so that when a user triggers a CI rerun or recovery action on the branch CI page, a signal is written to localStorage that other tabs can observe.

Affected files: crates/pika-news/templates/branch_ci.html

Evidence
@@ -236,6 +236,7 @@
         const reviewNextBtn = document.getElementById('review-next');
         const reviewDismissBtn = document.getElementById('review-dismiss');
         const reviewPosEl = document.getElementById('review-pos');
+        const branchCiSignalKey = 'pika_news_branch_ci_signal';
@@ -250,6 +251,15 @@
+        function announceBranchCiSignal() {
+          try {
+            localStorage.setItem(branchCiSignalKey, JSON.stringify({
+              branchId,
+              at: Date.now(),
+            }));
+          } catch (_err) {}
+        }
@@ -279,6 +289,7 @@
             button.textContent = 'Lane rerun queued.';
+            announceBranchCiSignal();
@@ -317,6 +328,7 @@
                 button.textContent = button.dataset.doneLabel || 'Done.';
+                announceBranchCiSignal();

A new constant branchCiSignalKey ('pika_news_branch_ci_signal') is declared at the top of the script block in branch_ci.html (crates/pika-news/templates/branch_ci.html:239).

A helper function announceBranchCiSignal() writes a JSON payload containing the current branchId and a timestamp into localStorage under that key. The timestamp ensures every write is a new value, which is required for the storage event to fire in other tabs.

This function is called in two places:

  1. After a lane rerun is queued — inside the rerun button click handler, immediately before startBranchCiStream(true) is called.
  2. After a recovery/action button succeeds — inside the generic action button handler, again right before the SSE stream is restarted.

Both calls are wrapped after the success path so the signal is only emitted when the server confirms the action succeeded.

Harden the SSE stream lifecycle in detail.html

Intent: Prevent duplicate EventSource connections and ensure the stream reference is properly tracked and cleaned up, which is a prerequisite for the force-restart logic that the cross-tab signal handler will use.

Affected files: crates/pika-news/templates/detail.html

Evidence
@@ -470,6 +470,8 @@
+        const branchCiSignalKey = 'pika_news_branch_ci_signal';
+        let branchCiSource = null;
@@ -524,24 +526,37 @@
-        function startBranchCiStream() {
-          if (!branchCiSummary || !branchCiSummaryEnabled || !window.EventSource) return;
-          const source = new EventSource(`/news/branch/${branchId}/ci/stream${reviewMode ? '?review=true' : ''}`);
+        function startBranchCiStream(force) {
+          if (!branchCiSummary || !window.EventSource) return;
+          if (!force && !branchCiSummaryEnabled) return;
+          if (branchCiSource && branchCiSource.readyState !== EventSource.CLOSED) return;
+          branchCiSource = new EventSource(`/news/branch/${branchId}/ci/stream${reviewMode ? '?review=true' : ''}`);
@@ -646,8 +661,9 @@
-        startBranchCiStream();
+        startBranchCiStream(false);

The startBranchCiStream function in detail.html is refactored with three key changes:

  1. force parameter — A new boolean parameter lets callers bypass the branchCiSummaryEnabled gate. The initial call at page load passes false, preserving the original behavior where the stream only starts if the server-side flag is set. Cross-tab signal handlers will pass true to force a reconnection.

  2. Deduplication guard — Before creating a new EventSource, the function checks branchCiSource.readyState !== EventSource.CLOSED. This prevents stacking multiple SSE connections if startBranchCiStream is called while a stream is already active.

  3. Module-level branchCiSource variable — The EventSource instance is now stored in a let binding at module scope (crates/pika-news/templates/detail.html:471) instead of a local const. Both the ci-update handler and the error handler now set branchCiSource = null after calling .close(), making the state accurately queryable for the deduplication guard.

Listen for cross-tab localStorage signals in detail.html

Intent: Allow the detail page to detect when a sibling tab (branch_ci.html) has triggered a CI rerun, and automatically restart the SSE stream to fetch updated CI summary data.

Affected files: crates/pika-news/templates/detail.html

Evidence
@@ -524,24 +526,37 @@
+        function handleBranchCiSignal(event) {
+          if (!event || event.key !== branchCiSignalKey || !event.newValue) return;
+          try {
+            const payload = JSON.parse(event.newValue);
+            if (payload.branchId !== branchId) return;
+            startBranchCiStream(true);
+          } catch (_err) {}
+        }
@@ -646,8 +661,9 @@
+        window.addEventListener('storage', handleBranchCiSignal);

A new handleBranchCiSignal function is registered as a listener for the storage event on window (crates/pika-news/templates/detail.html:664). The browser fires storage events only in other tabs sharing the same origin, which is exactly the desired cross-tab behavior.

The handler performs three checks before acting:

  1. The event key must match branchCiSignalKey.
  2. event.newValue must be truthy (filters out removeItem calls).
  3. The branchId inside the JSON payload must match the current page's branchId, so a rerun on one branch doesn't trigger a refresh on an unrelated branch's detail page.

If all checks pass, it calls startBranchCiStream(true), forcing a new SSE connection even if branchCiSummaryEnabled was originally false. The deduplication guard inside startBranchCiStream ensures this is safe to call repeatedly — if a stream is already open, the call is a no-op.

Diff