Skip to content

Commit c3c06ff

Browse files
authored
Merge pull request calesthio#53 from calesthio/codex/dashboard-regressions-opensky-fallback
Fix dashboard regressions and add OpenSky fallback
2 parents 6514d7c + 7e3ead0 commit c3c06ff

File tree

4 files changed

+136
-44
lines changed

4 files changed

+136
-44
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,8 @@ This is normal — the first sweep takes 30–60 seconds to query all 27 sources
470470
471471
Expected behavior. Sources that require API keys will return structured errors if the key isn't set. The rest of the sweep continues normally. Check the Source Integrity section in the dashboard (or the server logs) to see which sources failed and why. The 3 most impactful free keys to add are `FRED_API_KEY`, `FIRMS_MAP_KEY`, and `EIA_API_KEY`.
472472
473+
OpenSky can also return `HTTP 429` when its public hotspots are queried too aggressively. Crucix does not try to evade that limit. Instead, it surfaces the throttle/error in source health and preserves the most recent non-empty air traffic snapshot from `runs/` so the dashboard flight layer does not suddenly go blank on a throttled sweep.
474+
473475
### Telegram bot not responding to commands
474476
475477
Make sure both `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` are set in `.env`. The bot only responds to messages from the configured chat ID (security measure). You should see `[Crucix] Telegram alerts enabled` and `[Crucix] Bot command polling started` in the server logs on startup. If not, double-check your token with `curl https://api.telegram.org/bot<YOUR_TOKEN>/getMe`.

apis/sources/opensky.mjs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export async function briefing() {
6969
const results = await Promise.all(
7070
hotspotEntries.map(async ([key, box]) => {
7171
const data = await getFlightsInArea(box.lamin, box.lomin, box.lamax, box.lomax);
72+
const error = data?.error || null;
7273
const states = data?.states || [];
7374
return {
7475
region: box.label,
@@ -83,14 +84,25 @@ export async function briefing() {
8384
// Flag potentially interesting (military often have no callsign or specific patterns)
8485
noCallsign: states.filter(s => !s[1]?.trim()).length,
8586
highAltitude: states.filter(s => s[7] && s[7] > 12000).length, // >12km altitude
87+
...(error ? { error } : {}),
8688
};
8789
})
8890
);
8991

92+
const hotspotErrors = results
93+
.filter(r => r.error)
94+
.map(r => ({ region: r.region, error: r.error }));
95+
9096
return {
9197
source: 'OpenSky',
9298
timestamp: new Date().toISOString(),
9399
hotspots: results,
100+
...(hotspotErrors.length ? {
101+
error: hotspotErrors.length === results.length
102+
? `OpenSky unavailable across all hotspots: ${hotspotErrors[0].error}`
103+
: `OpenSky unavailable for ${hotspotErrors.length}/${results.length} hotspots`,
104+
hotspotErrors,
105+
} : {}),
94106
};
95107
}
96108

dashboard/inject.mjs

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
//
66
// Exports synthesize(), generateIdeas(), fetchAllNews() for use by server.mjs
77

8-
import { readFileSync, writeFileSync } from 'fs';
8+
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs';
99
import { dirname, join } from 'path';
1010
import { fileURLToPath } from 'url';
1111
import { exec } from 'child_process';
@@ -102,6 +102,49 @@ function sanitizeExternalUrl(raw) {
102102
}
103103
}
104104

