refactor(world): remove ~1.6k lines of dead code + fix 5 invalid alert backgrounds

Grep-verified dead (zero instantiations, zero non-barrel imports) — deleted end to end:
- 11 orphaned panel components never built in createPanels/ensureVariantPanels:
  Robotics/Quantum/PostQuantum/Weather/Sports/SpaceWeather (the "domain-lens" set),
  plus Regulation (superseded by the NewsPanel 'regulation' feed), GeoHubs, TechHubs,
  VerificationChecklist, and CountryIntelModal (superseded by CountryBriefPage).
- 5 dead backing services (robotics/quantum/post-quantum/space-weather/sports) +
  config/post-quantum.ts (only importer was the dead service).
- The 6 dead domain-lens panel keys in FULL_PANELS — they rendered Panels-menu toggles
  and Add-widget entries that did NOTHING (applyPanelSettings/getElement hit undefined),
  and their now-orphaned `.domain-*` CSS block.
- Barrel exports for the deleted panels.

KEPT (verified live, NOT panels): config/quantum.ts + config/robotics.ts + services/
weather.ts (map layers), and services/geo-hub-index + tech-hub-index (feed the map's
geo/tech activity markers via geo-activity/tech-activity — the audit missed the relative
import; the build caught it before commit). Relocated `StockIndexData` into its only
consumer, CountryBriefPage.

CSS: `rgba(var(--semantic-critical), 0.2)` is invalid (the token is a hex, not an r,g,b
triple) so 5 alert-fill backgrounds silently rendered nothing → `color-mix(in srgb,
var(--semantic-critical) 20%, transparent)`.

