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",