Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion apis/briefing.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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),
Expand Down
75 changes: 75 additions & 0 deletions apis/sources/usgs.mjs
Original file line number Diff line number Diff line change
@@ -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));
}
8 changes: 7 additions & 1 deletion dashboard/inject.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {};
Expand Down Expand Up @@ -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),
Expand Down
14 changes: 14 additions & 0 deletions dashboard/public/jarvis.html
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down Expand Up @@ -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')},
Expand Down Expand Up @@ -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}<br>Depth: ${eq.depth?.toFixed(1)||'--'} km<br>Time: ${new Date(eq.time).toLocaleString()}<br>${eq.tsunami?'TSUNAMI WARNING':'No tsunami warning'}`
});
});

// === SDR receivers (cyan) ===
D.sdr.zones.forEach(z=>{
z.receivers.forEach(r=>{
Expand Down Expand Up @@ -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}<br>${z.region}`,'KiwiSDR'),3)}));
// OSINT
Expand Down
4 changes: 3 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down