The Panel class is the foundational abstract class for all 86+ UI components. It's a class-based component system (not React/Preact based, despite the Preact build) that manages its own DOM lifecycle.
Constructor & Key Properties:
interface PanelOptions {
id: string; // Unique identifier (e.g., 'economic', 'news')
title: string; // Display title
showCount?: boolean; // Show item count badge
className?: string; // CSS classes
trackActivity?: boolean; // Track panel activity/novelty
infoTooltip?: string; // Methodology tooltip (HTML)
premium?: 'locked' | 'enhanced'; // Paywall gate
closable?: boolean; // User can close panel
collapsible?: boolean; // User can minimize panel
defaultRowSpan?: number; // Grid height (1-4 rows)
}Key Properties & State:
element: Root HTMLElementcontent: HTMLElement container for panel bodyheader: Panel header with title, badges, controlscountEl: Optional count badge (e.g., "News: 24")statusBadgeEl: Data freshness badge ("live", "cached", "unavailable")_fetching: Boolean flag tracking load state_locked: Boolean for paywall lock state_collapsed: Boolean for minimized state
Core Lifecycle Methods:
showLoading()— Renders spinner, called by panel subclasses during initial fetchsetContent(html: string, debounceMs?: number)— Debounced DOM update (150ms default); subclasses call this to render resultssetDataBadge(state, detail)— Sets "live · 2s ago", "cached", or "unavailable" badgesetSeverity(level)— Sets colored severity dot: 'critical', 'high', 'medium', 'low', 'none'showGatedCta(reason)— Renders "Sign In to Unlock" / "Upgrade to Pro" overlayunlockPanel()— Removes gate overlay, restores original contentshowRetry(callback, attemptNum)— Retry button with countdown timer (backoff)destroy()— Cleanup: abort pending fetches, remove DOM, clear listeners
Sizing & Persistence:
- Panels persist user-resized dimensions to localStorage (
worldmonitor-panel-spans,worldmonitor-panel-col-spans) - Supports 1-4 row spans and 1-3 column spans
- Resize handles use mouse/touch with debounced frame updates
- Double-click resize handle resets to default size
- Closed/minimized state stored in
worldmonitor-panel-collapsed
Premium & Gating:
- Checks
PanelGateReasonat render time (web-specificWEB_PREMIUM_PANELS, Clerk-onlyWEB_CLERK_PRO_ONLY_PANELS) - Saves content snapshot (
_savedContent) when showing lock, restores on unlock (preserves subclass DOM references)
Common Subclass Pattern:
export class StatusPanel extends Panel {
constructor() {
super({ id: 'status', title: 'Status', showCount: true });
this.init();
}
private init(): void {
// Set up initial state, event listeners
}
async load(): Promise<void> {
this.showLoading();
try {
const data = await fetchStatus();
this.renderStatus(data);
this.setDataBadge('live', '5s ago');
} catch (e) {
this.showRetry(() => this.load(), 1);
}
}
private renderStatus(data: StatusData): void {
const html = `<div>${data.items.map(item =>
`<div>${escapeHtml(item.name)}</div>`
).join('')}</div>`;
this.setContent(html);
}
}The data-loader is the orchestration layer that coordinates async data fetching for all panels. It's a state machine that manages panel callbacks, retry logic, circuit breakers, and hierarchical refresh scheduling.
Design Principles:
- No blocking: All fetches are non-blocking async; UI remains interactive
- Coalescing: Multiple concurrent requests to same endpoint use a single network call
- Circuit breaking: Failed data sources get cooldown periods (5–30 min depending on tier)
- Fallback cascades: When a primary feed fails, fall back to per-feed backups, then cached digests
- Debouncing: Heavy operations like news clustering debounced to prevent thrashing
Data Flow:
App.ts
↓
DataLoaderManager.init()
├─ Registers panel load callbacks
├─ Sets up refresh scheduler (60s interval default)
└─ Subscribes to framework changes (market, brief)
↓
loadAllData() / loadPanel(key)
├─ Calls panel.load() or enqueuePanelCall()
├─ Each panel fetches via services
├─ Services use cachedFetchJson() + Redis memoization
└─ Panel.setContent() + setDataBadge()
Key Methods:
-
loadAllData(): Promise<void>— Master orchestrator; loads all visible panels in dependency order- News digest (base feed resolution)
- Markets (stocks, commodities, crypto)
- Intelligence signals (conflicts, protests, military)
- Specialized panels (economic, climate, etc.)
- Runs ~60s on startup, repeats on interval
-
loadPanel(key: string): Promise<void>— Load a single panel by ID- Used for on-demand opens, targeted refresh
- Example:
loadPanel('economic')→ callspanels['economic'].load()
-
tryFetchDigest()— Central news aggregator- Fetches unified feed digest from
/api/news/v1/list-feed-digest - Circuit breaker with 5m cooldown on failure
- Cascades to cached digest if network fails
- Result memoized for news panels to avoid duplicate fetches
- Fetches unified feed digest from
-
loadNews()— Renders news into NewsPanel- Clusters headlines with ML worker (
clusterNewsHybrid()) - Enriches with threat classification, geolocation
- Dedupes against OREF breaking alerts
- Supports time-range filtering (1h, 24h, 7d)
- Clusters headlines with ML worker (
-
loadMarkets()— Stocks, commodities, crypto- Fetches from user's watchlist
- Calls
fetchMultipleStocks(),fetchCommodityQuotes(), etc. - Each has own circuit breaker
-
loadIntelligence()— Military, conflicts, protests- Aggregates conflicts (UCDP, ACLED), protests, military flights/vessels
- Runs geo-convergence analysis (detects clusters near critical infrastructure)
- Updates CII (Country Instability Index) scores
- Calls
ingestProtests(),ingestFlights(), etc. to update country-instability service
-
Panel Call Enqueueing — Asynchronous panel mounting
- If panel not yet instantiated, calls are queued in
pending-panel-data - When panel mounts, queued calls replay automatically
- Example: NewsPanel.load() queued before panel constructor runs
- If panel not yet instantiated, calls are queued in
Circuit Breaker Pattern:
private digestBreaker = {
state: 'closed' | 'open' | 'half-open',
failures: 0,
cooldownUntil: 0
};
// On failure count ≥ 2: state='open', cooldown 5min
// During cooldown: return cached result
// After cooldown: state='half-open', try once
// On success: state='closed', reset failuresCaching Tiers:
- Fast cache (5m): Real-time data (markets, outages, flights)
- Medium cache (10m): Feed aggregation, analysis
- Slow cache (30m): Economic indicators, climate anomalies
- Static cache (2h): Geometry, ports, bases
- Daily cache (24h): IMF WEO, JODI, historical data
The layout system manages panel instantiation, grid rendering, user interactions (drag, resize, close), and dynamic access control based on entitlements.
Panel Registry:
- 86+ panels defined in
src/config/panels.ts - Each variant (full, tech, finance, etc.) has a subset in
VARIANT_DEFAULTS - Free tier limited to
FREE_MAX_PANELS=12,FREE_MAX_SOURCES=3
Panel Gating (Paywall):
const WEB_PREMIUM_PANELS = new Set([
'stock-analysis',
'stock-backtest',
'daily-market-brief',
'market-implications',
'deduction',
'chat-analyst',
'wsb-ticker-scanner',
'latest-brief',
'regional-intelligence',
'trade-policy',
]);
const WEB_CLERK_PRO_ONLY_PANELS = new Set([
'latest-brief', // Must have Clerk auth (not just API key)
]);Layout Rendering:
-
renderLayout(): Promise<void>— Master init- Instantiates all visible panels for current variant
- Injects into grid DOM
- Applies saved user preferences (spans, collapsed, custom order)
- Mounts critical warning banner (e.g., OREF alerts)
-
Grid Structure:
<div class="panels-grid" style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px;"> <div class="panel span-2 col-span-2"> <div class="panel-header"> <div class="panel-header-left"> <span class="panel-title">News</span> <span class="panel-severity-dot"></span> </div> <span class="panel-data-badge">live · 3s ago</span> <span class="panel-count">24</span> <button class="panel-close-btn">×</button> </div> <div class="panel-content" id="newsContent"><!-- Rendered by panel --></div> <div class="panel-resize-handle"></div> <div class="panel-col-resize-handle"></div> </div> </div>
-
Panel Lifecycle in Layout:
mountPanel(panelId)→ new Panel() → injects into grid- User opens Country Brief →
openCountryBrief(code)→ modal overlay (CountryDeepDivePanel) - User closes panel →
closePanel(panelId)→ removes from DOM, cleans up listeners - Panel unmounts on variant switch → destroyAllPanels()
State Management & Context:
AppContext (src/app/app-context.ts) is the global state tree:
export interface AppContext {
map: MapContainer | null; // Deck.gl map instance
readonly isMobile: boolean;
readonly isDesktopApp: boolean; // Tauri desktop or browser
readonly container: HTMLElement; // Root mount point
panels: Record<string, Panel>; // panel[id] = instance
newsPanels: Record<string, NewsPanel>; // Convenience subset
panelSettings: Record<string, PanelConfig>; // Saved prefs (size, order, hidden)
mapLayers: MapLayers; // CII choropleth, conflict zones, etc.
allNews: NewsItem[]; // Aggregated headlines
newsByCategory: Record<string, NewsItem[]>; // Partitioned by feed
latestMarkets: MarketData[]; // Stock/commodity quotes
latestPredictions: PredictionMarket[]; // Polymarket data
latestClusters: ClusteredEvent[]; // Geo-clustered events (conflicts, protests)
intelligenceCache: IntelligenceCache; // Flights, vessels, earthquakes, etc.
disabledSources: Set<string>; // User-disabled feeds
currentTimeRange: TimeRange; // 1h | 24h | 7d
}Event Delegation:
Panels communicate via custom events:
// Panel A: Emit
window.dispatchEvent(new CustomEvent('wm-market-watchlist-changed', {
detail: { symbol: 'NVDA', action: 'add' }
}));
// DataLoaderManager: Listen
this.boundMarketWatchlistHandler = () => {
void this.loadMarkets().then(async () => {
if (hasPremiumAccess()) {
await this.loadStockAnalysis();
}
});
};
window.addEventListener('wm-market-watchlist-changed', this.boundMarketWatchlistHandler);User Interaction Handlers:
-
Drag/Reorder (not yet fully implemented for panels):
- Grid uses CSS Grid layout; manual drag would require custom JS
- Currently panels have fixed order per variant
-
Resize (vertical & horizontal):
- Mouse/touch handlers on resize handles
- Converts pixel deltas to grid span deltas (80px per span)
- Persists to localStorage on mouseUp
-
Close:
- Remove from DOM
- Emit
wm-panel-closedevent (optional) - Update panelSettings
-
Collapse:
- Toggle
_collapsedflag - Hide content, show only header
- Persist to localStorage
- Toggle
-
Search/Filter:
- SearchManager opens modal
- User types query
- Dispatches
wm-search-inputevent - Panels that support search receive callbacks (e.g., NewsPanel filters by keyword)
Core geo-lookup service using ISO 3166-1 alpha-2 codes (2-letter: US, CN, RU, etc.).
Data Structure:
interface IndexedCountryGeometry {
code: string; // ISO-2: "US", "GB", "JP"
name: string; // Full name: "United States"
bbox: [number, number, number, number]; // [minLon, minLat, maxLon, maxLat]
polygons: [number, number][][][]; // GeoJSON coordinates
}Loaded from:
- Primary:
public/data/countries.geojson(shipped with build) - Overrides:
https://maps.worldmonitor.app/country-boundary-overrides.geojson(Natural Earth, 3s timeout) - Political override:
POLITICAL_OVERRIDES['CN-TW'] = 'TW'(Taiwan as separate)
Key Functions:
-
preloadCountryGeometry(): Promise<void>— Load and index all countries- Called at App startup
- Normalizes codes (extracts
ISO3166-1-Alpha-2property from GeoJSON features) - Builds
iso3ToIso2map: ISO-3 → ISO-2 (e.g., "USA" → "US") - Builds
nameToIso2map: Country name → ISO-2 (with aliases: "DR Congo" → "CD") - Indexes by bbox for point-in-polygon queries
-
getCountryAtCoordinates(lon: number, lat: number): {code, name} | null- Ray-casting point-in-polygon test
- Used for reverse geocoding in panels (e.g., click on map → open country brief)
-
iso3ToIso2Code(iso3: string): string | null— Normalize "USA" → "US" -
nameToCountryCode(name: string): string | null— Lookup "Russia" → "RU" -
getCountryNameByCode(code: string): string | null— Lookup "JP" → "Japan" -
matchCountryNamesInText(text: string): string[]— Extract country codes from article text
Political Boundary Handling:
const POLITICAL_OVERRIDES: Record<string, string> = {
'CN-TW': 'TW', // Taiwan treated as separate country
};
function normalizeCode(properties): string | null {
const rawCode = properties['ISO3166-1-Alpha-2'] ?? properties.ISO_A2;
const trimmed = rawCode.trim().toUpperCase();
return POLITICAL_OVERRIDES[trimmed] ?? trimmed;
}Name Aliases (Common Misspellings):
const NAME_ALIASES: Record<string, string> = {
'dr congo': 'CD',
'czech republic': 'CZ',
'ivory coast': 'CI',
'uae': 'AE',
'uk': 'GB',
'usa': 'US',
'south korea': 'KR',
'north korea': 'KP',
};Real-time risk scoring system keyed by ISO-2 country code.
CountryScore Type:
export interface CountryScore {
code: string; // ISO-2: "RU", "UA", "IL"
name: string;
score: number; // 0–100 (0=peaceful, 100=critical)
level: 'low' | 'normal' | 'elevated' | 'high' | 'critical';
trend: 'rising' | 'stable' | 'falling';
change24h: number; // Score delta in last 24h
components: ComponentScores;
lastUpdated: Date | null;
}
export interface ComponentScores {
unrest: number; // Protests, strikes (0–25)
conflict: number; // Armed conflict, UCDP events (0–25)
security: number; // Cyber, advisories, sanctions (0–25)
information: number; // Internet outages, GPS jamming (0–25)
}Data Ingest Pipeline:
The ingestXxx() functions consume raw events and update per-country totals:
// Military flights in country
ingestFlights(militaryFlights: MilitaryFlight[])
// Count by country, weighted by operator/type
// Conflict events (UCDP, ACLED)
ingestConflicts(events: ConflictEvent[])
// Tally deaths, casualties, severity
// Protests & strikes
ingestProtests(events: SocialUnrestEvent[])
// Count by country
// Internet outages
ingestOutagesForCII(outages: InternetOutage[])
// Mark outages per country
// GPS jamming, cyber threats, advisories
ingestGpsJammingForCII()
ingestCyberThreatsForCII()
ingestAdvisoriesForCII()Scoring Model:
score = baselineRisk[country]
+ (conflicts.length * eventMultiplier[country])
+ (protests.length * 2)
+ (militaryFlights.length * 0.5)
+ (outages.length * 3)
+ ...other factorsCurated Country Config:
src/config/countries.ts defines per-country baseline risk and event weighting:
export const CURATED_COUNTRIES: Record<string, CuratedCountryConfig> = {
RU: {
name: 'Russia',
scoringKeywords: ['russia', 'moscow', 'kremlin', 'putin'],
baselineRisk: 35, // Always 35 before events
eventMultiplier: 2.0, // Each event worth 2× weight
},
CN: {
name: 'China',
baselineRisk: 25,
eventMultiplier: 2.5,
},
UA: {
name: 'Ukraine',
baselineRisk: 50, // Sustained conflict → high baseline
eventMultiplier: 0.8, // Less sensitive to daily events
},
// ... 20+ more
};Hotspot & Conflict Zone Configs:
src/config/geo.ts defines critical regions and their focal points:
export const INTEL_HOTSPOTS = [
{ code: 'UA', region: 'donbas', lat: 48.0, lon: 38.0, radiusKm: 150 },
{ code: 'IL', region: 'gaza', lat: 31.9, lon: 34.5, radiusKm: 50 },
{ code: 'IR', region: 'strait-hormuz', lat: 26.5, lon: 56.5, radiusKm: 100 },
// ...
];
export const STRATEGIC_WATERWAYS = [
{ name: 'Strait of Hormuz', lat: 26.5, lon: 56.5, code: 'IR/OM' },
{ name: 'Taiwan Strait', lat: 24.5, lon: 119.5, code: 'TW/CN' },
// ...
];CountryBriefPanel opens a detail modal with multi-tab data:
export interface CountryIntelData {
brief: string; // LLM-generated intel summary
country: string; // Full name: "Ukraine"
code: string; // ISO-2: "UA"
cached?: boolean;
generatedAt?: string;
error?: string;
reason?: string; // Why unavailable (e.g., "gated content")
}
export interface CountryDeepDiveSignalItem {
type: 'MILITARY' | 'PROTEST' | 'CYBER' | 'DISASTER' | 'OUTAGE' | 'OTHER';
severity: 'critical' | 'high' | 'medium' | 'low' | 'info';
description: string;
timestamp: Date;
}
export interface CountryEnergyProfileData {
// Energy mix: coal%, gas%, oil%, nuclear%, renewables%
coalShare: number;
gasShare: number;
// ... 30+ fields for electricity, storage, imports/exports
}Economic Indicators Service:
src/services/imf-country-data.ts fetches IMF WEO data keyed by ISO-2:
export interface ImfCountryBundle {
macro: ImfMacroEntry | null; // Inflation, debt, reserves
growth: ImfGrowthEntry | null; // GDP growth, investment
labor: ImfLaborEntry | null; // Unemployment, population
external: ImfExternalEntry | null; // Trade, current account
fetchedAt: number;
}
// Data indexed by ISO-2 code in Redis:
// imfMacro:{code} → { inflationPct, govRevenuePct, ... }A complex panel that aggregates military flights, vessels, and bases to assess escalation risk.
StrategicPosturePanel fetches from 4+ services, each with independent circuit breakers:
export class StrategicPosturePanel extends Panel {
async load(): Promise<void> {
this.showLoading();
try {
// Parallel fetch: all 4 services at once
const [flights, vessels, bases, posture] = await Promise.all([
this.fetchFlights(),
this.fetchVessels(),
this.fetchBases(),
this.fetchPosture(),
]);
this.renderTheaterView(flights, vessels, bases, posture);
this.setDataBadge('live', '12s ago');
} catch (e) {
this.showRetry(() => this.load(), 1);
}
}
private async fetchFlights(): Promise<MilitaryFlight[]> {
// Calls service which uses circuit breaker
return fetchMilitaryFlights({
signal: this.abortController.signal
});
}
private async fetchVessels(): Promise<MilitaryVessel[]> {
return fetchMilitaryVessels({
signal: this.abortController.signal
});
}
private async fetchBases(): Promise<MilitaryBase[]> {
// Cached for 24h: geo never changes
return MILITARY_BASES_EXPANDED;
}
private async fetchPosture(): Promise<TheaterPostureSummary[]> {
// Aggregates flights/vessels by theater, compares to baseline
return fetchCachedTheaterPosture({
signal: this.abortController.signal
});
}
private renderTheaterView(
flights: MilitaryFlight[],
vessels: MilitaryVessel[],
bases: MilitaryBase[],
posture: TheaterPostureSummary[]
): void {
const html = posture.map(theater => `
<div class="theater">
<h3>${escapeHtml(theater.name)}</h3>
<div class="stats">
<span>Flights: ${theater.flightCount}</span>
<span class="trend ${theater.trend}">${theater.trend}</span>
</div>
${flights.filter(f => isInTheater(f, theater)).map(f => `
<div class="flight">
<span>${escapeHtml(f.operator)}</span>
<span>${f.type}</span>
</div>
`).join('')}
</div>
`).join('');
this.setContent(html);
}
}fetchMilitaryFlights (src/services/military-flights.ts):
const MILITARY_FLIGHTS_CACHE_MS = 5 * 60 * 1000; // 5 min
export async function fetchMilitaryFlights(opts?: FetchOpts): Promise<MilitaryFlight[]> {
const rpcUrl = toApiUrl('/api/intelligence/v1/list-military-flights');
// Circuit breaker + deduping
return cachedFetchJson<ListMilitaryFlightsResponse>(
rpcUrl,
{ signal: opts?.signal },
MILITARY_FLIGHTS_CACHE_MS, // TTL
(data) => data.flights ?? [] // Result selector
);
}cachedFetchJson (src/services/cached-*.ts):
export async function cachedFetchJson<T>(
url: string,
init: RequestInit,
ttlMs: number,
selector: (data: unknown) => unknown
): Promise<unknown> {
// 1. Check Redis cache key: md5(url)
const cacheKey = `fetch:${md5(url)}`;
const cached = await redis.get(cacheKey);
if (cached && !isStale(cached.storedAt, ttlMs)) {
console.log('[Cache HIT]', url);
return selector(cached.data);
}
// 2. Coalesce: if request already in-flight, wait for it
if (inFlightRequests.has(url)) {
return inFlightRequests.get(url)!;
}
// 3. New request
const promise = (async () => {
try {
const resp = await fetch(url, init);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.json();
// 4. Store in Redis
await redis.set(cacheKey, { data, storedAt: Date.now() }, { ex: Math.ceil(ttlMs / 1000) });
return selector(data);
} finally {
inFlightRequests.delete(url);
}
})();
inFlightRequests.set(url, promise);
return promise;
}If fetchMilitaryFlights() fails 2×, cooldown 30m:
export class DataLoaderManager {
private militaryFlightsBreaker = {
state: 'closed',
failures: 0,
cooldownUntil: 0
};
private async loadIntelligence(): Promise<void> {
// Check breaker state
const now = Date.now();
if (this.militaryFlightsBreaker.state === 'open') {
if (now < this.militaryFlightsBreaker.cooldownUntil) {
console.warn('[Circuit Open] Military flights cooling down');
return; // Skip fetch, leave stale data on screen
}
this.militaryFlightsBreaker.state = 'half-open';
}
try {
const flights = await fetchMilitaryFlights();
// Update country-instability
ingestFlights(flights);
this.militaryFlightsBreaker = { state: 'closed', failures: 0, cooldownUntil: 0 };
} catch (e) {
this.militaryFlightsBreaker.failures++;
if (this.militaryFlightsBreaker.failures >= 2) {
this.militaryFlightsBreaker.state = 'open';
this.militaryFlightsBreaker.cooldownUntil = now + 30 * 60 * 1000;
}
console.error('[Fetch Failed] Military flights:', e);
}
}
}military-surge.ts combines flights + vessels + bases to detect escalations:
export interface TheaterPostureSummary {
id: string; // 'middle-east'
name: string; // 'Middle East / Persian Gulf'
baselineFlights: number; // Historical avg
currentFlights: number;
surgeFactor: number; // currentFlights / baselineFlights
flightTrend: 'rising' | 'stable' | 'falling';
nearbyBases: string[];
timeSeriesDataPoints: TheaterActivity[]; // Last 7 days
}
export async function analyzeFlightsForSurge(
flights: MilitaryFlight[]
): Promise<{ surgeAlerts: SurgeAlert[]; theaters: Map<string, TheaterActivity> }> {
const byTheater = new Map<string, TheaterActivity>();
// Cluster flights by theater (geographic region)
for (const flight of flights) {
const theater = findTheaterForFlight(flight);
if (!theater) continue;
const activity = byTheater.get(theater.id) ?? {
theaterId: theater.id,
timestamp: Date.now(),
transportCount: 0,
fighterCount: 0,
reconCount: 0,
totalMilitary: 0,
flightIds: [],
};
if (flight.type === 'transport') activity.transportCount++;
else if (flight.type === 'fighter') activity.fighterCount++;
else if (flight.type === 'reconnaissance') activity.reconCount++;
activity.totalMilitary++;
activity.flightIds.push(flight.id);
byTheater.set(theater.id, activity);
}
// Detect surges vs. historical baseline
const surgeAlerts: SurgeAlert[] = [];
for (const [theaterId, activity] of byTheater) {
const baseline = calculateBaselineForTheater(theaterId);
if (activity.totalMilitary > baseline * 1.5) {
surgeAlerts.push({
id: `surge-${theaterId}-${Date.now()}`,
theater: THEATERS.find(t => t.id === theaterId)!,
type: activity.fighterCount > activity.transportCount ? 'fighter' : 'airlift',
currentCount: activity.totalMilitary,
baselineCount: baseline,
surgeMultiple: activity.totalMilitary / baseline,
aircraftTypes: new Map(),
nearbyBases: findNearbyBases(activity),
firstDetected: new Date(),
lastUpdated: new Date(),
});
}
}
return { surgeAlerts, theaters: byTheater };
}
function findTheaterForFlight(flight: MilitaryFlight): MilitaryTheater | null {
// Geo-spatial search: which theater contains this lat/lon?
for (const theater of THEATERS) {
const dist = haversineDistanceKm(flight.lat, flight.lon, theater.centerLat, theater.centerLon);
if (dist < 1000) return theater; // Rough radius, actual impl more sophisticated
}
return null;
}| Component | Type | Key Files | Responsibility |
|---|---|---|---|
| Panel | Base Class | src/components/Panel.ts |
DOM lifecycle, resize, paywall gates |
| Panel Subclass | Subclass | src/components/EconomicPanel.ts |
Fetch data, render UI, respond to user input |
| DataLoaderManager | Orchestrator | src/app/data-loader.ts |
Schedule panel loads, circuit breakers, cascading fallbacks |
| RefreshScheduler | Scheduler | src/app/refresh-scheduler.ts |
60s interval loop, refresh all panels |
| PanelLayoutManager | Layout Engine | src/app/panel-layout.ts |
Grid layout, panel gating, user interactions |
| Service | Data Layer | src/services/market/, src/services/military-, etc. |
Fetch from RPC, Redis cache, circuit break |
| AppContext | State Container | src/app/app-context.ts |
Global singleton: panels, news, markets, cache |
| CountryGeometry | Geo Library | src/services/country-geometry.ts |
ISO-2 lookup, point-in-polygon, normalize names |
| CountryInstability | Scoring | src/services/country-instability.ts |
Real-time CII scores, ingest multi-source events |
| API Layer | Edge Functions | api/*.js, api/*/index.js |
Vercel serverless, no imports from src/ |
Every async panel operation stores an AbortController:
private abortController: AbortController = new AbortController();
async load() {
const resp = await fetch(url, { signal: this.abortController.signal });
}
destroy() {
this.abortController.abort(); // Cancel all pending fetches on unmount
}Panels batch rapid updates to avoid thrashing:
setContent(html: string, debounceMs = 150) {
this.pendingContentHtml = html;
clearTimeout(this.contentDebounceTimer);
this.contentDebounceTimer = setTimeout(() => {
replaceChildren(this.content, safeHtml(this.pendingContentHtml));
}, debounceMs);
}Multiple concurrent requests to the same URL merge into one network call:
const inFlightRequests = new Map<string, Promise<unknown>>();
if (inFlightRequests.has(url)) {
return inFlightRequests.get(url)!; // Reuse pending request
}Before a panel instance exists, data-loader queues method calls:
enqueuePanelCall('news', 'load', []);
// Later, when NewsPanel mounts:
replayPendingCalls('news'); // Plays back load()Complex panels orchestrate multiple independent services in parallel:
const [flights, vessels, bases, posture] = await Promise.all([
fetchMilitaryFlights(),
fetchMilitaryVessels(),
getBases(), // Static
fetchCachedTheaterPosture(),
]);User Action (load page / click country)
↓
App.ts: initAppContext() → DataLoaderManager.init()
↓
PanelLayoutManager.renderLayout() → mount 12+ panels
↓
DataLoaderManager.loadAllData()
├─ Panel A: load() → fetchService1() → cachedFetchJson() → Redis hit/miss → setContent()
├─ Panel B: load() → fetchService2,3,4() → all in parallel → aggregate → setContent()
├─ Panel C: load() → fetchService5() → circuit breaker open → use cached result
└─ ...
↓
AppContext.panels[id] / allNews / latestMarkets / intelligenceCache updated
↓
RefreshScheduler: every 60s → loadAllData() again
↓
User drag/resize panel → localStorage persist → next load restores
User click map → countryGeometry.getCountryAtCoordinates() → open CountryBriefPanel
User search → SearchManager → broadcast to panels → NewsPanel.filterByKeyword()
This architecture supports:
- 86+ concurrently-loadable panels with independent lifecycle
- 30+ external data sources aggregated via services
- Real-time geo-risk scoring (CII) across 195 countries
- Circuit breaker cascade preventing cascade failures
- Browser-local & Redis caching tiers
- Fully responsive UI with user-persisted preferences