Build + globe/panel e2e green. No feature loss.
This commit is contained in:
zeekay
2026-07-22 13:47:06 -07:00
parent 87dcd07cd3
commit be287d229f
102 changed files with 24709 additions and 2212 deletions
+94
View File
@@ -0,0 +1,94 @@
// Low-end laptop profiler for world.hanzo.ai
// Usage: node profile.mjs <url> <label> [cpuThrottle=6]
// Measures under Chrome DevTools CPU throttling via CDP.
import { chromium } from 'playwright';
const url = process.argv[2] || 'https://world.hanzo.ai/';
const label = process.argv[3] || 'run';
const throttle = Number(process.argv[4] || 6);
const browser = await chromium.launch({ args: ['--enable-precise-memory-info'] });
const context = await browser.newContext({ viewport: { width: 1440, height: 900 } });
const page = await context.newPage();
// Track network transfer sizes by type
const transfer = { js: 0, css: 0, other: 0, jsCount: 0, eagerJs: 0 };
const seen = new Set();
page.on('response', async (resp) => {
try {
const u = resp.url();
if (seen.has(u)) return; seen.add(u);
const hdr = resp.headers();
const enc = hdr['content-encoding'] || '';
const len = Number(hdr['content-length'] || 0);
const ct = hdr['content-type'] || '';
let bytes = len;
if (!bytes) { try { bytes = (await resp.body()).length; } catch { bytes = 0; } }
if (/javascript/.test(ct) || u.endsWith('.js')) { transfer.js += bytes; transfer.jsCount++; }
else if (/css/.test(ct) || u.endsWith('.css')) { transfer.css += bytes; }
else transfer.other += bytes;
} catch {}
});
const client = await context.newCDPSession(page);
await client.send('Emulation.setCPUThrottlingRate', { rate: throttle });
const t0 = Date.now();
await page.goto(url, { waitUntil: 'commit', timeout: 120000 });
// Inject longtask + paint observers ASAP
await page.addInitScript(() => {
window.__lt = { total: 0, count: 0, max: 0 };
try {
new PerformanceObserver((l) => { for (const e of l.getEntries()) { window.__lt.total += e.duration; window.__lt.count++; window.__lt.max = Math.max(window.__lt.max, e.duration); } }).observe({ entryTypes: ['longtask'] });
} catch {}
});
// Wait for the app shell / map container to exist and network to settle a bit
let ttiApprox = null;
try {
await page.waitForSelector('#app .header, #app .main-content, #mapContainer', { timeout: 60000 });
ttiApprox = Date.now() - t0;
} catch {}
// Let it run to steady state under throttle
await page.waitForTimeout(9000);
// Paint + nav timings
const timings = await page.evaluate(() => {
const nav = performance.getEntriesByType('navigation')[0] || {};
const fcp = (performance.getEntriesByType('paint').find(p => p.name === 'first-contentful-paint') || {}).startTime || null;
return {
domContentLoaded: nav.domContentLoadedEventEnd || null,
loadEvent: nav.loadEventEnd || null,
fcp,
longtasks: window.__lt || null,
scripts: performance.getEntriesByType('resource').filter(r => r.initiatorType === 'script' || /\.js(\?|$)/.test(r.name)).length,
};
});
// FPS sample over 4s (rAF frame deltas) — the idle/spin steady-state frame cost
const fps = await page.evaluate(() => new Promise((resolve) => {
const deltas = []; let last = performance.now(); let frames = 0; const start = last;
function tick(now) { deltas.push(now - last); last = now; frames++; if (now - start < 4000) requestAnimationFrame(tick); else {
deltas.sort((a,b)=>a-b);
const avg = deltas.reduce((a,b)=>a+b,0)/deltas.length;
const p95 = deltas[Math.floor(deltas.length*0.95)] || avg;
resolve({ fps: +(1000/avg).toFixed(1), avgFrameMs: +avg.toFixed(1), p95FrameMs: +p95.toFixed(1), frames });
} }
requestAnimationFrame(tick);
}));
const out = {
label, url, cpuThrottle: throttle,
transferKB: { js: Math.round(transfer.js/1024), css: Math.round(transfer.css/1024), jsFiles: transfer.jsCount },
fcpMs: timings.fcp ? Math.round(timings.fcp) : null,
domContentLoadedMs: timings.domContentLoaded ? Math.round(timings.domContentLoaded) : null,
loadEventMs: timings.loadEvent ? Math.round(timings.loadEvent) : null,
shellVisibleMs: ttiApprox,
longTasks: timings.longtasks,
steadyState: fps,
};
console.log('PROFILE_JSON ' + JSON.stringify(out));
console.log(JSON.stringify(out, null, 2));
await browser.close();
@@ -0,0 +1,53 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"metaTitle": {
"type": "string"
},
"keywords": {
"type": "string"
},
"audience": {
"type": "string"
},
"pubDate": {
"type": "string",
"format": "date-time"
},
"modifiedDate": {
"type": "string",
"format": "date-time"
},
"author": {
"type": "string"
},
"authorUrl": {
"type": "string",
"format": "uri"
},
"authorBio": {
"type": "string"
},
"heroImage": {
"type": "string"
},
"$schema": {
"type": "string"
}
},
"required": [
"title",
"description",
"metaTitle",
"keywords",
"audience",
"pubDate"
]
}
+1
View File
@@ -0,0 +1 @@
export default new Map();
+1
View File
@@ -0,0 +1 @@
export default new Map();
+163
View File
@@ -0,0 +1,163 @@
declare module 'astro:content' {
export interface RenderResult {
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
}
interface Render {
'.md': Promise<RenderResult>;
}
export interface RenderedContent {
html: string;
metadata?: {
imagePaths: Array<string>;
[key: string]: unknown;
};
}
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
export type CollectionKey = keyof DataEntryMap;
export type CollectionEntry<C extends CollectionKey> = Flatten<DataEntryMap[C]>;
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
export type ReferenceDataEntry<
C extends CollectionKey,
E extends keyof DataEntryMap[C] = string,
> = {
collection: C;
id: E;
};
export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
collection: C;
id: string;
};
export function getCollection<C extends keyof DataEntryMap, E extends CollectionEntry<C>>(
collection: C,
filter?: (entry: CollectionEntry<C>) => entry is E,
): Promise<E[]>;
export function getCollection<C extends keyof DataEntryMap>(
collection: C,
filter?: (entry: CollectionEntry<C>) => unknown,
): Promise<CollectionEntry<C>[]>;
export function getLiveCollection<C extends keyof LiveContentConfig['collections']>(
collection: C,
filter?: LiveLoaderCollectionFilterType<C>,
): Promise<
import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
entry: ReferenceDataEntry<C, E>,
): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
collection: C,
id: E,
): E extends keyof DataEntryMap[C]
? string extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]> | undefined
: Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getLiveEntry<C extends keyof LiveContentConfig['collections']>(
collection: C,
filter: string | LiveLoaderEntryFilterType<C>,
): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
/** Resolve an array of entry references from the same collection */
export function getEntries<C extends keyof DataEntryMap>(
entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
): Promise<CollectionEntry<C>[]>;
export function render<C extends keyof DataEntryMap>(
entry: DataEntryMap[C][string],
): Promise<RenderResult>;
export function reference<
C extends
| keyof DataEntryMap
// Allow generic `string` to avoid excessive type errors in the config
// if `dev` is not running to update as you edit.
// Invalid collection names will be caught at build time.
| (string & {}),
>(
collection: C,
): import('astro/zod').ZodPipe<
import('astro/zod').ZodString,
import('astro/zod').ZodTransform<
C extends keyof DataEntryMap
? {
collection: C;
id: string;
}
: never,
string
>
>;
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
type InferEntrySchema<C extends keyof DataEntryMap> = import('astro/zod').infer<
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
>;
type ExtractLoaderConfig<T> = T extends { loader: infer L } ? L : never;
type InferLoaderSchema<
C extends keyof DataEntryMap,
L = ExtractLoaderConfig<ContentConfig['collections'][C]>,
> = L extends { schema: import('astro/zod').ZodSchema }
? import('astro/zod').infer<L['schema']>
: any;
type DataEntryMap = {
"blog": Record<string, {
id: string;
body?: string;
collection: "blog";
data: InferEntrySchema<"blog">;
rendered?: RenderedContent;
filePath?: string;
}>;
};
type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
infer TData,
infer TEntryFilter,
infer TCollectionFilter,
infer TError
>
? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
: { data: never; entryFilter: never; collectionFilter: never; error: never };
type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
type ExtractDataType<T> = ExtractLoaderTypes<T>['data'];
type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
LiveContentConfig['collections'][C]['schema'] extends undefined
? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
: import('astro/zod').infer<
Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
>;
type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
ExtractCollectionFilterType<LiveContentConfig['collections'][C]['loader']>;
type LiveLoaderErrorType<C extends keyof LiveContentConfig['collections']> = ExtractErrorType<
LiveContentConfig['collections'][C]['loader']
>;
export type ContentConfig = typeof import("../src/content.config.js");
export type LiveContentConfig = never;
}
+2
View File
@@ -0,0 +1,2 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4
View File
@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://www.world.hanzo.ai/blog/sitemap-index.xml
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"><url><loc>https://www.world.hanzo.ai/blog/</loc><lastmod>2026-03-19T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/ai-powered-intelligence-without-the-cloud/</loc><lastmod>2026-03-07T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/build-on-worldmonitor-developer-api-open-source/</loc><lastmod>2026-03-09T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/command-palette-search-everything-instantly/</loc><lastmod>2026-03-06T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/cyber-threat-intelligence-for-security-teams/</loc><lastmod>2026-02-24T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/five-dashboards-one-platform-worldmonitor-variants/</loc><lastmod>2026-02-12T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/live-webcams-from-geopolitical-hotspots/</loc><lastmod>2026-03-01T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/monitor-global-supply-chains-and-commodity-disruptions/</loc><lastmod>2026-02-26T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/natural-disaster-monitoring-earthquakes-fires-volcanoes/</loc><lastmod>2026-02-19T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/osint-for-everyone-open-source-intelligence-democratized/</loc><lastmod>2026-02-17T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/prediction-markets-ai-forecasting-geopolitics/</loc><lastmod>2026-03-03T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/real-time-market-intelligence-for-traders-and-analysts/</loc><lastmod>2026-02-21T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/satellite-imagery-orbital-surveillance/</loc><lastmod>2026-02-28T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/track-global-conflicts-in-real-time/</loc><lastmod>2026-02-14T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/tracking-global-trade-routes-chokepoints-freight-costs/</loc><lastmod>2026-03-15T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/what-is-worldmonitor-real-time-global-intelligence/</loc><lastmod>2026-02-10T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/worldmonitor-in-21-languages-global-intelligence-for-everyone/</loc><lastmod>2026-03-04T00:00:00.000Z</lastmod></url><url><loc>https://www.world.hanzo.ai/blog/posts/worldmonitor-vs-traditional-intelligence-tools/</loc><lastmod>2026-03-11T00:00:00.000Z</lastmod></url></urlset>
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><sitemap><loc>https://www.world.hanzo.ai/blog/sitemap-0.xml</loc></sitemap></sitemapindex>
+21791
View File
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -5,7 +5,17 @@ import type { CountryScore } from '@/services/country-instability';
import type { PredictionMarket, NewsItem } from '@/types';
import type { AssetType } from '@/types';
import type { CountryBriefSignals } from '@/App';
import type { StockIndexData } from '@/components/CountryIntelModal';
// Local to its only consumer now that the superseded CountryIntelModal is gone.
export interface StockIndexData {
available: boolean;
code: string;
symbol: string;
indexName: string;
price: string;
weekChangePercent: string;
currency: string;
cached?: boolean;
}
import { getNearbyInfrastructure, haversineDistanceKm } from '@/services/related-assets';
import { PORTS } from '@/config/ports';
import type { Port } from '@/config/ports';
-283
View File
@@ -1,283 +0,0 @@
/**
* CountryIntelModal - Shows AI-generated intelligence brief when user clicks a country
*/
import { escapeHtml } from '@/utils/sanitize';
import { t } from '@/services/i18n';
import { sanitizeUrl } from '@/utils/sanitize';
import { getCSSColor } from '@/utils';
import type { CountryScore } from '@/services/country-instability';
import type { PredictionMarket } from '@/types';
interface CountryIntelData {
brief: string;
country: string;
code: string;
cached?: boolean;
generatedAt?: string;
error?: string;
}
export interface StockIndexData {
available: boolean;
code: string;
symbol: string;
indexName: string;
price: string;
weekChangePercent: string;
currency: string;
cached?: boolean;
}
interface ActiveSignals {
protests: number;
militaryFlights: number;
militaryVessels: number;
outages: number;
earthquakes: number;
}
export class CountryIntelModal {
private overlay: HTMLElement;
private contentEl: HTMLElement;
private headerEl: HTMLElement;
private onCloseCallback?: () => void;
private onShareStory?: (code: string, name: string) => void;
private currentCode: string | null = null;
private currentName: string | null = null;
constructor() {
this.overlay = document.createElement('div');
this.overlay.className = 'country-intel-overlay';
this.overlay.innerHTML = `
<div class="country-intel-modal">
<div class="country-intel-header">
<div class="country-intel-title"></div>
<button class="country-intel-close">×</button>
</div>
<div class="country-intel-content"></div>
</div>
`;
document.body.appendChild(this.overlay);
this.headerEl = this.overlay.querySelector('.country-intel-title')!;
this.contentEl = this.overlay.querySelector('.country-intel-content')!;
this.overlay.querySelector('.country-intel-close')?.addEventListener('click', () => this.hide());
this.overlay.addEventListener('click', (e) => {
if ((e.target as HTMLElement).classList.contains('country-intel-overlay')) this.hide();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.overlay.classList.contains('active')) this.hide();
});
}
private countryFlag(code: string): string {
try {
return code
.toUpperCase()
.split('')
.map((c) => String.fromCodePoint(0x1f1e6 + c.charCodeAt(0) - 65))
.join('');
} catch {
return '🌍';
}
}
private levelBadge(level: string): string {
const varMap: Record<string, string> = {
critical: '--semantic-critical',
high: '--semantic-high',
elevated: '--semantic-elevated',
normal: '--semantic-normal',
low: '--semantic-low',
};
const color = getCSSColor(varMap[level] || '--text-dim');
return `<span class="cii-badge" style="background:${color}20;color:${color};border:1px solid ${color}40">${level.toUpperCase()}</span>`;
}
private scoreBar(score: number): string {
const pct = Math.min(100, Math.max(0, score));
const color = pct >= 70 ? getCSSColor('--semantic-critical') : pct >= 50 ? getCSSColor('--semantic-high') : pct >= 30 ? getCSSColor('--semantic-elevated') : getCSSColor('--semantic-normal');
return `
<div class="cii-score-bar">
<div class="cii-score-fill" style="width:${pct}%;background:${color}"></div>
</div>
<span class="cii-score-value">${score}/100</span>
`;
}
public showLoading(): void {
this.currentCode = '__loading__';
this.headerEl.innerHTML = `
<span class="country-flag">🌍</span>
<span class="country-name">${t('modals.countryIntel.identifying')}</span>
`;
this.contentEl.innerHTML = `
<div class="intel-brief-section">
<div class="intel-brief-loading">
<div class="intel-skeleton"></div>
<div class="intel-skeleton short"></div>
<span class="intel-loading-text">${t('modals.countryIntel.locating')}</span>
</div>
</div>
`;
this.overlay.classList.add('active');
}
public show(country: string, code: string, score: CountryScore | null, signals?: ActiveSignals): void {
this.currentCode = code;
this.currentName = country;
const flag = this.countryFlag(code);
let html = '';
this.overlay.classList.add('active');
this.headerEl.innerHTML = `
<span class="country-flag">${flag}</span>
<span class="country-name">${escapeHtml(country)}</span>
${score ? this.levelBadge(score.level) : ''}
<button class="country-intel-share-btn" title="${t('modals.story.shareTitle')}"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12v7a2 2 0 002 2h12a2 2 0 002-2v-7"/><polyline points="16 6 12 2 8 6"/><line x1="12" y1="2" x2="12" y2="15"/></svg></button>
`;
if (score) {
html += `
<div class="cii-section">
<div class="cii-label">${t('modals.countryIntel.instabilityIndex')} ${this.scoreBar(score.score)}</div>
<div class="cii-components">
<span title="${t('common.unrest')}">📢 ${score.components.unrest.toFixed(0)}</span>
<span title="${t('common.conflict')}"> ${score.components.conflict.toFixed(0)}</span>
<span title="${t('common.security')}">🛡 ${score.components.security.toFixed(0)}</span>
<span title="${t('common.information')}">📡 ${score.components.information.toFixed(0)}</span>
<span class="cii-trend ${score.trend}">${score.trend === 'rising' ? '↗' : score.trend === 'falling' ? '↘' : '→'} ${score.trend}</span>
</div>
</div>
`;
}
const chips: string[] = [];
if (signals) {
if (signals.protests > 0) chips.push(`<span class="signal-chip protest">📢 ${signals.protests} ${t('modals.countryIntel.protests')}</span>`);
if (signals.militaryFlights > 0) chips.push(`<span class="signal-chip military">✈️ ${signals.militaryFlights} ${t('modals.countryIntel.militaryAircraft')}</span>`);
if (signals.militaryVessels > 0) chips.push(`<span class="signal-chip military">⚓ ${signals.militaryVessels} ${t('modals.countryIntel.militaryVessels')}</span>`);
if (signals.outages > 0) chips.push(`<span class="signal-chip outage">🌐 ${signals.outages} ${t('modals.countryIntel.outages')}</span>`);
if (signals.earthquakes > 0) chips.push(`<span class="signal-chip quake">🌍 ${signals.earthquakes} ${t('modals.countryIntel.earthquakes')}</span>`);
}
chips.push(`<span class="signal-chip stock-loading">📈 ${t('modals.countryIntel.loadingIndex')}</span>`);
html += `<div class="active-signals">${chips.join('')}</div>`;
html += `<div class="country-markets-section"><span class="intel-loading-text">${t('modals.countryIntel.loadingMarkets')}</span></div>`;
html += `
<div class="intel-brief-section">
<div class="intel-brief-loading">
<div class="intel-skeleton"></div>
<div class="intel-skeleton short"></div>
<div class="intel-skeleton"></div>
<div class="intel-skeleton short"></div>
<span class="intel-loading-text">${t('modals.countryIntel.generatingBrief')}</span>
</div>
</div>
`;
this.contentEl.innerHTML = html;
const shareBtn = this.headerEl.querySelector('.country-intel-share-btn');
shareBtn?.addEventListener('click', (e) => {
e.stopPropagation();
if (this.currentCode && this.currentName && this.onShareStory) {
this.onShareStory(this.currentCode, this.currentName);
}
});
}
public updateBrief(data: CountryIntelData & { skipped?: boolean; reason?: string; fallback?: boolean }): void {
if (this.currentCode !== data.code && this.currentCode !== '__loading__') return;
// If modal closed, don't update
if (!this.isVisible()) return;
if (data.error || data.skipped || !data.brief) {
const msg = data.error || data.reason || t('modals.countryIntel.unavailable');
const briefSection = this.contentEl.querySelector('.intel-brief-section');
if (briefSection) {
briefSection.innerHTML = `<div class="intel-error">${escapeHtml(msg)}</div>`;
}
return;
}
const briefSection = this.contentEl.querySelector('.intel-brief-section');
if (!briefSection) return;
const formatted = this.formatBrief(data.brief);
briefSection.innerHTML = `
<div class="intel-brief">${formatted}</div>
<div class="intel-footer">
${data.cached ? `<span class="intel-cached">📋 ${t('modals.countryIntel.cached')}</span>` : `<span class="intel-fresh">✨ ${t('modals.countryIntel.fresh')}</span>`}
<span class="intel-timestamp">${data.generatedAt ? new Date(data.generatedAt).toLocaleTimeString() : ''}</span>
</div>
`;
}
public updateMarkets(markets: PredictionMarket[]): void {
const section = this.contentEl.querySelector('.country-markets-section');
if (!section) return;
if (markets.length === 0) {
section.innerHTML = `<span class="intel-loading-text" style="opacity:0.5">${t('modals.countryIntel.noMarkets')}</span>`;
return;
}
const items = markets.map(market => {
const href = sanitizeUrl(market.url || '#') || '#';
return `
<div class="market-item">
<a href="${href}" target="_blank" rel="noopener noreferrer" class="prediction-market-card">
<div class="market-provider">Polymarket</div>
<div class="market-question">${escapeHtml(market.title)}</div>
<div class="market-prob">${(market.yesPrice * 100).toFixed(1)}%</div>
</a>
`;
}).join('');
section.innerHTML = `<div class="markets-label">📊 ${t('modals.countryIntel.predictionMarkets')}</div>${items}`;
}
public updateStock(data: StockIndexData): void {
const el = this.contentEl.querySelector('.stock-loading');
if (!el) return;
if (!data.available) {
el.remove();
return;
}
const pct = parseFloat(data.weekChangePercent);
const sign = pct >= 0 ? '+' : '';
const cls = pct >= 0 ? 'stock-up' : 'stock-down';
const arrow = pct >= 0 ? '📈' : '📉';
el.className = `signal-chip stock ${cls}`;
el.innerHTML = `${arrow} ${escapeHtml(data.indexName)}: ${sign}${data.weekChangePercent}% (1W)`;
}
private formatBrief(text: string): string {
return escapeHtml(text)
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>')
.replace(/^/, '<p>')
.replace(/$/, '</p>');
}
public hide(): void {
this.overlay.classList.remove('active');
this.currentCode = null;
this.onCloseCallback?.();
}
public onClose(cb: () => void): void {
this.onCloseCallback = cb;
}
public isVisible(): boolean {
return this.overlay.classList.contains('active');
}
}
-123
View File
@@ -1,123 +0,0 @@
import { Panel } from './Panel';
import type { GeoHubActivity } from '@/services/geo-activity';
import { escapeHtml, sanitizeUrl } from '@/utils/sanitize';
import { t } from '@/services/i18n';
import { getCSSColor } from '@/utils';
const COUNTRY_FLAGS: Record<string, string> = {
'USA': '🇺🇸', 'Russia': '🇷🇺', 'China': '🇨🇳', 'UK': '🇬🇧', 'Belgium': '🇧🇪',
'Israel': '🇮🇱', 'Iran': '🇮🇷', 'Ukraine': '🇺🇦', 'Taiwan': '🇹🇼', 'Japan': '🇯🇵',
'South Korea': '🇰🇷', 'North Korea': '🇰🇵', 'India': '🇮🇳', 'Saudi Arabia': '🇸🇦',
'Turkey': '🇹🇷', 'France': '🇫🇷', 'Germany': '🇩🇪', 'Egypt': '🇪🇬', 'Pakistan': '🇵🇰',
'Palestine': '🇵🇸', 'Yemen': '🇾🇪', 'Syria': '🇸🇾', 'Lebanon': '🇱🇧',
'Sudan': '🇸🇩', 'Ethiopia': '🇪🇹', 'Myanmar': '🇲🇲', 'Austria': '🇦🇹',
'International': '🌐',
};
const TYPE_ICONS: Record<string, string> = {
capital: '🏛️',
conflict: '⚔️',
strategic: '⚓',
organization: '🏢',
};
const TYPE_LABELS: Record<string, string> = {
capital: 'Capital',
conflict: 'Conflict Zone',
strategic: 'Strategic',
organization: 'Organization',
};
export class GeoHubsPanel extends Panel {
private activities: GeoHubActivity[] = [];
private onHubClick?: (hub: GeoHubActivity) => void;
constructor() {
super({
id: 'geo-hubs',
title: t('panels.geoHubs'),
showCount: true,
infoTooltip: t('components.geoHubs.infoTooltip', {
highColor: getCSSColor('--semantic-critical'),
elevatedColor: getCSSColor('--semantic-high'),
lowColor: getCSSColor('--text-dim'),
}),
});
}
public setOnHubClick(handler: (hub: GeoHubActivity) => void): void {
this.onHubClick = handler;
}
public setActivities(activities: GeoHubActivity[]): void {
this.activities = activities.slice(0, 10);
this.setCount(this.activities.length);
this.render();
}
private getFlag(country: string): string {
return COUNTRY_FLAGS[country] || '🌐';
}
private getTypeIcon(type: string): string {
return TYPE_ICONS[type] || '📍';
}
private getTypeLabel(type: string): string {
return TYPE_LABELS[type] || type;
}
private render(): void {
if (this.activities.length === 0) {
this.showError(t('common.noActiveGeoHubs'));
return;
}
const html = this.activities.map((hub, index) => {
const trendIcon = hub.trend === 'rising' ? '↑' : hub.trend === 'falling' ? '↓' : '';
const breakingTag = hub.hasBreaking ? '<span class="hub-breaking geo">ALERT</span>' : '';
const topStory = hub.topStories[0];
return `
<div class="geo-hub-item ${hub.activityLevel}" data-hub-id="${escapeHtml(hub.hubId)}" data-index="${index}">
<div class="hub-rank">${index + 1}</div>
<span class="geo-hub-indicator ${hub.activityLevel}"></span>
<div class="hub-info">
<div class="hub-header">
<span class="hub-name">${escapeHtml(hub.name)}</span>
<span class="hub-flag">${this.getFlag(hub.country)}</span>
${breakingTag}
</div>
<div class="hub-meta">
<span class="hub-news-count">${hub.newsCount} ${hub.newsCount === 1 ? t('components.geoHubs.story') : t('components.geoHubs.stories')}</span>
${trendIcon ? `<span class="hub-trend ${hub.trend}">${trendIcon}</span>` : ''}
<span class="geo-hub-type">${this.getTypeIcon(hub.type)} ${this.getTypeLabel(hub.type)}</span>
</div>
</div>
<div class="hub-score geo">${Math.round(hub.score)}</div>
</div>
${topStory ? `
<a class="hub-top-story geo" href="${sanitizeUrl(topStory.link)}" target="_blank" rel="noopener" data-hub-id="${escapeHtml(hub.hubId)}">
${escapeHtml(topStory.title.length > 80 ? topStory.title.slice(0, 77) + '...' : topStory.title)}
</a>
` : ''}
`;
}).join('');
this.setContent(html);
this.bindEvents();
}
private bindEvents(): void {
const items = this.content.querySelectorAll<HTMLDivElement>('.geo-hub-item');
items.forEach((item) => {
item.addEventListener('click', () => {
const hubId = item.dataset.hubId;
const hub = this.activities.find(a => a.hubId === hubId);
if (hub && this.onHubClick) {
this.onHubClick(hub);
}
});
});
}
}
-73
View File
@@ -1,73 +0,0 @@
import { Panel } from './Panel';
import { escapeHtml } from '@/utils/sanitize';
import { getPostQuantumData, type PostQuantumData } from '@/services/post-quantum';
import type { PQReadinessStatus } from '@/config/post-quantum';
const STATUS_SEV: Record<PQReadinessStatus, string> = {
'pq-native': 'sev-info',
deployed: 'sev-low',
'in-progress': 'sev-elevated',
planned: 'sev-high',
lagging: 'sev-critical',
};
const STATUS_LABEL: Record<PQReadinessStatus, string> = {
'pq-native': 'PQ-NATIVE',
deployed: 'DEPLOYED',
'in-progress': 'IN PROGRESS',
planned: 'PLANNED',
lagging: 'LAGGING',
};
export class PostQuantumPanel extends Panel {
constructor() {
super({
id: 'post-quantum',
title: 'Post-Quantum Readiness',
showCount: true,
infoTooltip: 'Hanzo World Model — Post-Quantum lens: NIST PQC standards (ML-KEM/ML-DSA/SLH-DSA), the NSA CNSA 2.0 timeline, and deployment status across governments, clouds and chains. Lux & Hanzo are PQ-native.',
});
this.render(getPostQuantumData());
}
public refresh(): void {
this.render(getPostQuantumData());
}
private render(data: PostQuantumData): void {
this.setCount(data.readiness.length);
this.setDataBadge('cached', 'curated');
const standardRows = data.standards.map((s) => `
<div class="domain-item">
<div class="domain-item-title">🔐 ${escapeHtml(s.id)} · ${escapeHtml(s.name)} <span class="domain-tag">${escapeHtml(s.status)}</span></div>
<div class="domain-item-meta">${escapeHtml(s.basedOn)} · ${escapeHtml(s.kind)} · ${s.year} ${escapeHtml(s.note)}</div>
</div>`).join('');
const readinessRows = data.readiness.map((r) => `
<div class="domain-item">
<div class="domain-item-title">${escapeHtml(r.org)} <span class="domain-tag ${STATUS_SEV[r.status]}">${STATUS_LABEL[r.status]}</span></div>
<div class="domain-item-meta">${escapeHtml(r.algorithms.join(', '))} ${escapeHtml(r.detail)}</div>
</div>`).join('');
const timelineRows = data.timeline.map((m) => `
<div class="domain-item">
<div class="domain-item-title">📅 CNSA 2.0 · ${m.year}</div>
<div class="domain-item-meta">${escapeHtml(m.milestone)}</div>
</div>`).join('');
this.setContent(`
<div class="domain-panel">
<div class="domain-insight">${escapeHtml(data.insight)}</div>
<div class="domain-warn"> ${escapeHtml(data.harvestNote)}</div>
<div class="domain-section-title">NIST PQC standards</div>
<div class="domain-list">${standardRows}</div>
<div class="domain-section-title">Readiness tracker</div>
<div class="domain-list">${readinessRows}</div>
<div class="domain-section-title">CNSA 2.0 timeline</div>
<div class="domain-list">${timelineRows}</div>
<div class="domain-footer"><span>NIST FIPS 203/204/205 · NSA CNSA 2.0</span><span>curated</span></div>
</div>
`);
}
}
-81
View File
@@ -1,81 +0,0 @@
import { Panel } from './Panel';
import { escapeHtml, sanitizeUrl } from '@/utils/sanitize';
import { getQuantumData, type QuantumData } from '@/services/quantum';
const MODALITY_ICON: Record<string, string> = {
superconducting: '❄️',
'trapped-ion': '⚛️',
'neutral-atom': '🔵',
photonic: '💡',
'silicon-spin': '🔶',
annealing: '🌀',
topological: '🪢',
};
export class QuantumPanel extends Panel {
private loading = false;
private lastFetch = 0;
private readonly REFRESH_MS = 60 * 60 * 1000;
constructor() {
super({
id: 'quantum',
title: 'Quantum Computing',
showCount: true,
infoTooltip: 'Hanzo World Model — Quantum lens: live arXiv quant-ph research plus a curated registry of hardware players and their best publicly-announced scale (snapshot, with year).',
});
void this.refresh();
}
public async refresh(): Promise<void> {
if (this.loading) return;
if (this.lastFetch > 0 && Date.now() - this.lastFetch < this.REFRESH_MS) return;
this.loading = true;
try {
const data = await getQuantumData();
this.lastFetch = Date.now();
this.setCount(data.papers.length + data.players.length);
this.setDataBadge(data.papers.length > 0 ? 'live' : 'cached');
this.render(data);
} catch (e) {
console.error('[Quantum] refresh failed:', e);
this.showError();
} finally {
this.loading = false;
}
}
private render(data: QuantumData): void {
const papers = data.papers.slice(0, 8);
const players = [...data.players].sort((a, b) => (b.qubits ?? -1) - (a.qubits ?? -1));
const paperRows = papers.map((p) => {
const url = sanitizeUrl(p.link);
const date = p.published.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return `<a class="domain-item" href="${url}" target="_blank" rel="noopener">
<div class="domain-item-title">${escapeHtml(p.title)}</div>
<div class="domain-item-meta">🔬 quant-ph · ${date}</div>
</a>`;
}).join('');
const playerRows = players.map((p) => {
const icon = MODALITY_ICON[p.modality] || '⚛️';
const link = p.url ? ` · <a href="${sanitizeUrl(p.url)}" target="_blank" rel="noopener">site ↗</a>` : '';
return `<div class="domain-item">
<div class="domain-item-title">${icon} ${escapeHtml(p.name)} <span class="domain-tag">${escapeHtml(p.metric)}</span></div>
<div class="domain-item-meta">${escapeHtml(p.modality)} · ${escapeHtml(p.city)}, ${escapeHtml(p.country)} · as of ${p.asOf}${link}</div>
</div>`;
}).join('');
this.setContent(`
<div class="domain-panel">
<div class="domain-insight">${escapeHtml(data.insight)}</div>
<div class="domain-section-title">Latest research (arXiv quant-ph)</div>
<div class="domain-list">${paperRows || '<div class="domain-empty">Research feed unavailable</div>'}</div>
<div class="domain-section-title">Hardware players &amp; scale</div>
<div class="domain-list">${playerRows}</div>
<div class="domain-footer"><span>arXiv quant-ph + curated registry</span><span>${new Date(this.lastFetch).toLocaleTimeString()}</span></div>
</div>
`);
}
}
-320
View File
@@ -1,320 +0,0 @@
import { Panel } from './Panel';
import type { AIRegulation, RegulatoryAction, CountryRegulationProfile } from '@/types';
import {
AI_REGULATIONS,
COUNTRY_REGULATION_PROFILES,
getUpcomingDeadlines,
getRecentActions,
} from '@/config';
import { escapeHtml, sanitizeUrl } from '@/utils/sanitize';
import { t } from '@/services/i18n';
import { getCSSColor } from '@/utils';
export class RegulationPanel extends Panel {
private viewMode: 'timeline' | 'deadlines' | 'regulations' | 'countries' = 'timeline';
constructor(id: string) {
super({ id, title: t('panels.regulation') });
this.render();
}
protected render(): void {
this.content.innerHTML = `
<div class="regulation-panel">
<div class="regulation-header">
<h3>AI Regulation Dashboard</h3>
<div class="regulation-tabs">
<button class="tab ${this.viewMode === 'timeline' ? 'active' : ''}" data-view="timeline">Timeline</button>
<button class="tab ${this.viewMode === 'deadlines' ? 'active' : ''}" data-view="deadlines">Deadlines</button>
<button class="tab ${this.viewMode === 'regulations' ? 'active' : ''}" data-view="regulations">Regulations</button>
<button class="tab ${this.viewMode === 'countries' ? 'active' : ''}" data-view="countries">Countries</button>
</div>
</div>
<div class="regulation-content">
${this.renderContent()}
</div>
</div>
`;
// Add event listeners for tabs
this.content.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const view = target.dataset.view as typeof this.viewMode;
if (view) {
this.viewMode = view;
this.render();
}
});
});
}
private renderContent(): string {
switch (this.viewMode) {
case 'timeline':
return this.renderTimeline();
case 'deadlines':
return this.renderDeadlines();
case 'regulations':
return this.renderRegulations();
case 'countries':
return this.renderCountries();
default:
return '';
}
}
private renderTimeline(): string {
const recentActions = getRecentActions(12); // Last 12 months
if (recentActions.length === 0) {
return '<div class="empty-state">No recent regulatory actions</div>';
}
return `
<div class="timeline-view">
<div class="timeline-header">
<h4>Recent Regulatory Actions (Last 12 Months)</h4>
<span class="count">${recentActions.length} actions</span>
</div>
<div class="timeline-list">
${recentActions.map(action => this.renderTimelineItem(action)).join('')}
</div>
</div>
`;
}
private renderTimelineItem(action: RegulatoryAction): string {
const date = new Date(action.date);
const formattedDate = date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
const typeIcons: Record<RegulatoryAction['type'], string> = {
'law-passed': '📜',
'executive-order': '🏛️',
'guideline': '📋',
'enforcement': '⚖️',
'consultation': '💬',
};
const impactColors: Record<RegulatoryAction['impact'], string> = {
high: getCSSColor('--semantic-critical'),
medium: getCSSColor('--semantic-elevated'),
low: getCSSColor('--semantic-normal'),
};
return `
<div class="timeline-item impact-${action.impact}">
<div class="timeline-marker">
<span class="timeline-icon">${typeIcons[action.type]}</span>
<div class="timeline-line"></div>
</div>
<div class="timeline-content">
<div class="timeline-header-row">
<span class="timeline-date">${formattedDate}</span>
<span class="timeline-country">${escapeHtml(action.country)}</span>
<span class="timeline-impact" style="color: ${impactColors[action.impact]}">${action.impact.toUpperCase()}</span>
</div>
<h5>${escapeHtml(action.title)}</h5>
<p>${escapeHtml(action.description)}</p>
${action.source ? `<span class="timeline-source">Source: ${escapeHtml(action.source)}</span>` : ''}
</div>
</div>
`;
}
private renderDeadlines(): string {
const upcomingDeadlines = getUpcomingDeadlines();
if (upcomingDeadlines.length === 0) {
return '<div class="empty-state">No upcoming compliance deadlines in the next 12 months</div>';
}
return `
<div class="deadlines-view">
<div class="deadlines-header">
<h4>Upcoming Compliance Deadlines</h4>
<span class="count">${upcomingDeadlines.length} deadlines</span>
</div>
<div class="deadlines-list">
${upcomingDeadlines.map(reg => this.renderDeadlineItem(reg)).join('')}
</div>
</div>
`;
}
private renderDeadlineItem(regulation: AIRegulation): string {
const deadline = new Date(regulation.complianceDeadline!);
const now = new Date();
const daysUntil = Math.ceil((deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
const formattedDate = deadline.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
const urgencyClass = daysUntil < 90 ? 'urgent' : daysUntil < 180 ? 'warning' : 'normal';
return `
<div class="deadline-item ${urgencyClass}">
<div class="deadline-countdown">
<div class="days-until">${daysUntil}</div>
<div class="days-label">days</div>
</div>
<div class="deadline-content">
<h5>${escapeHtml(regulation.shortName)}</h5>
<p class="deadline-name">${escapeHtml(regulation.name)}</p>
<div class="deadline-meta">
<span class="deadline-date">📅 ${formattedDate}</span>
<span class="deadline-country">🌍 ${escapeHtml(regulation.country)}</span>
</div>
${regulation.penalties ? `<p class="deadline-penalties">⚠️ Penalties: ${escapeHtml(regulation.penalties)}</p>` : ''}
<div class="deadline-scope">
${regulation.scope.map(s => `<span class="scope-tag">${escapeHtml(s)}</span>`).join('')}
</div>
</div>
</div>
`;
}
private renderRegulations(): string {
const activeRegulations = AI_REGULATIONS.filter(r => r.status === 'active');
const proposedRegulations = AI_REGULATIONS.filter(r => r.status === 'proposed');
return `
<div class="regulations-view">
<div class="regulations-section">
<h4>Active Regulations (${activeRegulations.length})</h4>
<div class="regulations-list">
${activeRegulations.map(reg => this.renderRegulationCard(reg)).join('')}
</div>
</div>
<div class="regulations-section">
<h4>Proposed Regulations (${proposedRegulations.length})</h4>
<div class="regulations-list">
${proposedRegulations.map(reg => this.renderRegulationCard(reg)).join('')}
</div>
</div>
</div>
`;
}
private renderRegulationCard(regulation: AIRegulation): string {
const typeColors: Record<AIRegulation['type'], string> = {
comprehensive: getCSSColor('--semantic-low'),
sectoral: getCSSColor('--semantic-high'),
voluntary: getCSSColor('--semantic-normal'),
proposed: getCSSColor('--semantic-elevated'),
};
const effectiveDate = regulation.effectiveDate
? new Date(regulation.effectiveDate).toLocaleDateString('en-US', { year: 'numeric', month: 'short' })
: 'TBD';
const regulationLink = regulation.link ? sanitizeUrl(regulation.link) : '';
return `
<div class="regulation-card">
<div class="regulation-card-header">
<h5>${escapeHtml(regulation.shortName)}</h5>
<span class="regulation-type" style="background-color: ${typeColors[regulation.type]}">${regulation.type}</span>
</div>
<p class="regulation-full-name">${escapeHtml(regulation.name)}</p>
<div class="regulation-meta">
<span>🌍 ${escapeHtml(regulation.country)}</span>
<span>📅 ${effectiveDate}</span>
<span class="status-badge status-${regulation.status}">${regulation.status}</span>
</div>
${regulation.description ? `<p class="regulation-description">${escapeHtml(regulation.description)}</p>` : ''}
<div class="regulation-provisions">
<strong>Key Provisions:</strong>
<ul>
${regulation.keyProvisions.slice(0, 3).map(p => `<li>${escapeHtml(p)}</li>`).join('')}
${regulation.keyProvisions.length > 3 ? `<li class="more-provisions">+${regulation.keyProvisions.length - 3} more...</li>` : ''}
</ul>
</div>
<div class="regulation-scope">
${regulation.scope.map(s => `<span class="scope-tag">${escapeHtml(s)}</span>`).join('')}
</div>
${regulationLink ? `<a href="${regulationLink}" target="_blank" rel="noopener noreferrer" class="regulation-link">Learn More →</a>` : ''}
</div>
`;
}
private renderCountries(): string {
const profiles = COUNTRY_REGULATION_PROFILES.sort((a, b) => {
const stanceOrder: Record<CountryRegulationProfile['stance'], number> = {
strict: 0,
moderate: 1,
permissive: 2,
undefined: 3,
};
return stanceOrder[a.stance] - stanceOrder[b.stance];
});
return `
<div class="countries-view">
<div class="countries-header">
<h4>Global Regulatory Landscape</h4>
<div class="stance-legend">
<span class="legend-item"><span class="color-box strict"></span> Strict</span>
<span class="legend-item"><span class="color-box moderate"></span> Moderate</span>
<span class="legend-item"><span class="color-box permissive"></span> Permissive</span>
<span class="legend-item"><span class="color-box undefined"></span> Undefined</span>
</div>
</div>
<div class="countries-list">
${profiles.map(profile => this.renderCountryCard(profile)).join('')}
</div>
</div>
`;
}
private renderCountryCard(profile: CountryRegulationProfile): string {
const stanceColors: Record<CountryRegulationProfile['stance'], string> = {
strict: getCSSColor('--semantic-critical'),
moderate: getCSSColor('--semantic-elevated'),
permissive: getCSSColor('--semantic-normal'),
undefined: getCSSColor('--text-muted'),
};
const activeCount = profile.activeRegulations.length;
const proposedCount = profile.proposedRegulations.length;
return `
<div class="country-card stance-${profile.stance}">
<div class="country-card-header" style="border-left: 4px solid ${stanceColors[profile.stance]}">
<h5>${escapeHtml(profile.country)}</h5>
<span class="stance-badge" style="background-color: ${stanceColors[profile.stance]}">${profile.stance.toUpperCase()}</span>
</div>
<p class="country-summary">${escapeHtml(profile.summary)}</p>
<div class="country-stats">
<div class="stat">
<span class="stat-value">${activeCount}</span>
<span class="stat-label">Active</span>
</div>
<div class="stat">
<span class="stat-value">${proposedCount}</span>
<span class="stat-label">Proposed</span>
</div>
<div class="stat">
<span class="stat-value">${new Date(profile.lastUpdated).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}</span>
<span class="stat-label">Updated</span>
</div>
</div>
</div>
`;
}
public updateData(): void {
this.render();
}
public setView(view: 'timeline' | 'deadlines' | 'regulations' | 'countries'): void {
this.viewMode = view;
this.render();
}
}
-79
View File
@@ -1,79 +0,0 @@
import { Panel } from './Panel';
import { escapeHtml, sanitizeUrl } from '@/utils/sanitize';
import { getRoboticsData, type RoboticsData } from '@/services/robotics';
const CATEGORY_ICON: Record<string, string> = {
humanoid: '🤖',
quadruped: '🐕',
industrial: '🦾',
platform: '🧠',
research: '🔬',
};
export class RoboticsPanel extends Panel {
private loading = false;
private lastFetch = 0;
private readonly REFRESH_MS = 60 * 60 * 1000;
constructor() {
super({
id: 'robotics',
title: 'Robotics',
showCount: true,
infoTooltip: 'Hanzo World Model — Robotics lens: live arXiv cs.RO research plus a curated registry of humanoid, quadruped, industrial and platform labs.',
});
void this.refresh();
}
public async refresh(): Promise<void> {
if (this.loading) return;
if (this.lastFetch > 0 && Date.now() - this.lastFetch < this.REFRESH_MS) return;
this.loading = true;
try {
const data = await getRoboticsData();
this.lastFetch = Date.now();
this.setCount(data.papers.length + data.orgs.length);
this.setDataBadge(data.papers.length > 0 ? 'live' : 'cached');
this.render(data);
} catch (e) {
console.error('[Robotics] refresh failed:', e);
this.showError();
} finally {
this.loading = false;
}
}
private render(data: RoboticsData): void {
const papers = data.papers.slice(0, 8);
const orgs = [...data.orgs].sort((a, b) => a.category.localeCompare(b.category));
const paperRows = papers.map((p) => {
const url = sanitizeUrl(p.link);
const date = p.published.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
return `<a class="domain-item" href="${url}" target="_blank" rel="noopener">
<div class="domain-item-title">${escapeHtml(p.title)}</div>
<div class="domain-item-meta">🔬 ${escapeHtml(p.categories[0] || 'cs.RO')} · ${date}</div>
</a>`;
}).join('');
const orgRows = orgs.map((o) => {
const icon = CATEGORY_ICON[o.category] || '🤖';
const link = o.url ? ` · <a href="${sanitizeUrl(o.url)}" target="_blank" rel="noopener">site ↗</a>` : '';
return `<div class="domain-item">
<div class="domain-item-title">${icon} ${escapeHtml(o.name)} <span class="domain-tag">${escapeHtml(o.category)}</span></div>
<div class="domain-item-meta">${escapeHtml(o.focus)} · ${escapeHtml(o.city)}, ${escapeHtml(o.country)}${link}</div>
</div>`;
}).join('');
this.setContent(`
<div class="domain-panel">
<div class="domain-insight">${escapeHtml(data.insight)}</div>
<div class="domain-section-title">Latest research (arXiv cs.RO)</div>
<div class="domain-list">${paperRows || '<div class="domain-empty">Research feed unavailable</div>'}</div>
<div class="domain-section-title">Major labs &amp; companies</div>
<div class="domain-list">${orgRows}</div>
<div class="domain-footer"><span>arXiv cs.RO + curated registry</span><span>${new Date(this.lastFetch).toLocaleTimeString()}</span></div>
</div>
`);
}
}
-67
View File
@@ -1,67 +0,0 @@
import { Panel } from './Panel';
import { escapeHtml } from '@/utils/sanitize';
import { fetchSpaceWeather, type SpaceWeatherState } from '@/services/space-weather';
function kpClass(kp: number | null): string {
if (kp === null) return 'sev-info';
if (kp >= 8) return 'sev-critical';
if (kp >= 7) return 'sev-high';
if (kp >= 5) return 'sev-elevated';
if (kp >= 4) return 'sev-low';
return 'sev-info';
}
export class SpaceWeatherPanel extends Panel {
private loading = false;
private lastFetch = 0;
private readonly REFRESH_MS = 10 * 60 * 1000;
constructor() {
super({
id: 'space-weather',
title: 'Space Weather',
showCount: true,
infoTooltip: 'Hanzo World Model — Space Weather lens: NOAA SWPC planetary Kp index and geomagnetic/solar advisories. Drives satellite, GPS, grid and HF-comms risk worldwide.',
});
void this.refresh();
}
public async refresh(): Promise<void> {
if (this.loading) return;
if (this.lastFetch > 0 && Date.now() - this.lastFetch < this.REFRESH_MS) return;
this.loading = true;
try {
const state = await fetchSpaceWeather();
this.lastFetch = Date.now();
this.setCount(state.alerts.length);
this.setDataBadge(state.kp !== null || state.alerts.length > 0 ? 'live' : 'unavailable');
this.render(state);
} catch (e) {
console.error('[SpaceWeather] refresh failed:', e);
this.showError();
} finally {
this.loading = false;
}
}
private render(state: SpaceWeatherState): void {
const alerts = state.alerts.map((a) => `
<div class="domain-item">
<div class="domain-item-title">📡 ${escapeHtml(a.message)}</div>
<div class="domain-item-meta">${a.issued.toLocaleString()}</div>
</div>`).join('');
this.setContent(`
<div class="domain-panel">
<div class="domain-insight">${escapeHtml(state.insight)}</div>
<div class="domain-item">
<div class="domain-item-title">🧭 Planetary Kp <span class="domain-tag ${kpClass(state.kp)}">${state.kp ?? '—'}</span></div>
<div class="domain-item-meta">${escapeHtml(state.stormLevel)}${state.kpTime ? ' · ' + state.kpTime.toLocaleString() : ''}</div>
</div>
<div class="domain-section-title">Advisories</div>
<div class="domain-list">${alerts || '<div class="domain-empty">No active advisories</div>'}</div>
<div class="domain-footer"><span>NOAA SWPC</span><span>${new Date(this.lastFetch).toLocaleTimeString()}</span></div>
</div>
`);
}
}
-71
View File
@@ -1,71 +0,0 @@
import { Panel } from './Panel';
import { escapeHtml, sanitizeUrl } from '@/utils/sanitize';
import { fetchSportsEvents, sportsInsight, type SportEvent } from '@/services/sports';
export class SportsPanel extends Panel {
private loading = false;
private lastFetch = 0;
private readonly REFRESH_MS = 2 * 60 * 1000;
constructor() {
super({
id: 'sports',
title: 'Sports & Events',
showCount: true,
infoTooltip: 'Hanzo World Model — Sports lens: live and scheduled major events across the NFL, NBA, MLB, NHL, Premier League and UEFA Champions League (ESPN public API).',
});
void this.refresh();
}
public async refresh(): Promise<void> {
if (this.loading) return;
if (this.lastFetch > 0 && Date.now() - this.lastFetch < this.REFRESH_MS) return;
this.loading = true;
try {
const events = await fetchSportsEvents();
this.lastFetch = Date.now();
this.setCount(events.length);
this.setDataBadge(events.length > 0 ? 'live' : 'unavailable');
this.render(events);
} catch (e) {
console.error('[Sports] refresh failed:', e);
this.showError();
} finally {
this.loading = false;
}
}
private renderEvent(e: SportEvent): string {
const live = e.state === 'in';
const badge = live
? '<span class="domain-tag sev-high">LIVE</span>'
: e.state === 'post'
? '<span class="domain-tag">FINAL</span>'
: `<span class="domain-tag">${escapeHtml(e.status)}</span>`;
const score = e.state === 'pre'
? escapeHtml(e.status)
: `${escapeHtml(e.away.name)} ${escapeHtml(e.away.score)}${escapeHtml(e.home.name)} ${escapeHtml(e.home.score)}`;
const title = e.url
? `<a href="${sanitizeUrl(e.url)}" target="_blank" rel="noopener">${escapeHtml(e.shortName || e.name)} ↗</a>`
: escapeHtml(e.shortName || e.name);
return `<div class="domain-item">
<div class="domain-item-title">${title} ${badge}</div>
<div class="domain-item-meta">${escapeHtml(e.league)} · ${score}</div>
</div>`;
}
private render(events: SportEvent[]): void {
if (events.length === 0) {
this.setContent(`<div class="domain-panel"><div class="domain-empty">No events available right now</div></div>`);
return;
}
const rows = events.slice(0, 40).map((e) => this.renderEvent(e)).join('');
this.setContent(`
<div class="domain-panel">
<div class="domain-insight">${escapeHtml(sportsInsight(events))}</div>
<div class="domain-list">${rows}</div>
<div class="domain-footer"><span>ESPN</span><span>${new Date(this.lastFetch).toLocaleTimeString()}</span></div>
</div>
`);
}
}
-146
View File
@@ -1,146 +0,0 @@
import { Panel } from './Panel';
import { t } from '@/services/i18n';
import type { TechHubActivity } from '@/services/tech-activity';
import { escapeHtml, sanitizeUrl } from '@/utils/sanitize';
import { getCSSColor } from '@/utils';
const COUNTRY_FLAGS: Record<string, string> = {
'USA': '🇺🇸', 'United States': '🇺🇸',
'UK': '🇬🇧', 'United Kingdom': '🇬🇧',
'China': '🇨🇳',
'India': '🇮🇳',
'Israel': '🇮🇱',
'Germany': '🇩🇪',
'France': '🇫🇷',
'Canada': '🇨🇦',
'Japan': '🇯🇵',
'South Korea': '🇰🇷',
'Singapore': '🇸🇬',
'Australia': '🇦🇺',
'Netherlands': '🇳🇱',
'Sweden': '🇸🇪',
'Switzerland': '🇨🇭',
'Brazil': '🇧🇷',
'Indonesia': '🇮🇩',
'UAE': '🇦🇪',
'Estonia': '🇪🇪',
'Ireland': '🇮🇪',
'Finland': '🇫🇮',
'Spain': '🇪🇸',
'Italy': '🇮🇹',
'Poland': '🇵🇱',
'Mexico': '🇲🇽',
'Argentina': '🇦🇷',
'Chile': '🇨🇱',
'Colombia': '🇨🇴',
'Nigeria': '🇳🇬',
'Kenya': '🇰🇪',
'South Africa': '🇿🇦',
'Egypt': '🇪🇬',
'Taiwan': '🇹🇼',
'Vietnam': '🇻🇳',
'Thailand': '🇹🇭',
'Malaysia': '🇲🇾',
'Philippines': '🇵🇭',
'New Zealand': '🇳🇿',
'Austria': '🇦🇹',
'Belgium': '🇧🇪',
'Denmark': '🇩🇰',
'Norway': '🇳🇴',
'Portugal': '🇵🇹',
'Czech Republic': '🇨🇿',
'Romania': '🇷🇴',
'Ukraine': '🇺🇦',
'Russia': '🇷🇺',
'Turkey': '🇹🇷',
'Saudi Arabia': '🇸🇦',
'Qatar': '🇶🇦',
'Pakistan': '🇵🇰',
'Bangladesh': '🇧🇩',
};
export class TechHubsPanel extends Panel {
private activities: TechHubActivity[] = [];
private onHubClick?: (hub: TechHubActivity) => void;
constructor() {
super({
id: 'tech-hubs',
title: t('panels.techHubs'),
showCount: true,
infoTooltip: t('components.techHubs.infoTooltip', {
highColor: getCSSColor('--semantic-normal'),
elevatedColor: getCSSColor('--semantic-elevated'),
lowColor: getCSSColor('--text-dim'),
}),
});
}
public setOnHubClick(handler: (hub: TechHubActivity) => void): void {
this.onHubClick = handler;
}
public setActivities(activities: TechHubActivity[]): void {
this.activities = activities.slice(0, 10);
this.setCount(this.activities.length);
this.render();
}
private getFlag(country: string): string {
return COUNTRY_FLAGS[country] || '🌐';
}
private render(): void {
if (this.activities.length === 0) {
this.showError(t('common.noActiveTechHubs'));
return;
}
const html = this.activities.map((hub, index) => {
const trendIcon = hub.trend === 'rising' ? '↑' : hub.trend === 'falling' ? '↓' : '';
const breakingTag = hub.hasBreaking ? '<span class="hub-breaking">ALERT</span>' : '';
const topStory = hub.topStories[0];
return `
<div class="tech-hub-item ${hub.activityLevel}" data-hub-id="${escapeHtml(hub.hubId)}" data-index="${index}">
<div class="hub-rank">${index + 1}</div>
<span class="hub-indicator ${hub.activityLevel}"></span>
<div class="hub-info">
<div class="hub-header">
<span class="hub-name">${escapeHtml(hub.city)}</span>
<span class="hub-flag">${this.getFlag(hub.country)}</span>
${breakingTag}
</div>
<div class="hub-meta">
<span class="hub-news-count">${hub.newsCount} ${hub.newsCount === 1 ? 'story' : 'stories'}</span>
${trendIcon ? `<span class="hub-trend ${hub.trend}">${trendIcon}</span>` : ''}
<span class="hub-tier">${hub.tier}</span>
</div>
</div>
<div class="hub-score">${Math.round(hub.score)}</div>
</div>
${topStory ? `
<a class="hub-top-story" href="${sanitizeUrl(topStory.link)}" target="_blank" rel="noopener" data-hub-id="${escapeHtml(hub.hubId)}">
${escapeHtml(topStory.title.length > 80 ? topStory.title.slice(0, 77) + '...' : topStory.title)}
</a>
` : ''}
`;
}).join('');
this.setContent(html);
this.bindEvents();
}
private bindEvents(): void {
const items = this.content.querySelectorAll<HTMLDivElement>('.tech-hub-item');
items.forEach((item) => {
item.addEventListener('click', () => {
const hubId = item.dataset.hubId;
const hub = this.activities.find(a => a.hubId === hubId);
if (hub && this.onHubClick) {
this.onHubClick(hub);
}
});
});
}
}
-163
View File
@@ -1,163 +0,0 @@
import { h, Component } from 'preact';
export interface VerificationCheck {
id: string;
label: string;
checked: boolean;
icon: string;
}
export interface VerificationResult {
score: number; // 0-100
checks: VerificationCheck[];
verdict: 'verified' | 'likely' | 'uncertain' | 'unreliable';
notes: string[];
}
const VERIFICATION_TEMPLATE: VerificationCheck[] = [
{ id: 'recency', label: 'Recent timestamp confirmed', checked: false, icon: '🕐' },
{ id: 'geolocation', label: 'Location verified', checked: false, icon: '📍' },
{ id: 'source', label: 'Primary source identified', checked: false, icon: '📰' },
{ id: 'crossref', label: 'Cross-referenced with other sources', checked: false, icon: '🔗' },
{ id: 'no_ai', label: 'No AI generation artifacts', checked: false, icon: '🤖' },
{ id: 'no_recrop', label: 'Not recycled/old footage', checked: false, icon: '🔄' },
{ id: 'metadata', label: 'Metadata verified', checked: false, icon: '📋' },
{ id: 'context', label: 'Context established', checked: false, icon: '📖' },
];
export class VerificationChecklist extends Component {
private checks: VerificationCheck[] = VERIFICATION_TEMPLATE.map(c => ({ ...c }));
private notes: string[] = [];
private manualNote: string = '';
private toggleCheck(id: string): void {
this.checks = this.checks.map(c =>
c.id === id ? { ...c, checked: !c.checked } : c
);
this.setState({});
}
private addNote(): void {
if (this.manualNote.trim()) {
this.notes = [...this.notes, this.manualNote.trim()];
this.manualNote = '';
this.setState({});
}
}
private calculateResult(): VerificationResult {
const checkedCount = this.checks.filter(c => c.checked).length;
const score = Math.round((checkedCount / this.checks.length) * 100);
let verdict: VerificationResult['verdict'];
if (score >= 90) verdict = 'verified';
else if (score >= 70) verdict = 'likely';
else if (score >= 40) verdict = 'uncertain';
else verdict = 'unreliable';
return { score, checks: this.checks, verdict, notes: this.notes };
}
private reset(): void {
this.checks = VERIFICATION_TEMPLATE.map(c => ({ ...c }));
this.notes = [];
this.manualNote = '';
this.setState({});
}
render() {
const result = this.calculateResult();
const verdictColors: Record<string, string> = {
verified: '#22c55e',
likely: '#84cc16',
uncertain: '#eab308',
unreliable: '#ef4444',
};
const verdictLabels: Record<string, string> = {
verified: 'VERIFIED',
likely: 'LIKELY AUTHENTIC',
uncertain: 'UNCERTAIN',
unreliable: 'UNRELIABLE',
};
return h('div', { class: 'verification-checklist' },
h('div', { class: 'checklist-header' },
h('h3', null, 'Information Verification Checklist'),
h('p', { class: 'hint' }, 'Based on Bellingcat\'s OSH Framework'),
),
h('div', {
class: 'score-display',
style: `background-color: ${verdictColors[result.verdict]}20; border-color: ${verdictColors[result.verdict]}`,
},
h('div', { class: 'score-value' }, `${result.score}%`),
h('div', { class: 'score-label', style: `color: ${verdictColors[result.verdict]}` },
verdictLabels[result.verdict],
),
),
h('div', { class: 'checks-grid' },
...this.checks.map(check =>
h('label', { key: check.id, class: `check-item ${check.checked ? 'checked' : ''}` },
h('input', {
type: 'checkbox',
checked: check.checked,
onChange: () => this.toggleCheck(check.id),
}),
h('span', { class: 'icon' }, check.icon),
h('span', { class: 'label' }, check.label),
)
),
),
h('div', { class: 'notes-section' },
h('h4', null, 'Verification Notes'),
h('div', { class: 'notes-list' },
this.notes.length === 0
? h('p', { class: 'empty' }, 'No notes added')
: this.notes.map((note, i) =>
h('div', { key: i, class: 'note-item' }, `${note}`)
),
),
h('div', { class: 'add-note' },
h('input', {
type: 'text',
value: this.manualNote,
onInput: (e: Event) => { this.manualNote = (e.target as HTMLInputElement).value; },
placeholder: 'Add verification note...',
onKeyPress: (e: KeyboardEvent) => { if (e.key === 'Enter') this.addNote(); },
}),
h('button', { onClick: () => this.addNote() }, 'Add'),
),
),
h('div', { class: 'checklist-actions' },
h('button', { class: 'reset-btn', onClick: () => this.reset() }, 'Reset Checklist'),
),
h('style', null, `
.verification-checklist { background: var(--bg); border-radius: 8px; padding: 16px; max-width: 400px; }
.checklist-header h3 { margin: 0 0 4px; font-size: 14px; color: var(--accent); }
.hint { margin: 0; font-size: 11px; color: var(--text-muted); }
.score-display { margin: 16px 0; padding: 16px; border-radius: 8px; border: 2px solid; text-align: center; }
.score-value { font-size: 32px; font-weight: 700; color: var(--accent); }
.score-label { font-size: 12px; font-weight: 600; text-transform: uppercase; }
.checks-grid { display: flex; flex-direction: column; gap: 8px; margin: 16px 0; }
.check-item { display: flex; align-items: center; gap: 8px; padding: 8px; background: var(--surface-hover); border-radius: 4px; cursor: pointer; transition: background 0.2s; }
.check-item:hover { background: var(--border); }
.check-item.checked { background: color-mix(in srgb, var(--semantic-normal) 15%, var(--bg)); }
.check-item input { width: 16px; height: 16px; }
.icon { font-size: 14px; }
.label { font-size: 12px; color: var(--text); }
.notes-section { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); }
.notes-section h4 { margin: 0 0 8px; font-size: 12px; color: var(--text-dim); }
.notes-list { max-height: 100px; overflow-y: auto; }
.note-item { font-size: 11px; color: var(--text-faint); padding: 4px 0; }
.empty { font-size: 11px; color: var(--text-ghost); font-style: italic; }
.add-note { display: flex; gap: 8px; margin-top: 8px; }
.add-note input { flex: 1; padding: 6px 8px; background: var(--surface-hover); border: 1px solid var(--border-strong); border-radius: 4px; color: var(--text); font-size: 12px; }
.add-note button { padding: 6px 12px; background: var(--border-strong); border: none; border-radius: 4px; color: var(--accent); font-size: 12px; cursor: pointer; }
.checklist-actions { margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); }
.reset-btn { width: 100%; padding: 8px; background: var(--border); border: none; border-radius: 4px; color: var(--text-dim); font-size: 12px; cursor: pointer; }
.reset-btn:hover { background: var(--border-strong); color: var(--text-faint); }
`),
);
}
}
-86
View File
@@ -1,86 +0,0 @@
import { Panel } from './Panel';
import { escapeHtml, sanitizeUrl } from '@/utils/sanitize';
import { getGlobalWeatherData, type GlobalWeatherData, type SevereWeatherKind } from '@/services/global-weather';
import type { WorldFeedSeverity } from '@/services/world-feed';
const KIND_ICON: Record<SevereWeatherKind, string> = {
cyclone: '🌀',
flood: '🌊',
drought: '☀️',
wildfire: '🔥',
storm: '⛈️',
other: '⚠️',
};
const SEV_CLASS: Record<WorldFeedSeverity, string> = {
critical: 'sev-critical',
high: 'sev-high',
elevated: 'sev-elevated',
low: 'sev-low',
info: 'sev-info',
};
export class WeatherPanel extends Panel {
private loading = false;
private lastFetch = 0;
private readonly REFRESH_MS = 10 * 60 * 1000;
constructor() {
super({
id: 'weather',
title: 'Severe Weather',
showCount: true,
infoTooltip: 'Hanzo World Model — Weather lens: worldwide severe weather from GDACS (cyclones, floods, droughts, wildfires) plus US National Weather Service alerts.',
});
void this.refresh();
}
public async refresh(): Promise<void> {
if (this.loading) return;
if (this.lastFetch > 0 && Date.now() - this.lastFetch < this.REFRESH_MS) return;
this.loading = true;
try {
const data = await getGlobalWeatherData();
this.lastFetch = Date.now();
this.setCount(data.events.length);
this.setDataBadge(data.events.length > 0 ? 'live' : 'cached');
this.render(data);
} catch (e) {
console.error('[Weather] refresh failed:', e);
this.showError();
} finally {
this.loading = false;
}
}
private render(data: GlobalWeatherData): void {
if (data.events.length === 0) {
this.setContent(`
<div class="domain-panel">
<div class="domain-insight">${escapeHtml(data.insight)}</div>
<div class="domain-empty">No active severe-weather events</div>
</div>`);
return;
}
const rows = data.events.slice(0, 40).map((e) => {
const icon = KIND_ICON[e.kind] || '⚠️';
const date = e.time.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
const title = e.url
? `<a href="${sanitizeUrl(e.url)}" target="_blank" rel="noopener">${escapeHtml(e.title)} ↗</a>`
: escapeHtml(e.title);
return `<div class="domain-item">
<div class="domain-item-title">${icon} ${title} <span class="domain-tag ${SEV_CLASS[e.severity]}">${e.severity}</span></div>
<div class="domain-item-meta">${escapeHtml(e.region || e.kind)} · ${e.source} · ${date}</div>
</div>`;
}).join('');
this.setContent(`
<div class="domain-panel">
<div class="domain-insight">${escapeHtml(data.insight)}</div>
<div class="domain-list">${rows}</div>
<div class="domain-footer"><span>GDACS + NWS</span><span>${new Date(this.lastFetch).toLocaleTimeString()}</span></div>
</div>
`);
}
}
-6
View File
@@ -50,12 +50,6 @@ export * from './DisplacementPanel';
export * from './ClimateAnomalyPanel';
export * from './PopulationExposurePanel';
export * from './InvestmentsPanel';
export * from './RoboticsPanel';
export * from './QuantumPanel';
export * from './PostQuantumPanel';
export * from './WeatherPanel';
export * from './SportsPanel';
export * from './SpaceWeatherPanel';
export * from './LanguageSelector';
export { SentimentPanel } from './SentimentPanel';
export { TraderDeskPanel } from './TraderDeskPanel';
-7
View File
@@ -61,13 +61,6 @@ const FULL_PANELS: Record<string, PanelConfig> = {
displacement: { name: 'UNHCR Displacement', enabled: true, priority: 2 },
climate: { name: 'Climate Anomalies', enabled: true, priority: 2 },
'population-exposure': { name: 'Population Exposure', enabled: true, priority: 2 },
// Hanzo World Model domain lenses — realtime feeds for app-builders on hanzo.ai.
robotics: { name: 'Robotics', enabled: true, priority: 2 },
quantum: { name: 'Quantum Computing', enabled: true, priority: 2 },
'post-quantum': { name: 'Post-Quantum Readiness', enabled: true, priority: 2 },
weather: { name: 'Severe Weather', enabled: true, priority: 2 },
sports: { name: 'Sports & Events', enabled: true, priority: 2 },
'space-weather': { name: 'Space Weather', enabled: true, priority: 2 },
};
const FULL_MAP_LAYERS: MapLayers = {
-86
View File
@@ -1,86 +0,0 @@
// Curated Post-Quantum readiness reference data.
//
// Tracks the migration to quantum-resistant cryptography: NIST PQC standards,
// the NSA CNSA 2.0 timeline, and public deployment status across governments,
// clouds, browsers, messengers, and chains. Reference data (published facts),
// not a live feed. Lux/Hanzo are post-quantum-native — included as the
// reference implementation of a PQ-from-genesis stack.
export type PQStandardKind = 'kem' | 'signature' | 'hash-signature';
export type PQStandardStatus = 'standardized' | 'draft' | 'selected';
export interface PQStandard {
id: string; // FIPS number
name: string; // ML-KEM, ML-DSA, ...
basedOn: string; // Kyber, Dilithium, ...
kind: PQStandardKind;
status: PQStandardStatus;
year: number;
note: string;
}
// NIST post-quantum standards (FIPS 203/204/205 finalized Aug 2024).
export const PQC_STANDARDS: PQStandard[] = [
{ id: 'FIPS 203', name: 'ML-KEM', basedOn: 'CRYSTALS-Kyber', kind: 'kem', status: 'standardized', year: 2024, note: 'Module-lattice key encapsulation — the primary PQ KEM.' },
{ id: 'FIPS 204', name: 'ML-DSA', basedOn: 'CRYSTALS-Dilithium', kind: 'signature', status: 'standardized', year: 2024, note: 'Module-lattice signatures — the primary PQ signature.' },
{ id: 'FIPS 205', name: 'SLH-DSA', basedOn: 'SPHINCS+', kind: 'hash-signature', status: 'standardized', year: 2024, note: 'Stateless hash-based signatures — conservative backup.' },
{ id: 'FIPS 206', name: 'FN-DSA', basedOn: 'Falcon', kind: 'signature', status: 'draft', year: 2025, note: 'Compact lattice signatures — draft in progress.' },
{ id: 'HQC', name: 'HQC-KEM', basedOn: 'HQC (code-based)', kind: 'kem', status: 'selected', year: 2025, note: 'Selected as a code-based KEM backup to ML-KEM.' },
];
// NSA CNSA 2.0 migration timeline (target years for US National Security Systems).
export interface CNSAMilestone {
year: number;
milestone: string;
}
export const CNSA2_TIMELINE: CNSAMilestone[] = [
{ year: 2025, milestone: 'PQC support required in new software/firmware signing.' },
{ year: 2027, milestone: 'PQC becomes default for new networking equipment.' },
{ year: 2030, milestone: 'PQC default across most NSS deployments.' },
{ year: 2033, milestone: 'Exclusive PQC; classical (RSA/ECC) deprecated for NSS.' },
];
export type PQOrgType = 'government' | 'cloud' | 'browser' | 'messaging' | 'blockchain' | 'standard';
// pq-native = built PQ from the start; deployed = shipping in production;
// in-progress = rolling out; planned = committed; lagging = no public plan.
export type PQReadinessStatus = 'pq-native' | 'deployed' | 'in-progress' | 'planned' | 'lagging';
export interface PQReadiness {
id: string;
org: string;
type: PQOrgType;
status: PQReadinessStatus;
algorithms: string[];
detail: string;
asOf: number;
}
export const PQ_READINESS: PQReadiness[] = [
{ id: 'lux', org: 'Lux Network', type: 'blockchain', status: 'pq-native', algorithms: ['ML-DSA-65', 'Pulsar (Ring-LWE)', 'BLS'], detail: 'Post-quantum from genesis: Quasar consensus triple-seals with BLS + Pulsar threshold + ML-DSA-65. No harvest-now-decrypt-later exposure.', asOf: 2026 },
{ id: 'hanzo', org: 'Hanzo AI', type: 'blockchain', status: 'pq-native', algorithms: ['ML-DSA-65', 'ML-KEM'], detail: 'PQ-native infrastructure and identity; ships quantum-resistant primitives across the stack.', asOf: 2026 },
{ id: 'nsa-cnsa2', org: 'US Government (NSA CNSA 2.0)', type: 'government', status: 'in-progress', algorithms: ['ML-KEM', 'ML-DSA', 'SLH-DSA'], detail: 'CNSA 2.0 mandates PQC across National Security Systems by 2033.', asOf: 2025 },
{ id: 'cloudflare', org: 'Cloudflare', type: 'cloud', status: 'deployed', algorithms: ['X25519+ML-KEM'], detail: 'Hybrid post-quantum key agreement live for a large share of TLS traffic.', asOf: 2024 },
{ id: 'google-chrome', org: 'Google Chrome', type: 'browser', status: 'deployed', algorithms: ['X25519+ML-KEM'], detail: 'Hybrid ML-KEM enabled by default in TLS.', asOf: 2024 },
{ id: 'apple-imessage', org: 'Apple iMessage (PQ3)', type: 'messaging', status: 'deployed', algorithms: ['ML-KEM'], detail: 'PQ3 protocol brings level-3 post-quantum security with ongoing rekeying.', asOf: 2024 },
{ id: 'signal', org: 'Signal (PQXDH)', type: 'messaging', status: 'deployed', algorithms: ['X25519+ML-KEM'], detail: 'PQXDH adds post-quantum protection to the initial key exchange.', asOf: 2023 },
{ id: 'aws', org: 'AWS', type: 'cloud', status: 'in-progress', algorithms: ['ML-KEM'], detail: 'Hybrid PQ TLS across KMS/ACM and s2n-tls endpoints.', asOf: 2024 },
{ id: 'microsoft', org: 'Microsoft', type: 'cloud', status: 'in-progress', algorithms: ['ML-KEM', 'ML-DSA'], detail: 'SymCrypt PQC and Windows/TLS integration rolling out.', asOf: 2025 },
{ id: 'nist', org: 'NIST', type: 'standard', status: 'deployed', algorithms: ['ML-KEM', 'ML-DSA', 'SLH-DSA'], detail: 'Published FIPS 203/204/205; FN-DSA + HQC in the pipeline.', asOf: 2024 },
];
// The core threat driving urgency: encrypted data captured today can be
// decrypted once a cryptographically-relevant quantum computer exists.
export const HARVEST_NOW_DECRYPT_LATER =
'Adversaries harvest encrypted traffic today to decrypt once a cryptographically-relevant quantum computer arrives. Data with a long secrecy lifetime must migrate to PQC now.';
export function pqReadinessRank(status: PQReadinessStatus): number {
switch (status) {
case 'pq-native': return 5;
case 'deployed': return 4;
case 'in-progress': return 3;
case 'planned': return 2;
case 'lagging': return 1;
default: return 0;
}
}
-94
View File
@@ -1,94 +0,0 @@
import {
PQC_STANDARDS,
CNSA2_TIMELINE,
PQ_READINESS,
HARVEST_NOW_DECRYPT_LATER,
pqReadinessRank,
type PQStandard,
type PQReadiness,
type CNSAMilestone,
} from '@/config/post-quantum';
import { registerWorldFeed, type WorldFeed, type WorldFeedItem, type WorldFeedSeverity } from './world-feed';
export interface PostQuantumData {
standards: PQStandard[];
timeline: CNSAMilestone[];
readiness: PQReadiness[];
harvestNote: string;
insight: string;
updatedAt: Date;
}
function computeInsight(readiness: PQReadiness[]): string {
const ready = readiness.filter((r) => r.status === 'pq-native' || r.status === 'deployed').length;
const standardized = PQC_STANDARDS.filter((s) => s.status === 'standardized').length;
const nowYear = new Date().getFullYear();
const nextMilestone = CNSA2_TIMELINE.find((m) => m.year >= nowYear);
const countdown = nextMilestone ? `CNSA 2.0 ${nextMilestone.year} (${nextMilestone.year - nowYear}y)` : 'CNSA 2.0 complete';
return `${standardized} NIST standards live · ${ready}/${readiness.length} orgs PQ-ready · next ${countdown}`;
}
export function getPostQuantumData(): PostQuantumData {
const readiness = [...PQ_READINESS].sort((a, b) => pqReadinessRank(b.status) - pqReadinessRank(a.status));
return {
standards: PQC_STANDARDS,
timeline: CNSA2_TIMELINE,
readiness,
harvestNote: HARVEST_NOW_DECRYPT_LATER,
insight: computeInsight(readiness),
updatedAt: new Date(),
};
}
function readinessSeverity(r: PQReadiness): WorldFeedSeverity {
switch (r.status) {
case 'pq-native': return 'info';
case 'deployed': return 'low';
case 'in-progress': return 'elevated';
case 'planned': return 'high';
default: return 'critical';
}
}
async function buildFeed(): Promise<WorldFeed> {
const data = getPostQuantumData();
const standardItems: WorldFeedItem[] = data.standards.map((s) => ({
id: `pq:standard:${s.id}`,
title: `${s.id} · ${s.name} (${s.basedOn})`,
summary: s.note,
category: 'standard',
tags: [s.kind, s.status],
meta: { year: s.year },
}));
const readinessItems: WorldFeedItem[] = data.readiness.map((r) => ({
id: `pq:readiness:${r.id}`,
title: `${r.org}${r.status}`,
summary: r.detail,
category: r.type,
severity: readinessSeverity(r),
tags: r.algorithms,
meta: { asOf: r.asOf },
}));
const timelineItems: WorldFeedItem[] = data.timeline.map((m) => ({
id: `pq:cnsa2:${m.year}`,
title: `CNSA 2.0 · ${m.year}`,
summary: m.milestone,
category: 'timeline',
timestamp: new Date(Date.UTC(m.year, 0, 1)).toISOString(),
}));
return {
domain: 'post-quantum',
label: 'Post-Quantum Readiness',
updatedAt: data.updatedAt.toISOString(),
source: 'NIST PQC (FIPS 203/204/205) + NSA CNSA 2.0 + curated deployment tracker',
live: false,
insight: data.insight,
items: [...standardItems, ...readinessItems, ...timelineItems],
};
}
export function getPostQuantumFeed(): Promise<WorldFeed> {
return buildFeed();
}
registerWorldFeed('post-quantum', getPostQuantumFeed);
-78
View File
@@ -1,78 +0,0 @@
import { fetchArxivPapers, getArxivStatus, type ArxivPaper } from './arxiv';
import { QUANTUM_PLAYERS, type QuantumPlayer } from '@/config/quantum';
import { registerWorldFeed, type WorldFeed, type WorldFeedItem } from './world-feed';
export interface QuantumData {
papers: ArxivPaper[];
players: QuantumPlayer[];
insight: string;
updatedAt: Date;
}
function topScale(players: QuantumPlayer[]): QuantumPlayer | null {
return players
.filter((p) => p.qubits !== null)
.sort((a, b) => (b.qubits ?? 0) - (a.qubits ?? 0))[0] ?? null;
}
function computeInsight(papers: QuantumData['papers']): string {
const leader = topScale(QUANTUM_PLAYERS);
const scale = leader ? `${leader.qubits!.toLocaleString()} qubits (${leader.name})` : 'multiple architectures';
const modalities = new Set(QUANTUM_PLAYERS.map((p) => p.modality)).size;
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
const recent = papers.filter((p) => p.published.getTime() >= weekAgo).length;
return `Max announced: ${scale} · ${modalities} modalities · ${recent} new quant-ph papers/wk`;
}
export async function getQuantumData(): Promise<QuantumData> {
const papers = await fetchArxivPapers('quant-ph', 30);
return {
papers,
players: QUANTUM_PLAYERS,
insight: computeInsight(papers),
updatedAt: new Date(),
};
}
async function buildFeed(): Promise<WorldFeed> {
const data = await getQuantumData();
const paperItems: WorldFeedItem[] = data.papers.slice(0, 20).map((p) => ({
id: `quantum:paper:${p.id}`,
title: p.title,
summary: p.summary.slice(0, 400),
category: 'research',
url: p.link,
timestamp: p.published.toISOString(),
tags: p.categories,
}));
const playerItems: WorldFeedItem[] = data.players.map((p) => ({
id: `quantum:player:${p.id}`,
title: p.name,
summary: `${p.metric}${p.modality} (${p.city}, as of ${p.asOf})`,
category: p.modality,
url: p.url,
lat: p.lat,
lon: p.lon,
tags: [p.country, p.modality],
meta: { qubits: p.qubits, asOf: p.asOf, milestone: p.milestone },
}));
return {
domain: 'quantum',
label: 'Quantum Computing',
updatedAt: data.updatedAt.toISOString(),
source: 'arXiv quant-ph + curated hardware registry',
live: data.papers.length > 0,
insight: data.insight,
items: [...paperItems, ...playerItems],
};
}
export function getQuantumFeed(): Promise<WorldFeed> {
return buildFeed();
}
export function getQuantumStatus(): string {
return getArxivStatus('quant-ph');
}
registerWorldFeed('quantum', getQuantumFeed);
-76
View File
@@ -1,76 +0,0 @@
import { fetchArxivPapers, getArxivStatus, type ArxivPaper } from './arxiv';
import { ROBOTICS_ORGS, type RoboticsOrg } from '@/config/robotics';
import { registerWorldFeed, type WorldFeed, type WorldFeedItem } from './world-feed';
export interface RoboticsData {
papers: ArxivPaper[];
orgs: RoboticsOrg[];
insight: string;
updatedAt: Date;
}
const HUMANOID_TERMS = ['humanoid', 'bipedal', 'manipulation', 'dexterous', 'locomotion', 'whole-body'];
function computeInsight(papers: ArxivPaper[]): string {
if (papers.length === 0) return `${ROBOTICS_ORGS.length} labs tracked · research feed unavailable`;
const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
const recent = papers.filter((p) => p.published.getTime() >= weekAgo).length;
const humanoid = papers.filter((p) => {
const t = `${p.title} ${p.summary}`.toLowerCase();
return HUMANOID_TERMS.some((term) => t.includes(term));
}).length;
const humanoidOrgs = ROBOTICS_ORGS.filter((o) => o.category === 'humanoid').length;
return `${recent} new papers this week · ${humanoid}/${papers.length} on embodied/humanoid · ${humanoidOrgs} humanoid labs`;
}
export async function getRoboticsData(): Promise<RoboticsData> {
const papers = await fetchArxivPapers('cs.RO', 30);
return {
papers,
orgs: ROBOTICS_ORGS,
insight: computeInsight(papers),
updatedAt: new Date(),
};
}
async function buildFeed(): Promise<WorldFeed> {
const data = await getRoboticsData();
const paperItems: WorldFeedItem[] = data.papers.slice(0, 20).map((p) => ({
id: `robotics:paper:${p.id}`,
title: p.title,
summary: p.summary.slice(0, 400),
category: 'research',
url: p.link,
timestamp: p.published.toISOString(),
tags: p.categories,
}));
const orgItems: WorldFeedItem[] = data.orgs.map((o) => ({
id: `robotics:org:${o.id}`,
title: o.name,
summary: o.focus,
category: o.category,
url: o.url,
lat: o.lat,
lon: o.lon,
tags: [o.country, o.category],
}));
return {
domain: 'robotics',
label: 'Robotics',
updatedAt: data.updatedAt.toISOString(),
source: 'arXiv cs.RO + curated robotics registry',
live: data.papers.length > 0,
insight: data.insight,
items: [...paperItems, ...orgItems],
};
}
export function getRoboticsFeed(): Promise<WorldFeed> {
return buildFeed();
}
export function getRoboticsStatus(): string {
return getArxivStatus('cs.RO');
}
registerWorldFeed('robotics', getRoboticsFeed);
-176
View File
@@ -1,176 +0,0 @@
import { createCircuitBreaker } from '@/utils';
import { registerWorldFeed, type WorldFeed, type WorldFeedItem, type WorldFeedSeverity } from './world-feed';
// NOAA Space Weather Prediction Center — free, no key, CORS-enabled.
// Geomagnetic conditions affect satellites, GPS, power grids, aviation and HF
// comms: a genuinely global world-model signal.
const KP_URL = 'https://services.swpc.noaa.gov/products/noaa-planetary-k-index.json';
const ALERTS_URL = 'https://services.swpc.noaa.gov/products/alerts.json';
export interface SpaceWeatherAlert {
id: string;
message: string;
issued: Date;
}
export interface SpaceWeatherState {
kp: number | null;
kpTime: Date | null;
stormLevel: string; // 'Quiet', 'G1 (Minor)', ... 'G5 (Extreme)'
alerts: SpaceWeatherAlert[];
insight: string;
updatedAt: Date;
}
const breaker = createCircuitBreaker<SpaceWeatherState>({ name: 'NOAA Space Weather', cacheTtlMs: 5 * 60 * 1000 });
function stormLevel(kp: number | null): string {
if (kp === null) return 'Unknown';
if (kp >= 9) return 'G5 (Extreme)';
if (kp >= 8) return 'G4 (Severe)';
if (kp >= 7) return 'G3 (Strong)';
if (kp >= 6) return 'G2 (Moderate)';
if (kp >= 5) return 'G1 (Minor)';
if (kp >= 4) return 'Unsettled';
return 'Quiet';
}
function kpSeverity(kp: number | null): WorldFeedSeverity {
if (kp === null) return 'info';
if (kp >= 8) return 'critical';
if (kp >= 7) return 'high';
if (kp >= 5) return 'elevated';
if (kp >= 4) return 'low';
return 'info';
}
function toUtcDate(raw: unknown): Date | null {
if (!raw) return null;
const s = String(raw).replace(' ', 'T');
return new Date(s.endsWith('Z') ? s : s + 'Z');
}
function parseKp(raw: unknown): { kp: number | null; time: Date | null } {
if (!Array.isArray(raw) || raw.length === 0) return { kp: null, time: null };
const last = raw[raw.length - 1];
let kpRaw: unknown;
let timeRaw: unknown;
if (Array.isArray(last)) {
// Legacy [time_tag, Kp, a_running, station_count] rows (with header row).
timeRaw = last[0];
kpRaw = last[1];
} else if (last && typeof last === 'object') {
const rec = last as { Kp?: unknown; kp?: unknown; kp_index?: unknown; time_tag?: unknown };
kpRaw = rec.Kp ?? rec.kp ?? rec.kp_index;
timeRaw = rec.time_tag;
} else {
return { kp: null, time: null };
}
const kp = Number(kpRaw);
return { kp: Number.isFinite(kp) ? kp : null, time: toUtcDate(timeRaw) };
}
const DESCRIPTIVE = /^(ALERT|WARNING|WATCH|SUMMARY|EXTENDED WARNING|CANCEL WARNING|CONTINUED ALERT):/i;
function parseAlerts(raw: unknown): SpaceWeatherAlert[] {
if (!Array.isArray(raw)) return [];
const wanted = /ALERT|WARNING|WATCH/i;
const out: SpaceWeatherAlert[] = [];
for (const item of raw) {
if (!item || typeof item !== 'object') continue;
const rec = item as { product_id?: string; issue_datetime?: string; message?: string };
const message = String(rec.message ?? '').trim();
if (!message || !wanted.test(message)) continue;
// Prefer the descriptive line (e.g. "ALERT: Geomagnetic K-index of 5");
// the leading "Space Weather Message Code: ..." line is not human-friendly.
const lines = message.split('\n').map((l) => l.trim()).filter(Boolean);
const headline = lines.find((l) => DESCRIPTIVE.test(l)) ?? lines[0] ?? message;
out.push({
id: `swpc:${rec.product_id ?? headline}:${rec.issue_datetime ?? ''}`,
message: headline.slice(0, 160),
issued: toUtcDate(rec.issue_datetime) ?? new Date(),
});
}
const sorted = out.sort((a, b) => b.issued.getTime() - a.issued.getTime());
const seen = new Set<string>();
const deduped: SpaceWeatherAlert[] = [];
for (const a of sorted) {
if (seen.has(a.message)) continue;
seen.add(a.message);
deduped.push(a);
if (deduped.length >= 12) break;
}
return deduped;
}
export async function fetchSpaceWeather(): Promise<SpaceWeatherState> {
return breaker.execute(async () => {
const [kpRes, alertsRes] = await Promise.allSettled([
fetch(KP_URL, { headers: { Accept: 'application/json' } }).then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}),
fetch(ALERTS_URL, { headers: { Accept: 'application/json' } }).then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
}),
]);
if (kpRes.status !== 'fulfilled' && alertsRes.status !== 'fulfilled') {
throw new Error('space weather sources unavailable');
}
const { kp, time } = kpRes.status === 'fulfilled' ? parseKp(kpRes.value) : { kp: null, time: null };
const alerts = alertsRes.status === 'fulfilled' ? parseAlerts(alertsRes.value) : [];
const level = stormLevel(kp);
return {
kp,
kpTime: time,
stormLevel: level,
alerts,
insight: `Kp ${kp ?? '—'} · ${level} · ${alerts.length} active advisories`,
updatedAt: new Date(),
};
}, { kp: null, kpTime: null, stormLevel: 'Unknown', alerts: [], insight: 'unavailable', updatedAt: new Date() });
}
async function buildFeed(): Promise<WorldFeed> {
const state = await fetchSpaceWeather();
const items: WorldFeedItem[] = [
{
id: 'space-weather:kp',
title: `Planetary Kp ${state.kp ?? '—'}${state.stormLevel}`,
summary: 'Global geomagnetic activity index (NOAA SWPC).',
category: 'geomagnetic',
severity: kpSeverity(state.kp),
timestamp: (state.kpTime ?? state.updatedAt).toISOString(),
},
...state.alerts.map((a) => ({
id: `space-weather:${a.id}`,
title: a.message,
category: 'advisory',
severity: 'elevated' as WorldFeedSeverity,
timestamp: a.issued.toISOString(),
})),
];
return {
domain: 'space-weather',
label: 'Space Weather',
updatedAt: state.updatedAt.toISOString(),
source: 'NOAA SWPC (planetary K-index + alerts)',
live: state.kp !== null || state.alerts.length > 0,
insight: state.insight,
items,
};
}
export function getSpaceWeatherFeed(): Promise<WorldFeed> {
return buildFeed();
}
export function getSpaceWeatherStatus(): string {
return breaker.getStatus();
}
registerWorldFeed('space-weather', getSpaceWeatherFeed);

Some files were not shown because too many files have changed in this diff Show More