From a3f9bdc1972ce8594be1b0059ea7a85a07837108 Mon Sep 17 00:00:00 2001 From: mbtariq82 Date: Fri, 27 Mar 2026 17:12:00 +0000 Subject: [PATCH] Add USGS Earthquake Hazards API source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Integrate USGS real-time earthquake monitoring - Fetch significant earthquakes (M≥4.0 or tsunami warnings) - Display as markers on globe/map with magnitude-based sizing - Add to dashboard layers and signals - Update source count to 28 across codebase - Add translations and README updates --- README.md | 15 ++++---- apis/briefing.mjs | 4 +- apis/sources/usgs.mjs | 75 ++++++++++++++++++++++++++++++++++++ dashboard/inject.mjs | 8 +++- dashboard/public/jarvis.html | 14 +++++++ locales/en.json | 4 +- locales/fr.json | 2 +- 7 files changed, 111 insertions(+), 11 deletions(-) create mode 100644 apis/sources/usgs.mjs diff --git a/README.md b/README.md index 29089eb..8fba746 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # Crucix -**Your own intelligence terminal. 27 sources. One command. Zero cloud.** +**Your own intelligence terminal. 28 sources. One command. Zero cloud.** ## [Visit The Live Site: crucix.live](https://www.crucix.live/) @@ -90,7 +90,7 @@ npm run dev > ``` > This bypasses npm's script runner, which can swallow errors on some systems (particularly PowerShell on Windows). You can also run `node diag.mjs` to diagnose the exact issue — it checks your Node version, tests each module import individually, and verifies port availability. See [Troubleshooting](#troubleshooting) for more. -The dashboard opens automatically at `http://localhost:3117` and immediately begins its first intelligence sweep. This initial sweep queries all 27 sources in parallel and typically takes 30–60 seconds — the dashboard will appear empty until the sweep completes and pushes the first data update. After that, it auto-refreshes every 15 minutes via SSE (Server-Sent Events). No manual page refresh needed. +The dashboard opens automatically at `http://localhost:3117` and immediately begins its first intelligence sweep. This initial sweep queries all 28 sources in parallel and typically takes 30–60 seconds — the dashboard will appear empty until the sweep completes and pushes the first data update. After that, it auto-refreshes every 15 minutes via SSE (Server-Sent Events). No manual page refresh needed. **Requirements:** Node.js 22+ (uses native `fetch`, top-level `await`, ESM) @@ -143,7 +143,7 @@ The preference is saved in browser local storage, so the UI will remember your l ### Auto-Refresh The server runs a sweep cycle every 15 minutes (configurable). Each cycle: -1. Queries all 27 sources in parallel (~30s) +1. Queries all 28 sources in parallel (~30s) 2. Synthesizes raw data into dashboard format 3. Computes delta from previous run (what changed, escalated, de-escalated) — visible in the **Sweep Delta** panel on the dashboard 4. Generates LLM trade ideas (if configured) @@ -282,7 +282,7 @@ crucix/ ├── docs/ # Screenshots for README │ ├── apis/ -│ ├── briefing.mjs # Master orchestrator — runs all 27 sources in parallel +│ ├── briefing.mjs # Master orchestrator — runs all 28 sources in parallel │ ├── save-briefing.mjs # CLI: save timestamped + latest.json │ ├── BRIEFING_PROMPT.md # Intelligence synthesis protocol │ ├── BRIEFING_TEMPLATE.md # Briefing output structure @@ -329,7 +329,7 @@ crucix/ ### Design Principles - **Pure ESM** — every file is `.mjs` with explicit imports - **Minimal dependencies** — Express is the only runtime dependency. `discord.js` is optional (for Discord bot). LLM providers use raw `fetch()`, no SDKs. -- **Parallel execution** — `Promise.allSettled()` fires all 27 sources simultaneously +- **Parallel execution** — `Promise.allSettled()` fires all 28 sources simultaneously - **Graceful degradation** — missing keys produce errors, not crashes. LLM failures don't kill sweeps. - **Each source is standalone** — run `node apis/sources/gdelt.mjs` to test any source independently - **Self-contained dashboard** — the HTML file works with or without the server @@ -366,12 +366,13 @@ crucix/ | **USAspending** | Federal spending and defense contracts | None | | **UN Comtrade** | Strategic commodity trade flows between major powers | None | -### Tier 3: Weather, Environment, Tech, Social, SIGINT (7) +### Tier 3: Weather, Environment, Tech, Social, SIGINT (8) | Source | What It Tracks | Auth | |--------|---------------|------| | **NOAA/NWS** | Active US weather alerts | None | | **EPA RadNet** | US government radiation monitoring | None | +| **USGS** | Significant earthquakes (M≥2.5) with tsunami warnings | None | | **USPTO Patents** | Patent filings in 7 strategic tech areas | None | | **Bluesky** | Social sentiment on geopolitical/market topics | None | | **Reddit** | Social sentiment from key subreddits | OAuth | @@ -487,7 +488,7 @@ Crucix requires Node.js 22 or later. If you have an older version, download the ### Dashboard shows empty panels after first start -This is normal — the first sweep takes 30–60 seconds to query all 27 sources. The dashboard will populate automatically once the sweep completes. Check the terminal for sweep progress logs. +This is normal — the first sweep takes 30–60 seconds to query all 28 sources. The dashboard will populate automatically once the sweep completes. Check the terminal for sweep progress logs. ### Some sources show errors diff --git a/apis/briefing.mjs b/apis/briefing.mjs index 1ae2cff..369b9ea 100644 --- a/apis/briefing.mjs +++ b/apis/briefing.mjs @@ -31,6 +31,7 @@ import { briefing as comtrade } from './sources/comtrade.mjs'; // === Tier 3: Weather, Environment, Technology, Social === import { briefing as noaa } from './sources/noaa.mjs'; import { briefing as epa } from './sources/epa.mjs'; +import { briefing as usgs } from './sources/usgs.mjs'; import { briefing as patents } from './sources/patents.mjs'; import { briefing as bluesky } from './sources/bluesky.mjs'; import { briefing as reddit } from './sources/reddit.mjs'; @@ -67,7 +68,7 @@ export async function runSource(name, fn, ...args) { } export async function fullBriefing() { - console.error('[Crucix] Starting intelligence sweep — 29 sources...'); + console.error('[Crucix] Starting intelligence sweep — 30 sources...'); const start = Date.now(); const allPromises = [ @@ -96,6 +97,7 @@ export async function fullBriefing() { // Tier 3: Weather, Environment, Technology, Social runSource('NOAA', noaa), runSource('EPA', epa), + runSource('USGS', usgs), runSource('Patents', patents), runSource('Bluesky', bluesky), runSource('Reddit', reddit), diff --git a/apis/sources/usgs.mjs b/apis/sources/usgs.mjs new file mode 100644 index 0000000..8ccf15d --- /dev/null +++ b/apis/sources/usgs.mjs @@ -0,0 +1,75 @@ +// USGS Earthquake Hazards Program — Real-time earthquake monitoring +// No auth required. Public domain data from USGS. + +import { safeFetch } from '../utils/fetch.mjs'; + +const BASE = 'https://earthquake.usgs.gov/earthquakes/feed/v1.0'; + +// Fetch recent earthquakes (last 24 hours) +export async function getEarthquakes() { + // Use 'significant' feed for earthquakes with mag >= 2.5 or with reviews + // Alternatively, 'all_day' for all, but we'll filter + const url = `${BASE}/summary/significant_day.geojson`; + return safeFetch(url); +} + +// Briefing — monitor significant earthquakes globally +export async function briefing() { + const data = await getEarthquakes(); + + if (!data || !data.features) { + return { + source: 'USGS', + timestamp: new Date().toISOString(), + earthquakes: [], + signals: ['No recent significant earthquake data available'], + }; + } + + // Filter for earthquakes with magnitude >= 4.0 or tsunami alerts + const significant = data.features.filter(feature => { + const props = feature.properties; + return props.mag >= 4.0 || props.tsunami > 0; + }); + + const earthquakes = significant.map(feature => { + const props = feature.properties; + const geometry = feature.geometry; + return { + id: feature.id, + magnitude: props.mag, + place: props.place, + time: new Date(props.time).toISOString(), + coordinates: geometry.coordinates, // [lon, lat, depth] + depth: geometry.coordinates[2], + tsunami: props.tsunami > 0 ? 'Warning issued' : 'No warning', + url: props.url, + felt: props.felt || 0, + cdi: props.cdi || null, // Community Determined Intensity + }; + }); + + // Sort by magnitude descending + earthquakes.sort((a, b) => b.magnitude - a.magnitude); + + const signals = earthquakes.length > 0 + ? earthquakes.slice(0, 5).map(eq => { + const tsunamiNote = eq.tsunami === 'Warning issued' ? ' (TSUNAMI WARNING)' : ''; + return `M${eq.magnitude.toFixed(1)} earthquake: ${eq.place}${tsunamiNote}`; + }) + : ['No significant earthquakes (M≥4.0) in the last 24 hours']; + + return { + source: 'USGS', + timestamp: new Date().toISOString(), + totalEarthquakes: data.metadata.count, + significantEarthquakes: earthquakes.length, + earthquakes: earthquakes.slice(0, 10), // Limit to top 10 for dashboard + signals, + }; +} + +if (process.argv[1]?.endsWith('usgs.mjs')) { + const data = await briefing(); + console.log(JSON.stringify(data, null, 2)); +} \ No newline at end of file diff --git a/dashboard/inject.mjs b/dashboard/inject.mjs index 962da19..7ad5b27 100644 --- a/dashboard/inject.mjs +++ b/dashboard/inject.mjs @@ -418,6 +418,12 @@ export async function synthesize(data) { site: s.site, anom: s.anomaly || false, cpm: s.avgCPM, n: s.recentReadings || 0 })); const nukeSignals = (data.sources.Safecast?.signals || []).filter(s => s); + const earthquakes = (data.sources.USGS?.earthquakes || []).map(eq => ({ + id: eq.id, mag: eq.magnitude, place: eq.place, time: eq.time, + lat: eq.coordinates[1], lon: eq.coordinates[0], depth: eq.depth, + tsunami: eq.tsunami === 'Warning issued' + })); + const quakeSignals = (data.sources.USGS?.signals || []).filter(s => s); const sdrData = data.sources.KiwiSDR || {}; const sdrNet = sdrData.network || {}; const sdrConflict = sdrData.conflictZones || {}; @@ -584,7 +590,7 @@ export async function synthesize(data) { const news = await fetchAllNews(); const V2 = { - meta: data.crucix, air, thermal, tSignals, chokepoints, nuke, nukeSignals, + meta: data.crucix, air, thermal, tSignals, chokepoints, nuke, nukeSignals, earthquakes, quakeSignals, airMeta: { fallback: Boolean(airFallback), liveTotal: sumAirHotspots(liveAirHotspots), diff --git a/dashboard/public/jarvis.html b/dashboard/public/jarvis.html index 0be7865..4ee03ae 100644 --- a/dashboard/public/jarvis.html +++ b/dashboard/public/jarvis.html @@ -81,6 +81,7 @@ .ldot.thermal{background:var(--danger);box-shadow:0 0 6px rgba(255,95,99,0.4)} .ldot.sdr{background:var(--accent2);box-shadow:0 0 6px rgba(68,204,255,0.4)} .ldot.nuke{background:#ffe082;box-shadow:0 0 6px rgba(255,224,130,0.4)} +.ldot.quake{background:#ff6b6b;box-shadow:0 0 6px rgba(255,107,107,0.4)} .ldot.incident{background:var(--warn);box-shadow:0 0 6px rgba(255,184,76,0.4)} .ldot.maritime{background:#b388ff;box-shadow:0 0 6px rgba(179,136,255,0.4)} .ldot.health{background:#69f0ae;box-shadow:0 0 6px rgba(105,240,174,0.4)} @@ -616,6 +617,7 @@ {name:t('layers.sdrCoverage','SDR Coverage'),count:D.sdr.total,dot:'sdr',sub:`${D.sdr.online} ${t('layers.online','online')}`}, {name:t('layers.maritimeWatch','Maritime Watch'),count:D.chokepoints.length,dot:'maritime',sub:t('layers.chokepoints','chokepoints')}, {name:t('layers.nuclearSites','Nuclear Sites'),count:D.nuke.length,dot:'nuke',sub:t('layers.monitors','monitors')}, + {name:t('layers.earthquakes','Earthquakes'),count:D.earthquakes.length,dot:'quake',sub:t('layers.significant','significant')}, {name:t('layers.conflictEvents','Conflict Events'),count:conflictEvents,dot:'thermal',sub:`${conflictFatal.toLocaleString()} ${t('layers.fatalities','fatalities')}`}, {name:t('layers.healthWatch','Health Watch'),count:D.who.length,dot:'health',sub:t('layers.whoAlerts','WHO alerts')}, {name:t('layers.worldNews','World News'),count:newsCount,dot:'news',sub:t('layers.rssGeolocated','RSS geolocated')}, @@ -888,6 +890,16 @@ }); }); + // === Earthquakes (red) === + D.earthquakes.forEach(eq=>{ + points.push({ + lat:eq.lat, lng:eq.lon, size:0.2 + (eq.mag - 4) * 0.1, alt:0.01, + color: eq.tsunami ? 'rgba(255,0,0,0.9)' : 'rgba(255,107,107,0.8)', type:'quake', priority:1, + popHead:`M${eq.mag.toFixed(1)} Earthquake`, popMeta:'USGS Seismic Monitoring', + popText:`${eq.place}
Depth: ${eq.depth?.toFixed(1)||'--'} km
Time: ${new Date(eq.time).toLocaleString()}
${eq.tsunami?'TSUNAMI WARNING':'No tsunami warning'}` + }); + }); + // === SDR receivers (cyan) === D.sdr.zones.forEach(z=>{ z.receivers.forEach(r=>{ @@ -1231,6 +1243,8 @@ // Nuclear const nukeCoords=[{lat:47.5,lon:34.6},{lat:51.4,lon:30.1},{lat:28.8,lon:50.9},{lat:39.8,lon:125.8},{lat:37.4,lon:141},{lat:31.0,lon:35.1}]; D.nuke.forEach((n,i)=>{const c=nukeCoords[i];if(!c)return;addPt(c.lat,c.lon,4,'rgba(255,224,130,0.7)','rgba(255,224,130,0.3)',ev=>showPopup(ev,n.site,`CPM: ${n.cpm?.toFixed(1)||'--'}`,'Radiation'),2)}); + // Earthquakes + D.earthquakes.forEach(eq=>{addPt(eq.lat,eq.lon,3 + (eq.mag - 4) * 0.5,'rgba(255,107,107,0.7)','rgba(255,107,107,0.3)',ev=>showPopup(ev,`M${eq.mag.toFixed(1)}`,eq.place,'Earthquake'),1)}); // SDR D.sdr.zones.forEach(z=>z.receivers.forEach(r=>{addPt(r.lat,r.lon,2.5,'rgba(68,204,255,0.5)','rgba(68,204,255,0.2)',ev=>showPopup(ev,'SDR',`${r.name}
${z.region}`,'KiwiSDR'),3)})); // OSINT diff --git a/locales/en.json b/locales/en.json index cfc74bb..5eb9466 100644 --- a/locales/en.json +++ b/locales/en.json @@ -7,7 +7,7 @@ "dashboard": { "title": "CRUCIX — Intelligence Terminal", "bootTitle": "CRUCIX INTELLIGENCE ENGINE", - "bootSubtitle": "Local Palantir · 31 Sources", + "bootSubtitle": "Local Palantir · 28 Sources", "waitingForSweep": "Waiting for first sweep...", "sourcesOk": "Sources OK", "lastSweep": "Last sweep", @@ -82,6 +82,8 @@ "whoAlerts": "WHO alerts", "rssGeolocated": "RSS geolocated", "earthquakes": "Earthquakes", + "significant": "significant", + "earthquakes": "Earthquakes", "seismicEvents": "Seismic Events", "cyberVulns": "Cyber Vulnerabilities", "spaceActivity": "Space Activity", diff --git a/locales/fr.json b/locales/fr.json index 0762b5b..9371f47 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -7,7 +7,7 @@ "dashboard": { "title": "CRUCIX — Terminal de Renseignement", "bootTitle": "CRUCIX MOTEUR DE RENSEIGNEMENT", - "bootSubtitle": "Palantir Local · 31 Sources", + "bootSubtitle": "Palantir Local · 28 Sources", "waitingForSweep": "En attente du premier scan...", "sourcesOk": "Sources OK", "lastSweep": "Dernier scan",