diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000000..3a6459a523 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,34 @@ +# Context: WorldMonitor + +WorldMonitor is a real-time global intelligence dashboard that fuses public and self-hostable data streams into a situational awareness interface. + +## Canonical terms + +### Sensor Fusion Deck + +A panel-level overview of which geospatial streams are currently fused into the dashboard and which are only roadmap lanes. + +_Avoid_: calling it "Palantir" or "panopticon" in product UI. Those are inspiration/reference points, not product claims. + +### Public OSINT feed + +A data source that can be used with public attribution, documented provenance, and no hidden proprietary collection assumption. + +_Avoid_: implying covert, private, or unconsented collection. + +### Sparse 3D reconstruction + +A roadmap capability for using sparse, provenance-reviewed imagery to add local 3D context. It is not an active surveillance feature. + +_Avoid_: "God's eye view" in operator-facing UI except in research notes. + +### WorldView-style layer + +A visual/data-layer pattern inspired by Bilawal Sidhu's WorldView demo: 3D world shell plus satellite, aircraft, webcam, seismic, traffic, and other public data streams. + +_Avoid_: claiming parity with Google Earth, Palantir, or proprietary data fusion systems. + +## Example dialogue + +- User: "Show me what sources are live." +- App: "Sensor Fusion Deck shows 5/9 layers live, 8 tracked objects, and the reconstruction lane still planned." diff --git a/docs/references/bilawal-worldview-sensor-fusion.md b/docs/references/bilawal-worldview-sensor-fusion.md new file mode 100644 index 0000000000..1535923ee0 --- /dev/null +++ b/docs/references/bilawal-worldview-sensor-fusion.md @@ -0,0 +1,37 @@ +# Bilawal WorldView and internet-3D notes + +Source videos reviewed during the Sensor Fusion Deck update: + +- `KWXuxfdZhwk` — **The Internet's Hidden 3D Model of the World** +- `rXvU7bPJ8n4` — **Ex-Google Maps PM Vibe Coded Palantir In a Weekend (Palantir Noticed)** + +## Takeaways for WorldMonitor + +### WorldView pattern + +The second video describes a weekend-built geospatial dashboard combining: + +- 3D globe / 3D tiles base map +- satellite tracking +- military and commercial flight data +- live CCTV / webcam feeds +- street traffic simulation +- seismic/earthquake data +- visual modes such as CRT, night vision, and FLIR +- parallel AI-agent implementation workflow + +WorldMonitor already has many equivalent public-data lanes: 3D globe, deck.gl map, military flights/vessels, aircraft positions, seismic activity, thermal escalation, satellite/imagery footprints, webcams, weather, and many correlation panels. + +### Internet 3D reconstruction pattern + +The first video follows the arc from Structure-from-Motion and NeRFs to 3D Gaussian Splatting, VGGT, π³, and MegaDepth-X. The relevant product insight is not to ingest random private images. The useful lane is a provenance-reviewed research/asset pipeline for sparse 3D context: + +1. public/owned imagery only; +2. explicit source attribution; +3. no private-person tracking; +4. separate roadmap status until privacy and provenance rules are documented; +5. local-first processing where possible. + +## Implementation decision + +Added a **Sensor Fusion Deck** instead of jumping straight to a 3D reconstruction feature. It gives operators a live inventory of what streams are fused now and marks sparse 3D reconstruction as planned/guardrailed. diff --git a/src/app/panel-layout.ts b/src/app/panel-layout.ts index 3a442ebd3c..d1b789eb62 100644 --- a/src/app/panel-layout.ts +++ b/src/app/panel-layout.ts @@ -37,6 +37,7 @@ import { TechEventsPanel, ServiceStatusPanel, InternetDisruptionsPanel, + SensorFusionPanel, RuntimeConfigPanel, InsightsPanel, MacroSignalsPanel, @@ -1085,6 +1086,10 @@ export class PanelLayoutManager implements AppModule { } this.createPanel('cascade', () => new CascadePanel()); + this.createPanel('sensor-fusion', () => new SensorFusionPanel(() => ({ + cache: this.ctx.intelligenceCache, + mapLayers: this.ctx.mapLayers, + }))); this.createPanel('satellite-fires', () => new SatelliteFiresPanel()); this.createPanel('defense-patents', () => new DefensePatentsPanel()); diff --git a/src/components/SensorFusionPanel.ts b/src/components/SensorFusionPanel.ts new file mode 100644 index 0000000000..f08a10bcbc --- /dev/null +++ b/src/components/SensorFusionPanel.ts @@ -0,0 +1,88 @@ +import type { IntelligenceCache } from '@/app/app-context'; +import type { MapLayers } from '@/types'; +import { buildSensorFusionSnapshot, type SensorFusionLayer, type SensorFusionStatus } from '@/services/sensor-fusion'; +import { h, replaceChildren } from '@/utils/dom-utils'; +import { Panel } from './Panel'; + +type SnapshotProvider = () => { + cache: IntelligenceCache; + mapLayers: Partial; +}; + +const STATUS_LABELS: Record = { + live: 'LIVE', + ready: 'READY', + available: 'DATA', + planned: 'ROADMAP', +}; + +export class SensorFusionPanel extends Panel { + private refreshTimer: ReturnType | null = null; + + constructor(private readonly getSnapshotInput: SnapshotProvider) { + super({ + id: 'sensor-fusion', + title: 'Sensor Fusion Deck', + showCount: true, + infoTooltip: 'WorldView-inspired overview of which public, attribution-friendly geospatial streams are currently fused into the dashboard, and which 3D reconstruction lanes remain roadmap-only.', + }); + this.element.classList.add('sensor-fusion-panel'); + this.render(); + this.refreshTimer = setInterval(() => this.refresh(), 30_000); + } + + public refresh(): void { + if (!this.element?.isConnected) return; + this.render(); + } + + public override destroy(): void { + if (this.refreshTimer) { + clearInterval(this.refreshTimer); + this.refreshTimer = null; + } + super.destroy(); + } + + protected render(): void { + const { cache, mapLayers } = this.getSnapshotInput(); + const snapshot = buildSensorFusionSnapshot(cache, mapLayers); + this.setCount(snapshot.liveLayers); + this.setDataBadge(snapshot.liveLayers > 0 ? 'live' : 'cached', `${snapshot.liveLayers}/${snapshot.layers.length} live`); + + replaceChildren(this.content, + h('div', { className: 'sensor-fusion-summary' }, + this.metric('Live layers', String(snapshot.liveLayers)), + this.metric('Available', String(snapshot.availableLayers)), + this.metric('Tracked objects', snapshot.trackedObjects.toLocaleString()), + ), + h('div', { className: 'sensor-fusion-grid' }, + ...snapshot.layers.map(layer => this.layerCard(layer)), + ), + h('div', { className: 'sensor-fusion-guardrail' }, + h('strong', null, 'Guardrail: '), + 'prioritize public, consent-aware, attributable feeds. Sparse 3D reconstruction stays a research/asset-review lane until provenance and privacy rules are explicit.', + ), + ); + } + + private metric(label: string, value: string): HTMLElement { + return h('div', { className: 'sensor-fusion-metric' }, + h('span', { className: 'sensor-fusion-metric-value' }, value), + h('span', { className: 'sensor-fusion-metric-label' }, label), + ); + } + + private layerCard(layer: SensorFusionLayer): HTMLElement { + const count = layer.count === null ? '—' : layer.count.toLocaleString(); + return h('div', { className: `sensor-fusion-layer ${layer.status}` }, + h('div', { className: 'sensor-fusion-layer-head' }, + h('span', { className: 'sensor-fusion-layer-label' }, layer.label), + h('span', { className: `sensor-fusion-status ${layer.status}` }, STATUS_LABELS[layer.status]), + ), + h('div', { className: 'sensor-fusion-source' }, layer.source), + h('div', { className: 'sensor-fusion-layer-count' }, count), + h('p', { className: 'sensor-fusion-note' }, layer.note), + ); + } +} diff --git a/src/components/index.ts b/src/components/index.ts index d84553731e..6b3ac8e7ee 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -38,6 +38,7 @@ export * from './IntelligenceGapBadge'; export * from './TechEventsPanel'; export * from './ServiceStatusPanel'; export * from './InternetDisruptionsPanel'; +export * from './SensorFusionPanel'; export * from './RuntimeConfigPanel'; export * from './InsightsPanel'; export * from './TechReadinessPanel'; diff --git a/src/config/panels.ts b/src/config/panels.ts index cea3f4b9ab..1c0c124161 100644 --- a/src/config/panels.ts +++ b/src/config/panels.ts @@ -27,6 +27,7 @@ const FULL_PANELS: Record = { intel: { name: 'Intel Feed', enabled: true, priority: 1 }, 'gdelt-intel': { name: 'Live Intelligence', enabled: true, priority: 1, ...(_desktop && { premium: 'enhanced' as const }) }, cascade: { name: 'Infrastructure Cascade', enabled: true, priority: 1 }, + 'sensor-fusion': { name: 'Sensor Fusion Deck', enabled: true, priority: 1 }, 'military-correlation': { name: 'Force Posture', enabled: true, priority: 2 }, 'escalation-correlation': { name: 'Escalation Monitor', enabled: true, priority: 2 }, 'economic-correlation': { name: 'Economic Warfare', enabled: true, priority: 2 }, diff --git a/src/services/sensor-fusion.ts b/src/services/sensor-fusion.ts new file mode 100644 index 0000000000..a5dff7f92e --- /dev/null +++ b/src/services/sensor-fusion.ts @@ -0,0 +1,138 @@ +import type { IntelligenceCache } from '@/app/app-context'; +import type { MapLayers } from '@/types'; + +export type SensorFusionLayerId = + | 'globe' + | 'satellites' + | 'aircraft' + | 'maritime' + | 'webcams' + | 'seismic' + | 'thermal' + | 'weather' + | '3d-reconstruction'; + +export type SensorFusionStatus = 'live' | 'ready' | 'available' | 'planned'; + +export interface SensorFusionLayer { + id: SensorFusionLayerId; + label: string; + source: string; + status: SensorFusionStatus; + count: number | null; + note: string; +} + +export interface SensorFusionSnapshot { + liveLayers: number; + availableLayers: number; + trackedObjects: number; + layers: SensorFusionLayer[]; + gaps: string[]; +} + +function countArray(value: unknown): number { + return Array.isArray(value) ? value.length : 0; +} + +function statusFromLayer(enabled: boolean | undefined, hasData: boolean): SensorFusionStatus { + if (enabled && hasData) return 'live'; + if (enabled) return 'ready'; + if (hasData) return 'available'; + return 'planned'; +} + +export function buildSensorFusionSnapshot( + cache: IntelligenceCache = {}, + mapLayers: Partial = {}, +): SensorFusionSnapshot { + const aircraftCount = countArray(cache.aircraftPositions) + countArray(cache.military?.flights); + const maritimeCount = countArray(cache.military?.vessels); + const seismicCount = countArray(cache.earthquakes); + const thermalCount = countArray(cache.thermalEscalation?.clusters); + const imageryCount = countArray(cache.imageryScenes); + + const layers: SensorFusionLayer[] = [ + { + id: 'globe', + label: '3D globe shell', + source: 'globe.gl / deck.gl map engines', + status: 'live', + count: null, + note: 'Base geospatial canvas for fused situational awareness.', + }, + { + id: 'aircraft', + label: 'Aircraft tracks', + source: 'commercial + military aviation feeds', + status: statusFromLayer(mapLayers.flights || mapLayers.military, aircraftCount > 0), + count: aircraftCount, + note: 'WorldView-style ADS-B layer for motion over the terrain.', + }, + { + id: 'maritime', + label: 'Maritime posture', + source: 'AIS / naval intelligence feeds', + status: statusFromLayer(mapLayers.ais || mapLayers.military, maritimeCount > 0), + count: maritimeCount, + note: 'Vessel layer for chokepoints, ports, and regional convergence.', + }, + { + id: 'satellites', + label: 'Satellites + imagery', + source: 'orbital tracks / imagery scene footprints', + status: statusFromLayer(mapLayers.satellites, imageryCount > 0), + count: imageryCount, + note: 'Imagery footprints are ready; orbital object counts depend on the satellite layer feed.', + }, + { + id: 'webcams', + label: 'Live webcams', + source: 'public webcam panels', + status: statusFromLayer(mapLayers.webcams, false), + count: null, + note: 'Public camera feeds belong here, with explicit source attribution and privacy guardrails.', + }, + { + id: 'seismic', + label: 'Seismic activity', + source: 'USGS / earthquake feed', + status: statusFromLayer(mapLayers.natural, seismicCount > 0), + count: seismicCount, + note: 'Earthquake points provide the fast disaster layer from the WorldView demo.', + }, + { + id: 'thermal', + label: 'Thermal escalation', + source: 'FIRMS-derived fire and thermal anomaly models', + status: statusFromLayer(mapLayers.fires || mapLayers.natural, thermalCount > 0), + count: thermalCount, + note: 'Conflict-adjacent heat signatures bridge remote sensing and strategic risk.', + }, + { + id: 'weather', + label: 'Weather / atmosphere', + source: 'weather and radar layers', + status: statusFromLayer(mapLayers.weather, false), + count: null, + note: 'Atmospheric context improves interpretation of aviation, maritime, and disaster layers.', + }, + { + id: '3d-reconstruction', + label: 'Sparse 3D reconstruction', + source: 'MegaDepth-X / VGGT / 3DGS research path', + status: 'planned', + count: null, + note: 'Roadmap lane for turning sparse public imagery into local 3D context, not active surveillance.', + }, + ]; + + const liveLayers = layers.filter(layer => layer.status === 'live').length; + const availableLayers = layers.filter(layer => layer.status !== 'planned').length; + const trackedObjects = layers.reduce((sum, layer) => sum + (layer.count ?? 0), 0); + const gaps = layers + .filter(layer => layer.status === 'planned') + .map(layer => layer.label); + + return { liveLayers, availableLayers, trackedObjects, layers, gaps }; +} diff --git a/src/styles/panels.css b/src/styles/panels.css index 27e184c25b..33b1699617 100644 --- a/src/styles/panels.css +++ b/src/styles/panels.css @@ -3003,4 +3003,118 @@ a.hub-top-story { text-align: center; color: var(--text-muted, #888); font-size: 12px; + +/* ---------------------------------------------------------- + Sensor Fusion Deck (WorldView-inspired public OSINT fusion) + ---------------------------------------------------------- */ +.sensor-fusion-panel .panel-content { + display: flex; + flex-direction: column; + gap: 10px; +} + +.sensor-fusion-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; +} + +.sensor-fusion-metric { + border: 1px solid var(--border); + border-radius: 8px; + padding: 8px; + background: var(--bg-secondary); +} + +.sensor-fusion-metric-value { + display: block; + color: var(--accent); + font-size: 18px; + font-weight: 700; + line-height: 1.1; +} + +.sensor-fusion-metric-label { + color: var(--text-dim); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.sensor-fusion-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); + gap: 8px; +} + +.sensor-fusion-layer { + border: 1px solid var(--border); + border-radius: 8px; + padding: 8px; + background: linear-gradient(135deg, var(--bg-secondary), var(--bg)); +} + +.sensor-fusion-layer.live { + border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); +} + +.sensor-fusion-layer-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.sensor-fusion-layer-label { + color: var(--text); + font-size: 12px; + font-weight: 700; +} + +.sensor-fusion-status { + border-radius: 999px; + padding: 2px 6px; + font-size: 9px; + font-weight: 700; + letter-spacing: 0.08em; + color: var(--text-dim); + background: var(--overlay-subtle); +} + +.sensor-fusion-status.live { + color: var(--accent); +} + +.sensor-fusion-status.planned { + color: var(--text-muted); +} + +.sensor-fusion-source, +.sensor-fusion-note, +.sensor-fusion-guardrail { + color: var(--text-dim); + font-size: 11px; + line-height: 1.4; +} + +.sensor-fusion-source { + margin-top: 4px; +} + +.sensor-fusion-layer-count { + margin-top: 6px; + color: var(--text); + font-size: 20px; + font-weight: 700; +} + +.sensor-fusion-note { + margin: 4px 0 0; +} + +.sensor-fusion-guardrail { + border-left: 2px solid var(--accent); + padding: 8px 10px; + background: var(--overlay-subtle); + border-radius: 6px; } diff --git a/tests/sensor-fusion.test.mts b/tests/sensor-fusion.test.mts new file mode 100644 index 0000000000..21efd78a56 --- /dev/null +++ b/tests/sensor-fusion.test.mts @@ -0,0 +1,58 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; + +import { buildSensorFusionSnapshot } from '../src/services/sensor-fusion.ts'; + +const emptyLayers = { + flights: false, + military: false, + ais: false, + satellites: false, + webcams: false, + natural: false, + fires: false, + weather: false, + weatherRadar: false, +}; + +describe('sensor fusion snapshot', () => { + it('summarizes live fused layers and tracked object counts', () => { + const snapshot = buildSensorFusionSnapshot({ + aircraftPositions: [{ lat: 1, lon: 2 }], + military: { + flights: [{ lat: 3, lon: 4 }, { lat: 5, lon: 6 }], + flightClusters: [], + vessels: [{ lat: 7, lon: 8 }], + vesselClusters: [], + }, + earthquakes: [{ location: { latitude: 1, longitude: 2 } }], + thermalEscalation: { clusters: [{ id: 'hot-zone' }] }, + imageryScenes: [{ id: 'scene-1' }, { id: 'scene-2' }], + } as any, { + ...emptyLayers, + flights: true, + military: true, + ais: true, + satellites: true, + natural: true, + fires: true, + }); + + assert.equal(snapshot.liveLayers, 6); + assert.equal(snapshot.trackedObjects, 8); + assert.equal(snapshot.layers.find(layer => layer.id === 'aircraft')?.count, 3); + assert.equal(snapshot.layers.find(layer => layer.id === '3d-reconstruction')?.status, 'planned'); + }); + + it('marks enabled but empty feeds as ready instead of live', () => { + const snapshot = buildSensorFusionSnapshot({}, { + ...emptyLayers, + satellites: true, + weather: true, + }); + + assert.equal(snapshot.layers.find(layer => layer.id === 'satellites')?.status, 'ready'); + assert.equal(snapshot.layers.find(layer => layer.id === 'weather')?.status, 'ready'); + assert.equal(snapshot.liveLayers, 1); // the base globe is always live + }); +});