Skip to content

Phase 1 PR3: RegionalIntelligenceBoard panel UI#2963

Merged
koala73 merged 2 commits into
mainfrom
feat/regional-intelligence-board
Apr 11, 2026
Merged

Phase 1 PR3: RegionalIntelligenceBoard panel UI#2963
koala73 merged 2 commits into
mainfrom
feat/regional-intelligence-board

Conversation

@koala73

@koala73 koala73 commented Apr 11, 2026

Copy link
Copy Markdown
Owner

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`

  • `src/components/RegionalIntelligenceBoard.ts` — Panel wrapper (region dropdown → RPC call → renderBoard)
  • `src/components/regional-intelligence-board-utils.ts` — pure HTML builders, no Vite-only deps

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)

  1. Narrative sections — situation, balance_assessment, outlook 24h/7d/30d. Each section hidden when empty; evidence IDs shown as pills.
  2. Regime block — current label, previous label, transition driver.
  3. Balance Vector — 4 pressure axes + 3 buffer axes with horizontal bars, plus a centered `net_balance` bar.
  4. Actors — top 5 by `leverage_score` with role, domains, colored delta.
  5. Scenarios — 3 horizon columns (24h/7d/30d) × 4 lanes (base/escalation/containment/fragmentation), sorted by probability within each horizon.
  6. Transmission Paths — top 5 by confidence with mechanism, corridor, severity, latency.
  7. Watchlist — active triggers + narrative `watch_items`.
  8. 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 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

  • `src/config/panels.ts` — `regional-intelligence` added 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 DeductionPanel, 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 (intentionally premium-gated + async-mounted, matches DeductionPanel)

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

  • What to monitor/search
    • Logs: Vercel edge function logs for `/api/intelligence/v1/get-regional-snapshot`. Should be low-volume (premium users only, one call per region change).
    • Client: Sentry breadcrumbs for `[RegionalIntelligenceBoard] load failed` — should be rare or absent.
  • Validation checks
    • After merge, as a Pro user: open CMD+K → search "regional intelligence" → panel loads. Change region dropdown. Each region returns either populated snapshot (once PR2 + one seed cycle land) or the graceful empty state.
    • Non-pro user: panel shows the locked/upgrade overlay.
  • Expected healthy behavior
    • Region switch latency <300ms on warm cache
    • Empty state shown for regions with no persisted snapshot yet (no JS errors)
    • All 6 blocks render for any region with a populated snapshot
  • Failure signal(s) / rollback trigger
    • Panel stuck at "Loading regional snapshot…" for >10s → network/RPC issue, investigate gateway
    • Persistent console errors from `buildBoardHtml` → shape mismatch, check proto codegen freshness
  • Validation window & owner

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

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.
@vercel

vercel Bot commented Apr 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
worldmonitor Ready Ready Preview, Comment Apr 11, 2026 8:10pm

Request Review

@greptile-apps

greptile-apps Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces RegionalIntelligenceBoard, a new premium panel that renders a RegionalSnapshot across six structured blocks (regime, balance vector, actors, scenarios, transmission paths, watchlist) plus LLM narrative sections, wired into the async-mount pattern used by DeductionPanel. The HTML builder/panel split, XSS escaping, and empty-state handling are clean, and the 38-test suite gives solid coverage of the pure builders.

Two P1 defects need to be resolved before merge:

  • Premium gate bypassed: regional-intelligence is absent from WEB_PREMIUM_PANELS in panel-layout.ts, so updatePanelGating always calls unlockPanel() for every user; non-entitled users receive the live API call and see the premium data.
  • Stale content on rapid region switch: when the user changes region while a request is in-flight, the loading guard returns early without scheduling a follow-up load, leaving the dropdown and rendered content out of sync.

Confidence Score: 3/5

Not 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)

Security Review

  • Premium gate bypass (src/app/panel-layout.ts): regional-intelligence is missing from WEB_PREMIUM_PANELS. updatePanelGating determines gating purely from this set; absent from it, the panel is unconditionally unlocked and the backend API is called for all users, including anonymous and free-tier. All other premium panels with the same entitlement pattern (deduction, market-implications, etc.) are present in the set.
  • No injection vulnerabilities found: all user-supplied fields in the HTML builders pass through escapeHtml; colors and bar widths are derived from numeric comparisons or hardcoded CSS variable strings, not from server data.

Important Files Changed

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
Loading

Comments Outside Diff (1)

  1. src/app/panel-layout.ts, line 113-121 (link)

    P1 security regional-intelligence missing from WEB_PREMIUM_PANELS

    updatePanelGating determines gating by checking WEB_PREMIUM_PANELS.has(key). Because regional-intelligence is absent from this set, isPremium is always false, getPanelGateReason returns PanelGateReason.NONE unconditionally, and unlockPanel() is called for every user — including anonymous and free-tier users. The panel's constructor also fires loadCurrent() 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

Comment on lines +82 to +101
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;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Misleading comment

The comment says regional-intelligence is "gated via WEB_PREMIUM_PANELS", but the panel is not currently in that set (see the P1 comment on panel-layout.ts). Once the fix lands, the comment will be accurate; without the fix it is actively misleading.

Comment on lines +76 to +80
public async loadRegion(regionId: string): Promise<void> {
this.currentRegion = regionId;
this.selector.value = regionId;
await this.loadCurrent();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant