Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions engine/web/src/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ export function App({ store, liveRegion }) {
const state = store.getState();
return (
<div id="dash-app" class="dash-app" data-tab={state.activeTab}>
{/* 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…"). */}
<StatusStrip strip={state.data?.strip} />
{/* 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…"). */}
<StatusStrip strip={state.strip} />
<ConnectionBanner status={state.status} lastGoodAt={state.lastGoodAt} />
<TabNav activeTab={state.activeTab} store={store} />
<ActiveView store={store} state={state} />
Expand Down
14 changes: 11 additions & 3 deletions engine/web/src/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,17 @@ export class Store {
/** @type {Set<() => void>} */
this.listeners = new Set();
/** @type {{ activeTab: string, expandedRows: Set<string>, openPrompts: Set<string>,
* 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,
};
Expand Down Expand Up @@ -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();
}

Expand Down Expand Up @@ -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();
}
Expand Down
25 changes: 25 additions & 0 deletions engine/web/test/store.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
});
Loading