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:
- After a lane rerun is queued — inside the rerun button click handler, immediately before
startBranchCiStream(true) is called.
- 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:
-
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.
-
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.
-
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:
- The event key must match
branchCiSignalKey.
event.newValue must be truthy (filters out removeItem calls).
- 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.