diff --git a/engine/web/src/app.jsx b/engine/web/src/app.jsx
index d5ea4fe..46cd27a 100644
--- a/engine/web/src/app.jsx
+++ b/engine/web/src/app.jsx
@@ -52,10 +52,12 @@ export function App({ store, liveRegion }) {
const state = store.getState();
return (
- {/* The persistent status strip — the honesty spine on every view. Before the first snapshot
- lands there is no strip data, so nothing renders here (blank is honest — absent is never a
- green all-clear; the ConnectionBanner already says "connecting…"). */}
-
+ {/* The persistent status strip — the honesty spine on every view. Read from the store's
+ decoupled `strip` (global posture), NOT the per-tab `data`, so a tab-swap never tears it
+ down; it stays mounted and reconciles in place as posture changes. Before the first
+ snapshot there is no strip, so nothing renders (blank is honest — absent is never a green
+ all-clear; the ConnectionBanner already says "connecting…"). */}
+
diff --git a/engine/web/src/store.js b/engine/web/src/store.js
index 4a7c92a..643073d 100644
--- a/engine/web/src/store.js
+++ b/engine/web/src/store.js
@@ -67,13 +67,17 @@ export class Store {
/** @type {Set<() => void>} */
this.listeners = new Set();
/** @type {{ activeTab: string, expandedRows: Set
, openPrompts: Set,
- * data: unknown, status: Status, lastGoodAt: number | null }} */
+ * data: unknown, strip: unknown, status: Status, lastGoodAt: number | null }} */
this.state = {
activeTab: seed.activeTab || "findings",
expandedRows: loadSet(EXPANDED_KEY),
openPrompts: loadSet(PROMPT_KEY),
disclosures: loadSet(DISCLOSURE_KEY),
data: null,
+ // The persistent status strip (global cluster posture), decoupled from the per-tab `data`
+ // so a tab-swap (which nulls `data`) never tears the header down. Updated on every snapshot;
+ // held across swaps. Null before the first snapshot (blank is honest — absent is never green).
+ strip: null,
status: "first-load",
lastGoodAt: null,
};
@@ -104,7 +108,10 @@ export class Store {
* @param {unknown} data
*/
applySnapshot(data) {
- this.state = { ...this.state, data, status: "live", lastGoodAt: Date.now() };
+ // Refresh the persistent strip from this snapshot's global posture (present in every tab's
+ // payload). Keep the last strip if a snapshot somehow omits it, so the header never blanks.
+ const strip = data && data.strip ? data.strip : this.state.strip;
+ this.state = { ...this.state, data, strip, status: "live", lastGoodAt: Date.now() };
this.emit();
}
@@ -215,7 +222,8 @@ export class Store {
// Drop the previous tab's snapshot: each tab has its own JSON shape, so rendering the old
// snapshot under the new view would be wrong. The next poll (repointed to the new tab) lands its
// snapshot; until then the view shows its quiet first-paint body, and the status remains LIVE
- // (this is a local view change, not a connection problem).
+ // (this is a local view change, not a connection problem). `strip` is deliberately NOT cleared —
+ // it is global posture that must persist across the swap so the header never tears down.
this.state = { ...this.state, activeTab: tab, data: null };
this.emit();
}
diff --git a/engine/web/test/store.test.js b/engine/web/test/store.test.js
index 9fb6ac7..89a0b18 100644
--- a/engine/web/test/store.test.js
+++ b/engine/web/test/store.test.js
@@ -97,3 +97,28 @@ describe("subscribe", () => {
expect(n).toBe(1);
});
});
+
+describe("persistent status strip (JEF-410)", () => {
+ it("updates the strip from each snapshot and holds it across a tab swap", () => {
+ const store = new Store();
+ expect(store.getState().strip).toBe(null); // no snapshot yet — blank is honest
+
+ const strip = { "all-clear": false, "judging-state": "watching" };
+ store.applySnapshot({ strip, rows: [] });
+ expect(store.getState().strip).toBe(strip);
+ expect(store.getState().data).toEqual({ strip, rows: [] });
+
+ // A tab swap drops the per-tab body but MUST NOT tear down the global strip.
+ store.setActiveTab("alerts");
+ expect(store.getState().data).toBe(null); // body cleared for the new tab
+ expect(store.getState().strip).toBe(strip); // header persists — no blank/redraw
+ });
+
+ it("keeps the last strip if a later snapshot omits it", () => {
+ const store = new Store();
+ const strip = { "all-clear": true, "judging-state": "all-clear" };
+ store.applySnapshot({ strip, rows: [] });
+ store.applySnapshot({ rows: [] }); // malformed / strip-less snapshot
+ expect(store.getState().strip).toBe(strip); // never regress the header to blank
+ });
+});