fix: stabilize v0.9.7 startup and feeds

This commit is contained in:
BigBodyCobain
2026-05-02 13:35:49 -06:00
parent f5b9d14b48
commit 08810f2537
22 changed files with 940 additions and 130 deletions
+24 -7
View File
@@ -51,6 +51,10 @@ const NO_STORE_PROXY_HEADERS = {
Pragma: 'no-cache',
};
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isSensitiveProxyPath(pathSegments: string[]): boolean {
const joined = pathSegments.join('/');
if (!joined) return false;
@@ -76,8 +80,7 @@ async function proxy(req: NextRequest, pathSegments: string[]): Promise<NextResp
isMesh &&
!isSensitiveMeshPath &&
['POST', 'PUT', 'DELETE'].includes(req.method.toUpperCase()) &&
(meshSegments.join('/') === 'send' ||
meshSegments.join('/') === 'vote' ||
(meshSegments.join('/') === 'vote' ||
meshSegments.join('/') === 'report' ||
meshSegments.join('/') === 'gate/create' ||
(meshSegments[0] === 'gate' && meshSegments[2] === 'message') ||
@@ -191,7 +194,7 @@ async function proxy(req: NextRequest, pathSegments: string[]): Promise<NextResp
}
const isBodyless = req.method === 'GET' || req.method === 'HEAD';
let upstream: Response;
let upstream: Response | null = null;
const requestInit: RequestInit & { duplex?: 'half' } = {
method: req.method,
headers: forwardHeaders,
@@ -202,12 +205,26 @@ async function proxy(req: NextRequest, pathSegments: string[]): Promise<NextResp
// Required for streaming request bodies in Node.js fetch
requestInit.duplex = 'half';
}
try {
upstream = await fetch(targetUrl.toString(), requestInit);
} catch {
const maxAttempts = isBodyless ? 18 : 1;
let fetchError: unknown = null;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try {
upstream = await fetch(targetUrl.toString(), requestInit);
fetchError = null;
break;
} catch (error) {
fetchError = error;
if (attempt >= maxAttempts) break;
await sleep(250);
}
}
if (!upstream) {
return new NextResponse(JSON.stringify({ error: 'Backend unavailable' }), {
status: 502,
headers: { 'Content-Type': 'application/json' },
headers: {
'Content-Type': 'application/json',
'X-Proxy-Error': fetchError instanceof Error ? fetchError.name : 'fetch_failed',
},
});
}
+132 -72
View File
@@ -30,7 +30,7 @@ import type { ActiveLayers, KiwiSDR, Scanner, SelectedEntity } from '@/types/das
import type { ShodanSearchMatch } from '@/types/shodan';
import { API_BASE } from '@/lib/api';
import { useDataPolling, LAYER_TOGGLE_EVENT } from '@/hooks/useDataPolling';
import { useBackendStatus, useDataKey } from '@/hooks/useDataStore';
import { useBackendStatus, useDataKey, useDataKeys } from '@/hooks/useDataStore';
import { useReverseGeocode } from '@/hooks/useReverseGeocode';
import { useRegionDossier } from '@/hooks/useRegionDossier';
import { useAgentActions } from '@/hooks/useAgentActions';
@@ -61,6 +61,9 @@ const MaplibreViewer = dynamic(() => import('@/components/MaplibreViewer'), { ss
export default function Dashboard() {
const viewBoundsRef = useRef<{ south: number; west: number; north: number; east: number } | null>(null);
// Start the critical map data request before panel/control-plane effects.
// Non-map widgets can warm up after this; first paint needs flights, ships, and intel first.
useDataPolling();
const { mouseCoords, locationLabel, handleMouseCoords } = useReverseGeocode();
const [selectedEntity, setSelectedEntity] = useState<SelectedEntity | null>(null);
const [trackedSdr, setTrackedSdr] = useState<KiwiSDR | null>(null);
@@ -211,10 +214,35 @@ export default function Dashboard() {
const [shodanResults, setShodanResults] = useState<ShodanSearchMatch[]>([]);
const [, setShodanQueryLabel] = useState('');
const [shodanStyle, setShodanStyle] = useState<import('@/types/shodan').ShodanStyleConfig>({ shape: 'circle', color: '#16a34a', size: 'md' });
useDataPolling();
const backendStatus = useBackendStatus();
const spaceWeather = useDataKey('space_weather');
const feedHealth = useFeedHealth();
const bootSignals = useDataKeys([
'bootstrap_ready',
'commercial_flights',
'military_flights',
'tracked_flights',
'ships',
'news',
'threat_level',
] as const);
const criticalPaintReady = Boolean(
bootSignals.bootstrap_ready ||
(bootSignals.commercial_flights?.length || 0) > 0 ||
(bootSignals.military_flights?.length || 0) > 0 ||
(bootSignals.tracked_flights?.length || 0) > 0 ||
(bootSignals.ships?.length || 0) > 0 ||
(bootSignals.news?.length || 0) > 0 ||
bootSignals.threat_level,
);
const [secondaryBootReady, setSecondaryBootReady] = useState(false);
useEffect(() => {
if (secondaryBootReady) return;
const delay = criticalPaintReady ? 900 : 5500;
const id = window.setTimeout(() => setSecondaryBootReady(true), delay);
return () => window.clearTimeout(id);
}, [criticalPaintReady, secondaryBootReady]);
// Global keyboard shortcuts
useKeyboardShortcuts({
@@ -249,6 +277,7 @@ export default function Dashboard() {
const layersTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const initialLayerSyncRef = useRef(false);
useEffect(() => {
if (!secondaryBootReady) return;
const syncLayers = (triggerRefetch: boolean) =>
fetch(`${API_BASE}/api/layers`, {
method: 'POST',
@@ -258,7 +287,7 @@ export default function Dashboard() {
if (triggerRefetch) {
window.dispatchEvent(new Event(LAYER_TOGGLE_EVENT));
}
}).catch((e) => console.error('Failed to update backend layers:', e));
}).catch((e) => console.warn('Backend layer sync will retry after runtime is reachable:', e));
if (layersTimerRef.current) clearTimeout(layersTimerRef.current);
if (!initialLayerSyncRef.current) {
@@ -272,7 +301,7 @@ export default function Dashboard() {
return () => {
if (layersTimerRef.current) clearTimeout(layersTimerRef.current);
};
}, [activeLayers]);
}, [activeLayers, secondaryBootReady]);
// Left panel accordion state
const [leftDataMinimized, setLeftDataMinimized] = useState(false);
@@ -393,12 +422,28 @@ export default function Dashboard() {
};
const [activeFilters, setActiveFilters] = useState<Record<string, string[]>>({});
const firstPaintActiveLayers = useMemo<ActiveLayers>(() => {
if (secondaryBootReady) return activeLayers;
return {
...activeLayers,
cctv: false,
sar: false,
gibs_imagery: false,
highres_satellite: false,
sentinel_hub: false,
viirs_nightlights: false,
psk_reporter: false,
tinygs: false,
datacenters: false,
power_plants: false,
};
}, [activeLayers, secondaryBootReady]);
// Agent fly_to handler (sar_focus_aoi etc.) — wired here now that
// setFlyToLocation is in scope. show_image is routed through
// useAgentActions at the top of Dashboard.
useAgentActions(handleMapRightClick, ({ lat, lng }) => {
setFlyToLocation({ lat, lng, ts: Date.now() });
});
}, secondaryBootReady);
// Eavesdrop Mode State
const [isEavesdropping] = useState(false);
@@ -415,7 +460,7 @@ export default function Dashboard() {
{/* MAPLIBRE WEBGL OVERLAY */}
<ErrorBoundary name="Map">
<MaplibreViewer
activeLayers={activeLayers}
activeLayers={firstPaintActiveLayers}
activeFilters={activeFilters}
effects={memoizedEffects}
onEntityClick={setSelectedEntity}
@@ -502,74 +547,87 @@ export default function Dashboard() {
>
{/* 1. DATA LAYERS (Top) */}
<div className="contents" style={{ direction: 'ltr' }}>
<ErrorBoundary name="WorldviewLeftPanel">
<WorldviewLeftPanel
activeLayers={activeLayers}
setActiveLayers={setActiveLayers}
shodanResultCount={shodanResults.length}
onSettingsClick={() => setSettingsOpen(true)}
onLegendClick={() => setLegendOpen(true)}
onOpenSarAoiEditor={() => setSarAoiEditorOpen(true)}
gibsDate={gibsDate}
setGibsDate={setGibsDate}
gibsOpacity={gibsOpacity}
setGibsOpacity={setGibsOpacity}
sentinelDate={sentinelDate}
setSentinelDate={setSentinelDate}
sentinelOpacity={sentinelOpacity}
setSentinelOpacity={setSentinelOpacity}
sentinelPreset={sentinelPreset}
setSentinelPreset={setSentinelPreset}
onEntityClick={setSelectedEntity}
onFlyTo={handleFlyTo}
trackedSdr={trackedSdr}
setTrackedSdr={setTrackedSdr}
trackedScanner={trackedScanner}
setTrackedScanner={setTrackedScanner}
isMinimized={leftDataMinimized}
onMinimizedChange={setLeftDataMinimized}
/>
</ErrorBoundary>
{secondaryBootReady ? (
<ErrorBoundary name="WorldviewLeftPanel">
<WorldviewLeftPanel
activeLayers={activeLayers}
setActiveLayers={setActiveLayers}
shodanResultCount={shodanResults.length}
onSettingsClick={() => setSettingsOpen(true)}
onLegendClick={() => setLegendOpen(true)}
onOpenSarAoiEditor={() => setSarAoiEditorOpen(true)}
gibsDate={gibsDate}
setGibsDate={setGibsDate}
gibsOpacity={gibsOpacity}
setGibsOpacity={setGibsOpacity}
sentinelDate={sentinelDate}
setSentinelDate={setSentinelDate}
sentinelOpacity={sentinelOpacity}
setSentinelOpacity={setSentinelOpacity}
sentinelPreset={sentinelPreset}
setSentinelPreset={setSentinelPreset}
onEntityClick={setSelectedEntity}
onFlyTo={handleFlyTo}
trackedSdr={trackedSdr}
setTrackedSdr={setTrackedSdr}
trackedScanner={trackedScanner}
setTrackedScanner={setTrackedScanner}
isMinimized={leftDataMinimized}
onMinimizedChange={setLeftDataMinimized}
/>
</ErrorBoundary>
) : (
<div className="bg-[#05090d]/95 border border-cyan-900/50 p-4 font-mono text-cyan-500/70">
<div className="text-[11px] tracking-[0.2em] text-cyan-400 font-bold">DATA LAYERS</div>
<div className="mt-3 text-[10px] tracking-wider">PRIORITIZING MAP FEEDS</div>
</div>
)}
</div>
{/* 2. MESH CHAT (Middle) */}
<div className="contents" style={{ direction: 'ltr' }}>
<MeshChat
onFlyTo={handleFlyTo}
expanded={leftMeshExpanded}
onExpandedChange={setLeftMeshExpanded}
onSettingsClick={() => setSettingsOpen(true)}
onTerminalToggle={openSecureTerminalLauncher}
launchRequest={meshChatLaunchRequest}
/>
</div>
{secondaryBootReady && (
<div className="contents" style={{ direction: 'ltr' }}>
<MeshChat
onFlyTo={handleFlyTo}
expanded={leftMeshExpanded}
onExpandedChange={setLeftMeshExpanded}
onSettingsClick={() => setSettingsOpen(true)}
onTerminalToggle={openSecureTerminalLauncher}
launchRequest={meshChatLaunchRequest}
/>
</div>
)}
{/* 3. SHODAN CONNECTOR (Bottom) */}
<div className="contents" style={{ direction: 'ltr' }}>
<ShodanPanel
currentResults={shodanResults}
onOpenSettings={() => setSettingsOpen(true)}
settingsOpen={settingsOpen}
onResultsChange={(results, queryLabel) => {
setShodanResults(results);
setShodanQueryLabel(queryLabel);
setActiveLayers((prev) => ({ ...prev, shodan_overlay: results.length > 0 }));
}}
onSelectEntity={setSelectedEntity}
onStyleChange={setShodanStyle}
isMinimized={leftShodanMinimized}
onMinimizedChange={setLeftShodanMinimized}
/>
</div>
{secondaryBootReady && (
<div className="contents" style={{ direction: 'ltr' }}>
<ShodanPanel
currentResults={shodanResults}
onOpenSettings={() => setSettingsOpen(true)}
settingsOpen={settingsOpen}
onResultsChange={(results, queryLabel) => {
setShodanResults(results);
setShodanQueryLabel(queryLabel);
setActiveLayers((prev) => ({ ...prev, shodan_overlay: results.length > 0 }));
}}
onSelectEntity={setSelectedEntity}
onStyleChange={setShodanStyle}
isMinimized={leftShodanMinimized}
onMinimizedChange={setLeftShodanMinimized}
/>
</div>
)}
{/* 4. AI INTEL (Below Shodan) */}
<div className="contents" style={{ direction: 'ltr' }}>
<AIIntelPanel
onFlyTo={handleFlyTo}
pinPlacementMode={pinPlacementMode}
onPinPlacementModeChange={setPinPlacementMode}
/>
</div>
{secondaryBootReady && (
<div className="contents" style={{ direction: 'ltr' }}>
<AIIntelPanel
onFlyTo={handleFlyTo}
pinPlacementMode={pinPlacementMode}
onPinPlacementModeChange={setPinPlacementMode}
/>
</div>
)}
</motion.div>
{/* LEFT SIDEBAR TOGGLE TAB — aligns with Data Layers section */}
@@ -647,11 +705,13 @@ export default function Dashboard() {
{/* GLOBAL TICKER REPLACES MARKETS PANEL - RENDERED OUTSIDE THIS DIV */}
{/* EVENT TIMELINE */}
<div className={`flex-shrink-0 ${rightFocusedPanel && rightFocusedPanel !== 'predictions' ? 'hidden' : ''}`}>
<ErrorBoundary name="TimelinePanel">
<TimelinePanel />
</ErrorBoundary>
</div>
{secondaryBootReady && (
<div className={`flex-shrink-0 ${rightFocusedPanel && rightFocusedPanel !== 'predictions' ? 'hidden' : ''}`}>
<ErrorBoundary name="TimelinePanel">
<TimelinePanel />
</ErrorBoundary>
</div>
)}
{/* DATA FILTERS */}
<div className={`flex-shrink-0 ${rightFocusedPanel && rightFocusedPanel !== 'filters' ? 'hidden' : ''}`}>
+8 -2
View File
@@ -345,6 +345,7 @@ const MaplibreViewer = ({
const data = useMemo(() => ({ ...coreData, ...extraData }) as DashboardData, [coreData, extraData]);
const mapRef = useRef<MapRef>(null);
const mapInitRef = useRef(false);
const [mapReady, setMapReady] = useState(false);
const { theme } = useTheme();
const mapThemeStyle = useMemo<maplibregl.StyleSpecification>(
() => (theme === 'light' ? lightStyle : darkStyle) as maplibregl.StyleSpecification,
@@ -914,15 +915,20 @@ const MaplibreViewer = ({
// Load Images into the Map Style once loaded
const onMapLoad = useCallback((e: { target: maplibregl.Map }) => {
initializeMap(e.target);
setMapReady(true);
}, [initializeMap]);
const onMapStyleData = useCallback((e: { target: maplibregl.Map }) => {
initializeMap(e.target);
setMapReady(true);
}, [initializeMap]);
useEffect(() => {
const map = mapRef.current?.getMap();
if (map) initializeMap(map);
if (map) {
initializeMap(map);
setMapReady(true);
}
}, [initializeMap, theme]);
// Build a set of tracked icao24s to exclude from other flight layers
@@ -1561,7 +1567,7 @@ const MaplibreViewer = ({
}, [activeLayers.uap_sightings, activeLayers.wastewater, theme]);
// --- Imperative source updates: bypass React reconciliation for GeoJSON layers ---
const mapForHook = mapRef.current;
const mapForHook = mapReady ? mapRef.current : null;
useImperativeSource(mapForHook, 'commercial-flights', commFlightsGeoJSON);
useImperativeSource(mapForHook, 'private-flights', privFlightsGeoJSON);
useImperativeSource(mapForHook, 'private-jets', privJetsGeoJSON);
@@ -3915,7 +3915,7 @@ export function useMeshChatController({
wormholeEnabled &&
wormholeReadyState &&
!selectedGateAccessReady) ||
((activeTab === 'infonet' || activeTab === 'meshtastic') && anonymousPublicBlocked) ||
(activeTab === 'infonet' && anonymousPublicBlocked) ||
(activeTab === 'dms' &&
(dmView !== 'chat' ||
!selectedContact ||
@@ -34,7 +34,7 @@ export function useImperativeSource(
};
const pushWhenReady = () => {
let attemptsRemaining = 20;
let attemptsRemaining = 150;
const tryPush = () => {
if (cancelled) return;
@@ -62,6 +62,7 @@ export function useImperativeSource(
pushWhenReady();
};
rawMap.on('load', handleStyleData);
rawMap.on('styledata', handleStyleData);
// Skip redundant writes for unchanged references, but keep the styledata
@@ -73,6 +74,7 @@ export function useImperativeSource(
return () => {
cancelled = true;
rawMap.off('load', handleStyleData);
rawMap.off('styledata', handleStyleData);
if (timerRef.current) clearTimeout(timerRef.current);
if (retryTimerRef.current) clearTimeout(retryTimerRef.current);
+3 -1
View File
@@ -35,6 +35,7 @@ interface AgentAction {
export function useAgentActions(
onShowImage: (coords: { lat: number; lng: number }) => void,
onFlyTo?: (coords: { lat: number; lng: number; zoom?: number }) => void,
enabled = true,
) {
const onShowImageRef = useRef(onShowImage);
onShowImageRef.current = onShowImage;
@@ -70,9 +71,10 @@ export function useAgentActions(
useEffect(() => {
// Poll every 3 seconds — lightweight endpoint, ~50 bytes when empty
if (!enabled) return;
const interval = setInterval(poll, 3000);
// Initial poll on mount
poll();
return () => clearInterval(interval);
}, [poll]);
}, [enabled, poll]);
}
+46 -8
View File
@@ -59,6 +59,8 @@ type FastDataProbe = {
ships?: unknown[];
sigint?: unknown[];
cctv?: unknown[];
news?: unknown[];
threat_level?: unknown;
};
function hasMeaningfulFastData(json: FastDataProbe): boolean {
@@ -100,11 +102,37 @@ export function useDataPolling() {
_slowEtagRef = slowEtag;
let hasData = false;
let fetchedStartupFastPayload = false;
let fastTimerId: ReturnType<typeof setTimeout> | null = null;
let slowTimerId: ReturnType<typeof setTimeout> | null = null;
const fastAbortRef = { current: null as AbortController | null };
const slowAbortRef = { current: null as AbortController | null };
const fetchCriticalBootstrap = async () => {
try {
const res = await fetch(`${API_BASE}/api/bootstrap/critical`, {
headers: { Accept: 'application/json' },
});
if (res.ok) {
setStoreBackendStatus('connected');
const json = await res.json();
mergeData(json);
if (hasMeaningfulFastData(json) || (json.news?.length || 0) > 0 || json.threat_level) {
hasData = true;
}
}
} catch (e) {
const aborted =
typeof e === 'object' &&
e !== null &&
'name' in e &&
(e as { name?: string }).name === 'AbortError';
if (!aborted) {
console.warn("Critical bootstrap fetch will retry via live polling", e);
}
}
};
const fetchFastData = async () => {
if (fastTimerId) {
clearTimeout(fastTimerId);
@@ -116,9 +144,11 @@ export function useDataPolling() {
const controller = new AbortController();
fastAbortRef.current = controller;
try {
const useStartupPayload = !fetchedStartupFastPayload && !fastEtag.current;
const headers: Record<string, string> = {};
if (fastEtag.current) headers['If-None-Match'] = fastEtag.current;
const res = await fetch(`${API_BASE}/api/live-data/fast`, {
if (!useStartupPayload && fastEtag.current) headers['If-None-Match'] = fastEtag.current;
const url = `${API_BASE}/api/live-data/fast${useStartupPayload ? '?initial=1' : ''}`;
const res = await fetch(url, {
headers,
signal: controller.signal,
});
@@ -129,7 +159,10 @@ export function useDataPolling() {
}
if (res.ok) {
setStoreBackendStatus('connected');
fastEtag.current = res.headers.get('etag') || null;
// Do not keep the capped startup ETag. The next steady poll should
// request the full fast dataset and replace the representative first paint.
fastEtag.current = useStartupPayload ? null : res.headers.get('etag') || null;
if (useStartupPayload) fetchedStartupFastPayload = true;
const json = await res.json();
mergeData(json);
if (hasMeaningfulFastData(json)) hasData = true;
@@ -141,7 +174,7 @@ export function useDataPolling() {
'name' in e &&
(e as { name?: string }).name === 'AbortError';
if (!aborted) {
console.error("Failed fetching fast live data", e);
console.warn("Fast live data fetch will retry after runtime is reachable", e);
setStoreBackendStatus('disconnected');
}
} finally {
@@ -177,7 +210,7 @@ export function useDataPolling() {
'name' in e &&
(e as { name?: string }).name === 'AbortError';
if (!aborted) {
console.error("Failed fetching slow live data", e);
console.warn("Slow live data fetch will retry after runtime is reachable", e);
}
} finally {
if (slowAbortRef.current === controller) {
@@ -191,7 +224,8 @@ export function useDataPolling() {
const scheduleNext = (tier: 'fast' | 'slow') => {
if (tier === 'fast') {
const delay = hasData ? 15000 : 3000; // 3s startup retry → 15s steady state
fastTimerId = setTimeout(fetchFastData, delay);
const needsFullFastPayload = fetchedStartupFastPayload && !fastEtag.current;
fastTimerId = setTimeout(fetchFastData, needsFullFastPayload ? 750 : delay);
} else {
const delay = hasData ? 120000 : 5000; // 5s startup retry → 120s steady state
slowTimerId = setTimeout(fetchSlowData, delay);
@@ -208,8 +242,12 @@ export function useDataPolling() {
};
window.addEventListener(LAYER_TOGGLE_EVENT, onLayerToggle);
fetchFastData();
fetchSlowData();
void (async () => {
await fetchCriticalBootstrap();
fetchFastData();
// Let the bootstrap/fast payload paint before competing with the slow tier.
slowTimerId = setTimeout(fetchSlowData, 5000);
})();
return () => {
window.removeEventListener(LAYER_TOGGLE_EVENT, onLayerToggle);
+2 -2
View File
@@ -387,7 +387,7 @@ export async function refreshSnapshotList(): Promise<void> {
const json = await res.json();
updateTimelineFromSnapshots(sortSnapshots(json.snapshots || []));
} catch (e) {
console.error('Time Machine: failed to fetch snapshots', e);
console.warn('Time Machine snapshots will retry after runtime is reachable', e);
}
}
@@ -402,7 +402,7 @@ export async function refreshHourlyIndex(): Promise<void> {
setState({ hourlyIndex: json.hours || {} });
}
} catch (e) {
console.error('Time Machine: failed to fetch hourly index', e);
console.warn('Time Machine hourly index will retry after runtime is reachable', e);
}
}
+2
View File
@@ -826,6 +826,8 @@ export interface DashboardData {
cctv_total?: number;
satnogs_total?: number;
tinygs_total?: number;
bootstrap_ready?: boolean;
bootstrap_payload?: boolean;
sigint_totals?: {
total?: number;
meshtastic?: number;