|
1 | | -import type { NewsItem, ClusteredEvent, MarketData } from '@/types'; |
| 1 | +import type { NewsItem, ClusteredEvent, MarketData, CyberThreat, Monitor } from '@/types'; |
2 | 2 | import type { PredictionMarket } from '@/services/prediction'; |
| 3 | +import type { IntelligenceCache } from '@/app/app-context'; |
| 4 | +import type { GpsJamData } from '@/services/gps-interference'; |
| 5 | +import type { ConvergenceCard } from '@/services/correlation-engine'; |
3 | 6 | import { t } from '@/services/i18n'; |
4 | 7 |
|
5 | 8 | type ExportFormat = 'json' | 'csv'; |
6 | 9 |
|
7 | | -interface ExportData { |
8 | | - news?: NewsItem[] | ClusteredEvent[]; |
| 10 | +export interface ExportMeta { |
| 11 | + exportedAt: string; |
| 12 | + note: string; |
| 13 | +} |
| 14 | + |
| 15 | +export interface ExportData { |
| 16 | + meta?: ExportMeta; |
| 17 | + timestamp: number; |
| 18 | + news?: NewsItem[]; |
| 19 | + newsClusters?: ClusteredEvent[]; |
| 20 | + newsByCategory?: Record<string, NewsItem[]>; |
9 | 21 | markets?: MarketData[]; |
10 | 22 | predictions?: PredictionMarket[]; |
11 | | - signals?: unknown[]; |
12 | | - timestamp: number; |
| 23 | + intelligence?: IntelligenceCache; |
| 24 | + cyberThreats?: CyberThreat[]; |
| 25 | + gpsJamming?: GpsJamData; |
| 26 | + convergenceCards?: Omit<ConvergenceCard, 'assessment'>[]; |
| 27 | + monitors?: Monitor[]; |
| 28 | +} |
| 29 | + |
| 30 | +// Strip LLM-derived threat annotations so AI does not feed back into itself. |
| 31 | +// Keyword and ML (local model) classifications are retained. |
| 32 | +function sanitizeNewsItem(item: NewsItem): NewsItem { |
| 33 | + if (item.threat?.source !== 'llm') return item; |
| 34 | + const { threat: _t, ...rest } = item; |
| 35 | + return rest as NewsItem; |
| 36 | +} |
| 37 | + |
| 38 | +function sanitizeCluster(cluster: ClusteredEvent): ClusteredEvent { |
| 39 | + return { |
| 40 | + ...cluster, |
| 41 | + threat: cluster.threat?.source === 'llm' ? undefined : cluster.threat, |
| 42 | + allItems: cluster.allItems.map(sanitizeNewsItem), |
| 43 | + }; |
| 44 | +} |
| 45 | + |
| 46 | +function sanitizeData(data: ExportData): ExportData { |
| 47 | + return { |
| 48 | + ...data, |
| 49 | + news: data.news?.map(sanitizeNewsItem), |
| 50 | + newsClusters: data.newsClusters?.map(sanitizeCluster), |
| 51 | + newsByCategory: data.newsByCategory |
| 52 | + ? Object.fromEntries( |
| 53 | + Object.entries(data.newsByCategory).map(([k, items]) => [k, items.map(sanitizeNewsItem)]), |
| 54 | + ) |
| 55 | + : undefined, |
| 56 | + }; |
13 | 57 | } |
14 | 58 |
|
15 | 59 | export function exportToJSON(data: ExportData, filename = 'worldmonitor-export'): void { |
16 | | - const jsonStr = JSON.stringify(data, null, 2); |
| 60 | + const jsonStr = JSON.stringify(sanitizeData(data), null, 2); |
17 | 61 | downloadFile(jsonStr, `${filename}.json`, 'application/json'); |
18 | 62 | } |
19 | 63 |
|
20 | 64 | export function exportToCSV(data: ExportData, filename = 'worldmonitor-export'): void { |
| 65 | + const clean = sanitizeData(data); |
21 | 66 | const lines: string[] = []; |
22 | 67 |
|
23 | | - if (data.news && data.news.length > 0) { |
| 68 | + lines.push(`# WorldMonitor Export — ${new Date(clean.timestamp).toISOString()}`); |
| 69 | + lines.push('# Note: CSV is a structured summary. Use JSON export for full fidelity.'); |
| 70 | + if (clean.meta?.note) lines.push(`# ${clean.meta.note}`); |
| 71 | + lines.push(''); |
| 72 | + |
| 73 | + // News — prefer raw items over clusters; clusters lose individual sources |
| 74 | + const newsItems = clean.news ?? []; |
| 75 | + if (newsItems.length > 0) { |
24 | 76 | lines.push('=== NEWS ==='); |
25 | | - lines.push('Title,Source,Link,Published,IsAlert'); |
26 | | - data.news.forEach(item => { |
27 | | - if ('primaryTitle' in item) { |
28 | | - const cluster = item as ClusteredEvent; |
29 | | - lines.push(csvRow([ |
30 | | - cluster.primaryTitle, |
31 | | - cluster.primarySource, |
32 | | - cluster.primaryLink, |
33 | | - cluster.lastUpdated.toISOString(), |
34 | | - String(cluster.isAlert), |
35 | | - ])); |
36 | | - } else { |
37 | | - const news = item as NewsItem; |
38 | | - lines.push(csvRow([ |
39 | | - news.title, |
40 | | - news.source, |
41 | | - news.link, |
42 | | - news.pubDate?.toISOString() || '', |
43 | | - String(news.isAlert), |
44 | | - ])); |
45 | | - } |
| 77 | + lines.push('Title,Source,Link,Published,IsAlert,ThreatLevel,ThreatCategory'); |
| 78 | + newsItems.forEach(item => { |
| 79 | + lines.push(csvRow([ |
| 80 | + item.title, |
| 81 | + item.source, |
| 82 | + item.link, |
| 83 | + item.pubDate?.toISOString() || '', |
| 84 | + String(item.isAlert), |
| 85 | + item.threat?.level ?? '', |
| 86 | + item.threat?.category ?? '', |
| 87 | + ])); |
46 | 88 | }); |
47 | 89 | lines.push(''); |
48 | 90 | } |
49 | 91 |
|
50 | | - if (data.markets && data.markets.length > 0) { |
| 92 | + if (clean.markets && clean.markets.length > 0) { |
51 | 93 | lines.push('=== MARKETS ==='); |
52 | 94 | lines.push('Symbol,Name,Price,Change'); |
53 | | - data.markets.forEach(m => { |
| 95 | + clean.markets.forEach(m => { |
54 | 96 | lines.push(csvRow([m.symbol, m.name, String(m.price ?? ''), String(m.change ?? '')])); |
55 | 97 | }); |
56 | 98 | lines.push(''); |
57 | 99 | } |
58 | 100 |
|
59 | | - if (data.predictions && data.predictions.length > 0) { |
| 101 | + if (clean.predictions && clean.predictions.length > 0) { |
60 | 102 | lines.push('=== PREDICTIONS ==='); |
61 | 103 | lines.push('Title,Yes Price,Volume'); |
62 | | - data.predictions.forEach(p => { |
| 104 | + clean.predictions.forEach(p => { |
63 | 105 | lines.push(csvRow([p.title, String(p.yesPrice), String(p.volume ?? '')])); |
64 | 106 | }); |
65 | 107 | lines.push(''); |
66 | 108 | } |
67 | 109 |
|
| 110 | + const intel = clean.intelligence; |
| 111 | + if (intel) { |
| 112 | + if (intel.protests?.events && intel.protests.events.length > 0) { |
| 113 | + lines.push('=== PROTESTS ==='); |
| 114 | + lines.push('Title,Country,EventType,Severity,Time'); |
| 115 | + intel.protests.events.forEach(e => { |
| 116 | + lines.push(csvRow([e.title, e.country, e.eventType, e.severity, e.time.toISOString()])); |
| 117 | + }); |
| 118 | + lines.push(''); |
| 119 | + } |
| 120 | + |
| 121 | + if (intel.earthquakes && intel.earthquakes.length > 0) { |
| 122 | + lines.push('=== EARTHQUAKES ==='); |
| 123 | + lines.push('Place,Magnitude,DepthKm,OccurredAt,URL'); |
| 124 | + intel.earthquakes.forEach(e => { |
| 125 | + lines.push(csvRow([e.place, String(e.magnitude), String(e.depthKm), new Date(e.occurredAt * 1000).toISOString(), e.sourceUrl])); |
| 126 | + }); |
| 127 | + lines.push(''); |
| 128 | + } |
| 129 | + |
| 130 | + if (intel.outages && intel.outages.length > 0) { |
| 131 | + lines.push('=== INTERNET OUTAGES ==='); |
| 132 | + lines.push('Title,Country,Severity,PubDate,Link'); |
| 133 | + intel.outages.forEach(o => { |
| 134 | + lines.push(csvRow([o.title, o.country, o.severity, o.pubDate.toISOString(), o.link])); |
| 135 | + }); |
| 136 | + lines.push(''); |
| 137 | + } |
| 138 | + |
| 139 | + if (intel.flightDelays && intel.flightDelays.length > 0) { |
| 140 | + lines.push('=== FLIGHT DELAYS ==='); |
| 141 | + lines.push('Airport,IATA,City,Country,DelayType,Severity,AvgDelayMin,Source'); |
| 142 | + intel.flightDelays.forEach(d => { |
| 143 | + lines.push(csvRow([d.name, d.iata, d.city, d.country, d.delayType, d.severity, String(d.avgDelayMinutes), d.source])); |
| 144 | + }); |
| 145 | + lines.push(''); |
| 146 | + } |
| 147 | + |
| 148 | + if (intel.military?.flights && intel.military.flights.length > 0) { |
| 149 | + lines.push('=== MILITARY FLIGHTS ==='); |
| 150 | + lines.push('Callsign,HexCode,AircraftType,Operator,Country,Lat,Lon'); |
| 151 | + intel.military.flights.forEach(f => { |
| 152 | + lines.push(csvRow([f.callsign, f.hexCode, f.aircraftType, f.operator, f.operatorCountry, String(f.lat), String(f.lon)])); |
| 153 | + }); |
| 154 | + lines.push(''); |
| 155 | + } |
| 156 | + |
| 157 | + if (intel.military?.vessels && intel.military.vessels.length > 0) { |
| 158 | + lines.push('=== MILITARY VESSELS ==='); |
| 159 | + lines.push('Name,MMSI,Country,VesselType,Lat,Lon'); |
| 160 | + intel.military.vessels.forEach(v => { |
| 161 | + lines.push(csvRow([v.name, v.mmsi, v.operatorCountry, v.vesselType, String(v.lat), String(v.lon)])); |
| 162 | + }); |
| 163 | + lines.push(''); |
| 164 | + } |
| 165 | + |
| 166 | + if (intel.iranEvents && intel.iranEvents.length > 0) { |
| 167 | + lines.push('=== IRAN EVENTS ==='); |
| 168 | + lines.push('Title,Category,Location,Severity,Timestamp'); |
| 169 | + intel.iranEvents.forEach(e => { |
| 170 | + lines.push(csvRow([e.title, e.category, e.locationName, e.severity, e.timestamp])); |
| 171 | + }); |
| 172 | + lines.push(''); |
| 173 | + } |
| 174 | + |
| 175 | + if (intel.orefAlerts) { |
| 176 | + lines.push('=== OREF ALERTS ==='); |
| 177 | + lines.push('ActiveAlerts,History24h'); |
| 178 | + lines.push(csvRow([String(intel.orefAlerts.alertCount), String(intel.orefAlerts.historyCount24h)])); |
| 179 | + lines.push(''); |
| 180 | + } |
| 181 | + |
| 182 | + if (intel.advisories && intel.advisories.length > 0) { |
| 183 | + lines.push('=== SECURITY ADVISORIES ==='); |
| 184 | + lines.push('Title,Source,Level,Country,PubDate,Link'); |
| 185 | + intel.advisories.forEach(a => { |
| 186 | + lines.push(csvRow([a.title, a.source, a.level ?? '', a.country ?? '', a.pubDate.toISOString(), a.link])); |
| 187 | + }); |
| 188 | + lines.push(''); |
| 189 | + } |
| 190 | + |
| 191 | + if (intel.radiation?.observations && intel.radiation.observations.length > 0) { |
| 192 | + lines.push('=== RADIATION MONITORING ==='); |
| 193 | + lines.push('Location,Country,Value,Unit,ObservedAt'); |
| 194 | + intel.radiation.observations.forEach(s => { |
| 195 | + lines.push(csvRow([s.location, s.country, String(s.value), s.unit, s.observedAt.toISOString()])); |
| 196 | + }); |
| 197 | + lines.push(''); |
| 198 | + } |
| 199 | + |
| 200 | + if (intel.imageryScenes && intel.imageryScenes.length > 0) { |
| 201 | + lines.push('=== SATELLITE IMAGERY ==='); |
| 202 | + lines.push('ID,Satellite,DateTime,ResolutionM,Mode'); |
| 203 | + intel.imageryScenes.forEach(s => { |
| 204 | + lines.push(csvRow([s.id, s.satellite, s.datetime, String(s.resolutionM), s.mode])); |
| 205 | + }); |
| 206 | + lines.push(''); |
| 207 | + } |
| 208 | + |
| 209 | + if (intel.sanctions) { |
| 210 | + lines.push('=== SANCTIONS ==='); |
| 211 | + lines.push('# See JSON export for full sanctions data'); |
| 212 | + lines.push(`TotalCount,${intel.sanctions.totalCount}`); |
| 213 | + lines.push(`SDNCount,${intel.sanctions.sdnCount}`); |
| 214 | + lines.push(`NewEntries,${intel.sanctions.newEntryCount}`); |
| 215 | + lines.push(''); |
| 216 | + } |
| 217 | + |
| 218 | + if (intel.thermalEscalation) { |
| 219 | + lines.push('=== THERMAL ESCALATION ==='); |
| 220 | + lines.push('# See JSON export for full thermal data'); |
| 221 | + lines.push(`ClusterCount,${intel.thermalEscalation.summary.clusterCount}`); |
| 222 | + lines.push(`ElevatedCount,${intel.thermalEscalation.summary.elevatedCount}`); |
| 223 | + lines.push(''); |
| 224 | + } |
| 225 | + |
| 226 | + if (intel.usniFleet) { |
| 227 | + lines.push('=== USNI FLEET ==='); |
| 228 | + lines.push('# See JSON export for full fleet data'); |
| 229 | + lines.push(`Vessels,${intel.usniFleet.vessels?.length ?? 0}`); |
| 230 | + lines.push(''); |
| 231 | + } |
| 232 | + |
| 233 | + if (intel.aircraftPositions && intel.aircraftPositions.length > 0) { |
| 234 | + lines.push('=== AIRCRAFT POSITIONS ==='); |
| 235 | + lines.push(`# ${intel.aircraftPositions.length} positions — see JSON for full data`); |
| 236 | + lines.push(''); |
| 237 | + } |
| 238 | + } |
| 239 | + |
| 240 | + if (clean.cyberThreats && clean.cyberThreats.length > 0) { |
| 241 | + lines.push('=== CYBER THREATS ==='); |
| 242 | + lines.push('Indicator,Type,Severity,Country,Source,FirstSeen'); |
| 243 | + clean.cyberThreats.forEach(c => { |
| 244 | + lines.push(csvRow([c.indicator, c.indicatorType, String(c.severity), c.country ?? '', c.source, c.firstSeen ?? ''])); |
| 245 | + }); |
| 246 | + lines.push(''); |
| 247 | + } |
| 248 | + |
| 249 | + if (clean.gpsJamming) { |
| 250 | + lines.push('=== GPS JAMMING ==='); |
| 251 | + lines.push('FetchedAt,TotalHexes,HighCount,MediumCount'); |
| 252 | + const s = clean.gpsJamming.stats; |
| 253 | + lines.push(csvRow([clean.gpsJamming.fetchedAt, String(s.totalHexes), String(s.highCount), String(s.mediumCount)])); |
| 254 | + lines.push('# Per-hex data available in JSON export'); |
| 255 | + lines.push(''); |
| 256 | + } |
| 257 | + |
| 258 | + if (clean.convergenceCards && clean.convergenceCards.length > 0) { |
| 259 | + lines.push('=== SIGNAL CONVERGENCE ==='); |
| 260 | + lines.push('Domain,Title,Score,Trend,Countries'); |
| 261 | + clean.convergenceCards.forEach(c => { |
| 262 | + lines.push(csvRow([c.domain, c.title, String(c.score), c.trend, c.countries.join(';')])); |
| 263 | + }); |
| 264 | + lines.push(''); |
| 265 | + } |
| 266 | + |
| 267 | + if (clean.monitors && clean.monitors.length > 0) { |
| 268 | + lines.push('=== MONITORS ==='); |
| 269 | + lines.push('Name,Keywords,Color'); |
| 270 | + clean.monitors.forEach(m => { |
| 271 | + lines.push(csvRow([m.name ?? '', m.keywords.join(';'), m.color])); |
| 272 | + }); |
| 273 | + lines.push(''); |
| 274 | + } |
| 275 | + |
68 | 276 | downloadFile(lines.join('\n'), `${filename}.csv`, 'text/csv'); |
69 | 277 | } |
70 | 278 |
|
|
0 commit comments