105+
function sumAirHotspots(hotspots = []) {
106+
return hotspots.reduce((sum, hotspot) => sum + (hotspot.totalAircraft || 0), 0);
107+
}
108+
109+
function summarizeAirHotspots(hotspots = []) {
110+
return hotspots.map(h => ({
111+
region: h.region,
112+
total: h.totalAircraft || 0,
113+
noCallsign: h.noCallsign || 0,
114+
highAlt: h.highAltitude || 0,
115+
top: Object.entries(h.byCountry || {}).sort((a, b) => b[1] - a[1]).slice(0, 5),
116+
}));
117+
}
118+
119+
function loadOpenSkyFallback(currentTimestamp) {
120+
const runsDir = join(ROOT, 'runs');
121+
if (!existsSync(runsDir)) return null;
122+
123+
const currentMs = currentTimestamp ? new Date(currentTimestamp).getTime() : NaN;
124+
const files = readdirSync(runsDir)
125+
.filter(name => /^briefing_.*\.json$/.test(name))
126+
.sort()
127+
.reverse();
128+
129+
for (const file of files) {
130+
const filePath = join(runsDir, file);
131+
try {
132+
const prior = JSON.parse(readFileSync(filePath, 'utf8'));
133+
const priorTimestamp = prior.sources?.OpenSky?.timestamp || prior.crucix?.timestamp || null;
134+
if (priorTimestamp && Number.isFinite(currentMs) && new Date(priorTimestamp).getTime() >= currentMs) continue;
135+
136+
const hotspots = prior.sources?.OpenSky?.hotspots || [];
137+
if (sumAirHotspots(hotspots) > 0) {
138+
return { file, timestamp: priorTimestamp, hotspots };
139+
}
140+
} catch {
141+
// Ignore unreadable historical runs and continue searching backward.
142+
}
143+
}
144+
145+
return null;
146+
}
147+
105148
// === RSS Fetching ===
106149
async function fetchRSS(url, source) {
107150
try {
@@ -326,11 +369,12 @@ export function generateIdeas(V2) {
326369

327370
// === Synthesize raw sweep data into dashboard format ===
328371
export async function synthesize(data) {
329-
const air = (data.sources.OpenSky?.hotspots || []).map(h => ({
330-
region: h.region, total: h.totalAircraft || 0, noCallsign: h.noCallsign || 0,
331-
highAlt: h.highAltitude || 0,
332-
top: Object.entries(h.byCountry || {}).sort((a, b) => b[1] - a[1]).slice(0, 5)
333-
}));
372+
const liveAirHotspots = data.sources.OpenSky?.hotspots || [];
373+
const airFallback = sumAirHotspots(liveAirHotspots) > 0
374+
? null
375+
: loadOpenSkyFallback(data.sources.OpenSky?.timestamp || data.crucix?.timestamp);
376+
const effectiveAirHotspots = airFallback?.hotspots || liveAirHotspots;
377+
const air = summarizeAirHotspots(effectiveAirHotspots);
334378
const thermal = (data.sources.FIRMS?.hotspots || []).map(h => ({
335379
region: h.region, det: h.totalDetections || 0, night: h.nightDetections || 0,
336380
hc: h.highConfidence || 0,
@@ -511,6 +555,14 @@ export async function synthesize(data) {
511555

512556
const V2 = {
513557
meta: data.crucix, air, thermal, tSignals, chokepoints, nuke, nukeSignals,
558+
airMeta: {
559+
fallback: Boolean(airFallback),
560+
liveTotal: sumAirHotspots(liveAirHotspots),
561+
timestamp: airFallback?.timestamp || data.sources.OpenSky?.timestamp || data.crucix?.timestamp || null,
562+
source: airFallback ? 'OpenSky fallback' : 'OpenSky',
563+
...(airFallback ? { fallbackFile: airFallback.file } : {}),
564+
...(data.sources.OpenSky?.error ? { error: data.sources.OpenSky.error } : {}),
565+
},
514566
sdr: { total: sdrNet.totalReceivers || 0, online: sdrNet.online || 0, zones: sdrZones },
515567
tg: { posts: tgData.totalPosts || 0, urgent: tgUrgent, topPosts: tgTop },
516568
who, fred, energy, bls, treasury, gscpi, defense, noaa, epa, acled, gdelt, space, health, news,

dashboard/public/jarvis.html

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
.econ-row .eval{font-family:var(--mono);font-weight:600}
9898

9999
/* CENTER: MAP */
100+
.map-region-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 12px;border:1px solid var(--border);background:var(--panel);backdrop-filter:blur(20px)}
100101
.map-container{flex:1;min-height:560px;border:1px solid var(--border);background:radial-gradient(ellipse at center,rgba(4,12,20,1),rgba(2,4,8,1));position:relative;overflow:hidden}
101102
#globeViz{width:100%;height:100%;cursor:grab}
102103
#globeViz:active{cursor:grabbing}
@@ -272,6 +273,7 @@
272273
.topbar{padding:10px 12px}
273274
.top-left,.top-center,.top-right{width:100%}
274275
.top-center{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}
276+
.map-region-bar{display:none}
275277
.top-right{gap:6px}
276278
.region-btn,.meta-pill,.alert-badge,.guide-btn{font-size:10px}
277279
.grid{display:flex;flex-direction:column}
@@ -331,6 +333,7 @@
331333
<div class="grid">
332334
<div class="col" id="leftRail"></div>
333335
<div class="col" id="centerCol">
336+
<div class="map-region-bar" id="mapRegionBar"></div>
334337
<div class="map-container" id="mapContainer">
335338
<div id="globeViz"></div>
336339
<svg id="flatMapSvg" style="display:none;width:100%;height:100%;position:absolute;top:0;left:0;cursor:grab"></svg>
@@ -389,6 +392,7 @@
389392
let flightsVisible = true;
390393
let lowPerfMode = localStorage.getItem('crucix_low_perf') === 'true';
391394
let isFlat = shouldStartFlat();
395+
let currentRegion = 'world';
392396
let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH;
393397
const signalGuideItems = [
394398
{
@@ -554,7 +558,26 @@
554558
}
555559

556560
// === TOPBAR ===
561+
function getRegionControlsMarkup(){
562+
return ['world','americas','europe','middleEast','asiaPacific','africa'].map(r=>
563+
`<button class="region-btn ${r===currentRegion?'active':''}" data-region="${r}" onclick="setRegion('${r}')">${r==='middleEast'?'MIDDLE EAST':r==='asiaPacific'?'ASIA PACIFIC':r.toUpperCase()}</button>`
564+
).join('');
565+
}
566+
567+
function renderRegionControls(){
568+
const mapRegionBar = document.getElementById('mapRegionBar');
569+
if(!mapRegionBar) return;
570+
if(isMobileLayout()){
571+
mapRegionBar.innerHTML = '';
572+
mapRegionBar.style.display = 'none';
573+
return;
574+
}
575+
mapRegionBar.innerHTML = getRegionControlsMarkup();
576+
mapRegionBar.style.display = 'flex';
577+
}
578+
557579
function renderTopbar(){
580+
const mobile = isMobileLayout();
558581
const ts = new Date(D.meta.timestamp);
559582
const d = ts.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}).toUpperCase();
560583
const timeStr = ts.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:true});
@@ -563,11 +586,7 @@
563586
<span class="brand">CRUCIX MONITOR</span>
564587
<span class="regime-chip"><span class="blink"></span>WARTIME STAGFLATION RISK</span>
565588
</div>
566-
<div class="top-center">
567-
${['world','americas','europe','middleEast','asiaPacific','africa'].map(r=>
568-
`<button class="region-btn ${r==='world'?'active':''}" data-region="${r}" onclick="setRegion('${r}')">${r==='middleEast'?'MIDDLE EAST':r==='asiaPacific'?'ASIA PACIFIC':r.toUpperCase()}</button>`
569-
).join('')}
570-
</div>
589+
${mobile ? `<div class="top-center">${getRegionControlsMarkup()}</div>` : ''}
571590
<div class="top-right">
572591
<button class="meta-pill perf-pill" onclick="togglePerfMode()" title="Reduce visual effects and start mobile in flat mode">${t('dashboard.perf','PERF')} <span class="v" id="perfStatus">${lowPerfMode?t('dashboard.perfLow','LOW'):t('dashboard.perfHigh','HIGH')}</span></button>
573592
<span class="meta-pill">${t('dashboard.sweep','SWEEP')} <span class="v">${(D.meta.totalDurationMs/1000).toFixed(1)}s</span></span>
@@ -577,6 +596,7 @@
577596
<button class="guide-btn" onclick="openGlossary()">${t('dashboard.guideBtn','What Signals Mean')}</button>
578597
<span class="alert-badge">${t('dashboard.highAlert','HIGH ALERT')}</span>
579598
</div>`;
599+
renderRegionControls();
580600
}
581601

582602
// === LEFT RAIL ===
@@ -1077,6 +1097,13 @@
10771097
flightsVisible = !flightsVisible;
10781098
const btn = document.getElementById('flightToggle');
10791099
btn.classList.toggle('off', !flightsVisible);
1100+
if(isFlat){
1101+
if(flatG){
1102+
flatG.selectAll('*').remove();
1103+
drawFlatMap();
1104+
}
1105+
return;
1106+
}
10801107
if(!globe){
10811108
return;
10821109
}
@@ -1149,13 +1176,6 @@
11491176
flatG.selectAll('.marker-circle').attr('r',function(){return +this.dataset.baseR/Math.sqrt(k)});
11501177
flatG.selectAll('.marker-label').style('font-size',Math.max(7,9/Math.sqrt(k))+'px')
11511178
.style('display',k>=2.5?'block':'none');
1152-
// Priority-based visibility: hide low-priority markers at low zoom
1153-
flatG.selectAll('[data-priority]').style('display',function(){
1154-
const p=+this.dataset.priority;
1155-
if(p<=1) return 'block';
1156-
if(p<=2) return k>=2?'block':'none';
1157-
return k>=3.5?'block':'none';
1158-
});
11591179
});
11601180
flatSvg.call(flatZoom);
11611181
drawFlatMap();
@@ -1184,12 +1204,14 @@
11841204
};
11851205
// Air
11861206
const airCoords=[{lat:30,lon:44},{lat:24,lon:120},{lat:49,lon:32},{lat:57,lon:24},{lat:14,lon:114},{lat:37,lon:127},{lat:25,lon:-80},{lat:4,lon:2},{lat:-34,lon:18},{lat:10,lon:51}];
1187-
D.air.forEach((a,i)=>{
1188-
const c=airCoords[i];if(!c)return;
1189-
const g=addPt(c.lat,c.lon,4+a.total/40,'rgba(100,240,200,0.7)','rgba(100,240,200,0.3)',
1190-
ev=>showPopup(ev,a.region,`${a.total} aircraft<br>No callsign: ${a.noCallsign}<br>High alt: ${a.highAlt}`,'Air Activity'),1);
1191-
if(g) g.append('text').attr('class','marker-label').attr('x',10).attr('y',3).attr('fill','var(--dim)').attr('font-size','9px').attr('font-family','var(--mono)').text(a.region.replace(' Region','')+' '+a.total);
1192-
});
1207+
if(flightsVisible){
1208+
D.air.forEach((a,i)=>{
1209+
const c=airCoords[i];if(!c)return;
1210+
const g=addPt(c.lat,c.lon,4+a.total/40,'rgba(100,240,200,0.7)','rgba(100,240,200,0.3)',
1211+
ev=>showPopup(ev,a.region,`${a.total} aircraft<br>No callsign: ${a.noCallsign}<br>High alt: ${a.highAlt}`,'Air Activity'),1);
1212+
if(g) g.append('text').attr('class','marker-label').attr('x',10).attr('y',3).attr('fill','var(--dim)').attr('font-size','9px').attr('font-family','var(--mono)').text(a.region.replace(' Region','')+' '+a.total);
1213+
});
1214+
}
11931215
// Thermal
11941216
D.thermal.forEach(t=>t.fires.forEach(f=>{
11951217
addPt(f.lat,f.lon,2+Math.min(f.frp/50,5),'rgba(255,95,99,0.6)','rgba(255,95,99,0.2)',
@@ -1237,25 +1259,27 @@
12371259
g.append('circle').attr('r',r*0.4).attr('fill','rgba(255,120,80,0.3)');
12381260
});
12391261
// Flight corridors
1240-
const airCoordsFlight=[{lat:30,lon:44},{lat:24,lon:120},{lat:49,lon:32},{lat:57,lon:24},{lat:14,lon:114},{lat:37,lon:127},{lat:25,lon:-80},{lat:4,lon:2},{lat:-34,lon:18},{lat:10,lon:51}];
1241-
const hubs=[{lat:40.6,lon:-73.8},{lat:51.5,lon:-0.5},{lat:25.3,lon:55.4},{lat:1.4,lon:103.8},{lat:-33.9,lon:151.2},{lat:-23.4,lon:-46.5}];
1242-
const cG=flatG.append('g').attr('class','corridors-layer');
1243-
for(let i=0;i<D.air.length;i++){for(let j=i+1;j<D.air.length;j++){
1244-
const a=D.air[i],b=D.air[j],from=airCoordsFlight[i],to=airCoordsFlight[j];
1245-
if(!from||!to)continue;const traffic=a.total+b.total;if(traffic<30)continue;
1246-
const ncR=(a.noCallsign+b.noCallsign)/Math.max(traffic,1);
1247-
const clr=ncR>0.15?'rgba(255,95,99,0.4)':ncR>0.05?'rgba(255,184,76,0.35)':'rgba(100,240,200,0.25)';
1248-
const interp=d3.geoInterpolate([from.lon,from.lat],[to.lon,to.lat]);
1249-
const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40));
1250-
const feat={type:'Feature',geometry:{type:'LineString',coordinates:coords}};
1251-
cG.append('path').datum(feat).attr('d',flatPath).attr('fill','none').attr('stroke',clr).attr('stroke-width',Math.max(0.8,Math.min(3,traffic/80)));
1252-
}}
1253-
D.air.forEach((a,i)=>{if(!airCoordsFlight[i]||a.total<25)return;hubs.forEach(hub=>{
1254-
if(Math.abs(airCoordsFlight[i].lat-hub.lat)+Math.abs(airCoordsFlight[i].lon-hub.lon)<20)return;
1255-
const interp=d3.geoInterpolate([airCoordsFlight[i].lon,airCoordsFlight[i].lat],[hub.lon,hub.lat]);
1256-
const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40));
1257-
cG.append('path').datum({type:'Feature',geometry:{type:'LineString',coordinates:coords}}).attr('d',flatPath).attr('fill','none').attr('stroke','rgba(100,240,200,0.15)').attr('stroke-width',0.6);
1258-
})});
1262+
if(flightsVisible){
1263+
const airCoordsFlight=[{lat:30,lon:44},{lat:24,lon:120},{lat:49,lon:32},{lat:57,lon:24},{lat:14,lon:114},{lat:37,lon:127},{lat:25,lon:-80},{lat:4,lon:2},{lat:-34,lon:18},{lat:10,lon:51}];
1264+
const hubs=[{lat:40.6,lon:-73.8},{lat:51.5,lon:-0.5},{lat:25.3,lon:55.4},{lat:1.4,lon:103.8},{lat:-33.9,lon:151.2},{lat:-23.4,lon:-46.5}];
1265+
const cG=flatG.append('g').attr('class','corridors-layer');
1266+
for(let i=0;i<D.air.length;i++){for(let j=i+1;j<D.air.length;j++){
1267+
const a=D.air[i],b=D.air[j],from=airCoordsFlight[i],to=airCoordsFlight[j];
1268+
if(!from||!to)continue;const traffic=a.total+b.total;if(traffic<30)continue;
1269+
const ncR=(a.noCallsign+b.noCallsign)/Math.max(traffic,1);
1270+
const clr=ncR>0.15?'rgba(255,95,99,0.4)':ncR>0.05?'rgba(255,184,76,0.35)':'rgba(100,240,200,0.25)';
1271+
const interp=d3.geoInterpolate([from.lon,from.lat],[to.lon,to.lat]);
1272+
const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40));
1273+
const feat={type:'Feature',geometry:{type:'LineString',coordinates:coords}};
1274+
cG.append('path').datum(feat).attr('d',flatPath).attr('fill','none').attr('stroke',clr).attr('stroke-width',Math.max(0.8,Math.min(3,traffic/80)));
1275+
}}
1276+
D.air.forEach((a,i)=>{if(!airCoordsFlight[i]||a.total<25)return;hubs.forEach(hub=>{
1277+
if(Math.abs(airCoordsFlight[i].lat-hub.lat)+Math.abs(airCoordsFlight[i].lon-hub.lon)<20)return;
1278+
const interp=d3.geoInterpolate([airCoordsFlight[i].lon,airCoordsFlight[i].lat],[hub.lon,hub.lat]);
1279+
const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40));
1280+
cG.append('path').datum({type:'Feature',geometry:{type:'LineString',coordinates:coords}}).attr('d',flatPath).attr('fill','none').attr('stroke','rgba(100,240,200,0.15)').attr('stroke-width',0.6);
1281+
})});
1282+
}
12591283
}
12601284

12611285
// Update setRegion for flat mode
@@ -1265,6 +1289,7 @@
12651289
const _origMapZoom = mapZoom;
12661290

12671291
function setRegion(r){
1292+
currentRegion = r;
12681293
document.querySelectorAll('.region-btn').forEach(b=>b.classList.toggle('active',b.dataset.region===r));
12691294
closePopup();
12701295
if(isFlat && flatSvg && flatZoom){
@@ -1553,7 +1578,7 @@
15531578
document.querySelectorAll('.mbar span,.smb span').forEach(bar=>{const w=bar.style.width;bar.style.width='0%';gsap.to(bar,{width:w,duration:1,ease:'power2.out'})});
15541579
document.querySelectorAll('.spark-bar').forEach(bar=>{const h=bar.style.height;bar.style.height='0%';gsap.to(bar,{height:h,duration:0.8,ease:'power2.out'})});
15551580
},1000);
1556-
},4.0);
1581+
},[],4.0);
15571582
}
15581583

15591584
function isMobileLayout(){ return window.innerWidth <= 1100; }
@@ -1644,6 +1669,7 @@
16441669
const mobileNow = isMobileLayout();
16451670
if(force || lastResponsiveMobile === null || mobileNow !== lastResponsiveMobile){
16461671
lastResponsiveMobile = mobileNow;
1672+
renderTopbar();
16471673
renderLeftRail();
16481674
renderLower();
16491675
renderRight();

0 commit comments

Comments
 (0)