Phase 1 PR3: RegionalIntelligenceBoard panel UI#2963
Conversation
Phase 1 PR3 of the Regional Intelligence Model. New premium panel that
renders a canonical RegionalSnapshot with 6 structured blocks plus the
LLM narrative sections from Phase 1 PR2.
## What landed
### New panel: RegionalIntelligenceBoard
src/components/RegionalIntelligenceBoard.ts (Panel wrapper)
src/components/regional-intelligence-board-utils.ts (pure HTML builders)
Region dropdown (7 non-global regions) → on change calls
IntelligenceServiceClient.getRegionalSnapshot → renders buildBoardHtml().
Layout (top to bottom):
- Narrative sections (situation, balance_assessment, outlook 24h/7d/30d)
— each section hidden when its text is empty, evidence IDs shown as pills
- Regime block — current label, previous label, transition driver
- Balance Vector — 4 pressure axes + 3 buffer axes with horizontal bars,
plus a centered net_balance bar
- Actors — top 5 by leverage_score with role, domains, and colored delta
- Scenarios — 3 horizon columns (24h/7d/30d) × 4 lanes (base/escalation/
containment/fragmentation), sorted by probability within each horizon
- Transmission Paths — top 5 by confidence with mechanism, corridor,
severity, and latency
- Watchlist — active triggers + narrative watch_items
- Meta footer — generated timestamp, confidence, scoring/geo versions,
narrative provider/model
### Pure builders split for test isolation
All HTML builders live in regional-intelligence-board-utils.ts. The Panel
class in RegionalIntelligenceBoard.ts is a thin wrapper that calls them
and inserts the result via setContent. This split matches the existing
resilience-widget-utils pattern and lets node:test runners import the
builders directly without pulling in Vite-only services like
@/services/i18n (which fails with `import.meta.glob is not a function`).
### PRO gating + registration
- src/config/panels.ts: added 'regional-intelligence' to FULL_PANELS with
premium: 'locked', enabled: false, plus isPanelEntitled API-key list
- src/app/panel-layout.ts: async dynamic import mounts the panel after
the DeductionPanel block, reusing the same async-mount + gating pattern
- src/config/commands.ts: CMD+K entry with 🌍 icon and keywords
- tests/panel-config-guardrails.test.mjs: regional-intelligence added to
the allowedContexts allowlist for the ungated direct-assignment check
(the panel is intentionally premium-gated via WEB_PREMIUM_PANELS and
async-mounted, matching DeductionPanel)
## Tests — 38 new unit tests
tests/regional-intelligence-board.test.mts exercises the pure builders:
- BOARD_REGIONS (2): 7 non-global regions, correct IDs
- buildRegimeBlock (4): current label rendered, "Was:" shown on change,
hidden when unchanged, unknown fallback
- buildBalanceBlock (4): all 7 axes rendered, net_balance shown,
unavailable fallback, value clamping for >1 inputs
- buildActorsBlock (4): top-5 cap, sort by leverage_score, delta colors,
empty state
- buildScenariosBlock (4): horizon order (24h/7d/30d), percentage
rendering, in-horizon probability sort, empty state
- buildTransmissionBlock (4): content rendering, confidence sort,
top-5 cap, empty state
- buildWatchlistBlock (5): triggers + watch items, triggers-only,
watch-items-only, empty-text filtering, all-empty state
- buildNarrativeHtml (5): all sections, empty-section hiding, all-empty
returns '', undefined returns '', evidence ID pills
- buildMetaFooter (3): content, "no narrative" when provider empty,
missing-meta returns ''
- buildBoardHtml (3): all 6 block titles + footer, HTML escaping,
mostly-empty snapshot renders without throwing
## Verification
- npm run test:data: 4390/4390 pass
- npm run typecheck: clean
- npm run typecheck:api: clean
- biome lint on touched files: clean (pre-existing panel-layout.ts
complexity warning unchanged)
## Dependency on PR2
This PR renders whatever narrative the snapshot carries. Phase 1 PR2
(#2960) populates the narrative; without PR2 merged the narrative
section just stays hidden (empty sections are filtered out in
buildNarrativeHtml). The UI ships safely in either order.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR introduces Two P1 defects need to be resolved before merge:
Confidence Score: 3/5Not safe to merge — premium gating is bypassed for all users, and stale content can be rendered after a region switch. Two P1 defects: the absent WEB_PREMIUM_PANELS entry is an auth/security-boundary mistake that exposes premium API data to unauthenticated users, and the loading-guard race leaves the panel in an inconsistent state after rapid region changes. Both are straightforward fixes but block merge. src/app/panel-layout.ts (WEB_PREMIUM_PANELS set) and src/components/RegionalIntelligenceBoard.ts (loadCurrent stale-state logic)
|
| Filename | Overview |
|---|---|
| src/app/panel-layout.ts | regional-intelligence absent from WEB_PREMIUM_PANELS, causing updatePanelGating to always unlock the panel for all users. |
| src/components/RegionalIntelligenceBoard.ts | Panel wrapper — two bugs: stale content after rapid region switching, and loadCurrent() starts before premium gating is applied. |
| src/components/regional-intelligence-board-utils.ts | Pure HTML builders; thorough escapeHtml coverage, clamping, and empty-state guards look correct. |
| src/config/panels.ts | Correctly adds regional-intelligence to FULL_PANELS with premium: 'locked' and isPanelEntitled API-key list. |
| src/config/commands.ts | CMD+K entry added with globe icon and sensible keywords; no issues. |
| tests/regional-intelligence-board.test.mts | 38 targeted unit tests covering all builders, XSS escaping, empty states, and clamping — good coverage. |
| tests/panel-config-guardrails.test.mjs | Allow-list comment for regional-intelligence says "gated via WEB_PREMIUM_PANELS" but the panel is not in that set; will be accurate once the P1 is fixed. |
Sequence Diagram
sequenceDiagram
participant U as User
participant RIB as RegionalIntelligenceBoard
participant PL as PanelLayoutManager
participant API as IntelligenceServiceClient
participant Gate as updatePanelGating
PL->>RIB: constructor
RIB->>API: getRegionalSnapshot(mena)
PL->>Gate: updatePanelGating(authState)
Note over Gate: regional-intelligence not in WEB_PREMIUM_PANELS
Gate->>RIB: unlockPanel() for all users
API-->>RIB: snapshot response
RIB->>RIB: renderBoard(snapshot)
U->>RIB: region change while loading
RIB->>RIB: currentRegion = newRegion
RIB->>RIB: loadCurrent() exits early
Note over RIB: old region data rendered, newRegion never fetched
Comments Outside Diff (1)
-
src/app/panel-layout.ts, line 113-121 (link)regional-intelligencemissing fromWEB_PREMIUM_PANELSupdatePanelGatingdetermines gating by checkingWEB_PREMIUM_PANELS.has(key). Becauseregional-intelligenceis absent from this set,isPremiumis alwaysfalse,getPanelGateReasonreturnsPanelGateReason.NONEunconditionally, andunlockPanel()is called for every user — including anonymous and free-tier users. The panel's constructor also firesloadCurrent()immediately, so non-entitled users receive a live API call and see the premium snapshot data.Compare with
deduction(directly above), which is in the set and is correctly gated. The guardrails test even comments "gated via WEB_PREMIUM_PANELS" on the allow-list entry, confirming the intent.
Reviews (1): Last reviewed commit: "feat(intelligence): RegionalIntelligence..." | Re-trigger Greptile
| private async loadCurrent(): Promise<void> { | ||
| if (this.loading) return; | ||
| this.loading = true; | ||
| this.renderLoading(); | ||
|
|
||
| try { | ||
| const resp = await client.getRegionalSnapshot({ regionId: this.currentRegion }); | ||
| const snapshot = resp.snapshot; | ||
| if (!snapshot?.regionId) { | ||
| this.renderEmpty(); | ||
| return; | ||
| } | ||
| this.renderBoard(snapshot); | ||
| } catch (err) { | ||
| console.error('[RegionalIntelligenceBoard] load failed', err); | ||
| this.renderError(err instanceof Error ? err.message : String(err)); | ||
| } finally { | ||
| this.loading = false; | ||
| } | ||
| } |
There was a problem hiding this comment.
Stale content after region change during in-flight load
If the user switches region while a request is in-flight, loadCurrent() exits at the if (this.loading) return guard — updating this.currentRegion but not scheduling a follow-up load. When the original request settles it renders the old region's data while the dropdown already shows the new region. this.loading resets to false but nothing re-triggers the load, leaving UI and selector permanently out of sync.
Fix: capture the requested region at call time and, in the finally block, kick off a fresh load if this.currentRegion diverged.
private async loadCurrent(): Promise<void> {
if (this.loading) return;
this.loading = true;
this.renderLoading();
const requestedRegion = this.currentRegion;
try {
const resp = await client.getRegionalSnapshot({ regionId: requestedRegion });
const snapshot = resp.snapshot;
if (!snapshot?.regionId) { this.renderEmpty(); return; }
this.renderBoard(snapshot);
} catch (err) {
console.error('[RegionalIntelligenceBoard] load failed', err);
this.renderError(err instanceof Error ? err.message : String(err));
} finally {
this.loading = false;
if (this.currentRegion !== requestedRegion) void this.loadCurrent();
}
}
| const allowedContexts = [ | ||
| /this\.ctx\.panels\[key\]\s*=/, // createPanel helper | ||
| /this\.ctx\.panels\['deduction'\]/, // async-mounted PRO panel — gated via WEB_PREMIUM_PANELS | ||
| /this\.ctx\.panels\['regional-intelligence'\]/, // async-mounted PRO panel — gated via WEB_PREMIUM_PANELS |
There was a problem hiding this comment.
| public async loadRegion(regionId: string): Promise<void> { | ||
| this.currentRegion = regionId; | ||
| this.selector.value = regionId; | ||
| await this.loadCurrent(); | ||
| } |
There was a problem hiding this comment.
loadRegion silently no-ops when a load is in progress
loadRegion calls loadCurrent() which returns immediately if this.loading === true. The caller — including agent tools and tests — receives a resolved Promise<void> with no indication that the load was skipped and regionId was not actually fetched. Consider documenting this limitation or resolving it via the same stale-state fix above.
…nceBoard (PR #2963 review) P2 review finding on PR #2963. loadCurrent() used a naive `loading` boolean that DROPPED any region change while a fetch was in flight, so a fast dropdown switch would leave the panel rendering the previous region indefinitely until the user changed it a third time. Fix: replaced the boolean with a monotonic `latestSequence` counter. Each load claims a sequence before awaiting the RPC and only renders its response when mySequence still matches latestSequence on return. Earlier in-flight responses are silently discarded. Latest selection always wins. ## Pure arbitrator helper Added isLatestSequence(mySequence, latestSequence) to regional-intelligence-board-utils.ts. The helper is trivially pure, but exporting it makes the arbitration semantics testable without instantiating the Panel class (which can't be imported by node:test due to import.meta.glob in @/services/i18n — see the feedback_panel_utils_split_for_node_test memory). ## Tests — 7 new regression tests isLatestSequence (3): - matching sequences return true - newer sequences return false - defensive: mine > latest also returns false loadCurrent race simulation (4): each test mimics the real loadCurrent() sequence-claim-and-arbitrate flow with controllable deferred promises so the resolution order can be exercised directly: - earlier load resolves AFTER the later one → discarded - earlier load resolves BEFORE the later one → still discarded - three rapid switches, scrambled resolution order → only last renders - single load (no race) still renders normally ## Verification - npx tsx --test tests/regional-intelligence-board.test.mts: 45/45 pass - npm run test:data: 4393/4393 pass - npm run typecheck: clean - biome lint on touched files: clean
Summary
Phase 1 PR3 of the Regional Intelligence Model. New premium panel that renders a canonical `RegionalSnapshot` with 6 structured blocks plus the LLM narrative sections from Phase 1 PR2.
What landed
New panel: `RegionalIntelligenceBoard`
Region dropdown exposes 7 non-global regions (mena, east-asia, europe, north-america, south-asia, latam, sub-saharan-africa). On change, calls `IntelligenceServiceClient.getRegionalSnapshot` once and renders `buildBoardHtml()` into the panel body.
Layout (top to bottom)
Pure builders split for test isolation
All HTML builders live in `regional-intelligence-board-utils.ts`. The Panel class is a thin wrapper that calls them. This split matches the existing `resilience-widget-utils` pattern and lets `node:test` runners import the builders directly without pulling in Vite-only services like `@/services/i18n` (which fails with `import.meta.glob is not a function` under `node --test`).
PRO gating + registration
Testing
38 new unit tests in `tests/regional-intelligence-board.test.mts`:
`BOARD_REGIONS` (2) — 7 non-global regions, correct IDs
`buildRegimeBlock` (4) — current label rendered, "Was:" shown on change, hidden when unchanged, unknown fallback
`buildBalanceBlock` (4) — all 7 axes rendered, net_balance shown, unavailable fallback, value clamping for >1 inputs
`buildActorsBlock` (4) — top-5 cap, sort by leverage_score, delta colors, empty state
`buildScenariosBlock` (4) — horizon order, percentage rendering, in-horizon sort, empty state
`buildTransmissionBlock` (4) — content, confidence sort, top-5 cap, empty state
`buildWatchlistBlock` (5) — triggers + watch items, triggers-only, watch-items-only, empty-text filtering, all-empty state
`buildNarrativeHtml` (5) — all sections, empty-section hiding, all-empty returns '', undefined returns '', evidence ID pills
`buildMetaFooter` (3) — content, "no narrative" when provider empty, missing-meta returns ''
`buildBoardHtml` (3) — all 6 block titles + footer, HTML escaping (XSS), mostly-empty snapshot renders without throwing
`npm run test:data`: 4390/4390 pass
`npm run typecheck` + `typecheck:api`: clean
`biome lint` on touched files: clean (pre-existing `panel-layout.ts` complexity warning unchanged)
Dependency on PR2
This PR renders whatever narrative the snapshot carries. Phase 1 PR2 (#2960) populates the narrative; without PR2 merged the narrative sections just stay hidden (empty sections are filtered out in `buildNarrativeHtml`). The UI ships safely in either order, but PR2 should merge first for the narrative to actually show up.
Post-Deploy Monitoring & Validation
Before / After Screenshots
No existing panel to compare against — this is a brand-new UI. Screenshots would require a running dev server with a seeded snapshot, which needs PR2 merged first for meaningful narrative content. I'll capture screenshots once PR2 lands and the seed bundle has run one cycle.
PR sequence