mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
Phase 1 PR3: RegionalIntelligenceBoard panel UI (#2963)
* feat(intelligence): RegionalIntelligenceBoard panel UI (Phase 1 PR3)
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.
* fix(intelligence): request-sequence cancellation in RegionalIntelligenceBoard (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
This commit is contained in:
@@ -40,6 +40,7 @@ describe('panel-config guardrails', () => {
|
||||
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
|
||||
/this\.ctx\.panels\['runtime-config'\]/, // desktop-only, intentionally ungated
|
||||
/this\.ctx\.panels\['live-news'\]/, // mountLiveNewsIfReady — has its own channel guard
|
||||
/panel as unknown as/, // lazyPanel generic cast
|
||||
|
||||
Reference in New Issue
Block a user