mirror of
https://github.com/smittix/intercept.git
synced 2026-07-07 09:08:12 -07:00
Stabilize satellite dashboard refreshes
This commit is contained in:
@@ -777,14 +777,19 @@
|
|||||||
let _passRequestId = 0;
|
let _passRequestId = 0;
|
||||||
let _passAbortController = null;
|
let _passAbortController = null;
|
||||||
let _passTimeoutId = null;
|
let _passTimeoutId = null;
|
||||||
|
let _activePassRequestKey = null;
|
||||||
let trackedSatelliteCatalog = [];
|
let trackedSatelliteCatalog = [];
|
||||||
let receiverDevices = [];
|
let receiverDevices = [];
|
||||||
let packetHistory = [];
|
let packetHistory = [];
|
||||||
let packetConsoleCollapsed = false;
|
let packetConsoleCollapsed = false;
|
||||||
let _dashboardRetryTimer = null;
|
let _dashboardRetryTimer = null;
|
||||||
let _dashboardRetryAttempts = 0;
|
let _dashboardRetryAttempts = 0;
|
||||||
|
const passCache = new Map();
|
||||||
|
const telemetryCache = new Map();
|
||||||
|
const transmitterCache = new Map();
|
||||||
const RECEIVER_STORAGE_KEY = 'satellite.dashboard.receiver';
|
const RECEIVER_STORAGE_KEY = 'satellite.dashboard.receiver';
|
||||||
const DASHBOARD_FETCH_TIMEOUT_MS = 30000;
|
const DASHBOARD_FETCH_TIMEOUT_MS = 30000;
|
||||||
|
const PASS_FETCH_TIMEOUT_MS = 90000;
|
||||||
const SAT_DRAWER_FETCH_TIMEOUT_MS = 8000;
|
const SAT_DRAWER_FETCH_TIMEOUT_MS = 8000;
|
||||||
const BUILTIN_TX_FALLBACK = {
|
const BUILTIN_TX_FALLBACK = {
|
||||||
25544: [
|
25544: [
|
||||||
@@ -885,6 +890,124 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getObserverCoords() {
|
||||||
|
const lat = parseFloat(document.getElementById('obsLat')?.value);
|
||||||
|
const lon = parseFloat(document.getElementById('obsLon')?.value);
|
||||||
|
return {
|
||||||
|
lat,
|
||||||
|
lon,
|
||||||
|
valid: Number.isFinite(lat) && Number.isFinite(lon)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPassCacheKey(noradId = selectedSatellite) {
|
||||||
|
const { lat, lon, valid } = getObserverCoords();
|
||||||
|
if (!valid) return `sat:${noradId}:observer:unknown`;
|
||||||
|
return `sat:${noradId}:observer:${lat.toFixed(3)}:${lon.toFixed(3)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActivePassRequestKey(noradId = selectedSatellite) {
|
||||||
|
return getPassCacheKey(noradId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheCurrentPasses(noradId = selectedSatellite, passList = passes) {
|
||||||
|
if (!Array.isArray(passList) || !passList.length) return;
|
||||||
|
passCache.set(getPassCacheKey(noradId), {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
passes: passList
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCachedPasses(noradId = selectedSatellite) {
|
||||||
|
return passCache.get(getPassCacheKey(noradId)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheLivePosition(noradId = selectedSatellite, position = latestLivePosition) {
|
||||||
|
if (!position) return;
|
||||||
|
telemetryCache.set(String(noradId), {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
position
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCachedLivePosition(noradId = selectedSatellite) {
|
||||||
|
return telemetryCache.get(String(noradId)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheTransmitters(noradId, txList) {
|
||||||
|
if (!noradId || !Array.isArray(txList) || !txList.length) return;
|
||||||
|
transmitterCache.set(String(noradId), {
|
||||||
|
timestamp: Date.now(),
|
||||||
|
transmitters: txList
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCachedTransmitters(noradId = selectedSatellite) {
|
||||||
|
return transmitterCache.get(String(noradId)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTelemetryPosition(pos, options = {}) {
|
||||||
|
const { updateVisible = false } = options;
|
||||||
|
if (!pos) return;
|
||||||
|
latestLivePosition = pos;
|
||||||
|
cacheLivePosition(selectedSatellite, pos);
|
||||||
|
|
||||||
|
const telLat = document.getElementById('telLat');
|
||||||
|
const telLon = document.getElementById('telLon');
|
||||||
|
const telAlt = document.getElementById('telAlt');
|
||||||
|
const telEl = document.getElementById('telEl');
|
||||||
|
const telAz = document.getElementById('telAz');
|
||||||
|
const telDist = document.getElementById('telDist');
|
||||||
|
if (telLat) telLat.textContent = (pos.lat ?? 0).toFixed(4) + '°';
|
||||||
|
if (telLon) telLon.textContent = (pos.lon ?? 0).toFixed(4) + '°';
|
||||||
|
if (telAlt) telAlt.textContent = (pos.altitude ?? pos.alt ?? 0).toFixed(0) + ' km';
|
||||||
|
if (telEl) telEl.textContent = (pos.elevation ?? pos.el ?? 0).toFixed(1) + '°';
|
||||||
|
if (telAz) telAz.textContent = (pos.azimuth ?? pos.az ?? 0).toFixed(1) + '°';
|
||||||
|
if (telDist) telDist.textContent = (pos.distance ?? pos.dist ?? 0).toFixed(0) + ' km';
|
||||||
|
|
||||||
|
if (selectedPass == null && (pos.azimuth ?? pos.az) != null && (pos.elevation ?? pos.el) != null) {
|
||||||
|
drawPolarPlotWithPosition(
|
||||||
|
pos.azimuth ?? pos.az,
|
||||||
|
pos.elevation ?? pos.el,
|
||||||
|
satellites[selectedSatellite]?.color || '#00d4ff'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
renderMapTrackOverlays();
|
||||||
|
updateMapTrackSummary();
|
||||||
|
|
||||||
|
if (updateVisible) {
|
||||||
|
const visEl = document.getElementById('statVisible');
|
||||||
|
if (visEl && Number.isFinite(pos.visibleCount)) visEl.textContent = String(pos.visibleCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreSatelliteStateFromCache() {
|
||||||
|
const cachedPasses = getCachedPasses(selectedSatellite);
|
||||||
|
if (cachedPasses?.passes?.length) {
|
||||||
|
passes = cachedPasses.passes;
|
||||||
|
renderPassList();
|
||||||
|
updateStats();
|
||||||
|
if (!Number.isInteger(selectedPass) || !passes[selectedPass]) {
|
||||||
|
selectedPass = 0;
|
||||||
|
}
|
||||||
|
if (passes[selectedPass]) {
|
||||||
|
selectPass(selectedPass);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedTelemetry = getCachedLivePosition(selectedSatellite);
|
||||||
|
if (cachedTelemetry?.position) {
|
||||||
|
applyTelemetryPosition(cachedTelemetry.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedTransmitters = getCachedTransmitters(selectedSatellite);
|
||||||
|
if (cachedTransmitters?.transmitters?.length) {
|
||||||
|
renderTransmitters(cachedTransmitters.transmitters);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMissionDrawerInfo();
|
||||||
|
}
|
||||||
|
|
||||||
function loadDashboardSatellites() {
|
function loadDashboardSatellites() {
|
||||||
const btn = document.getElementById('satRefreshBtn');
|
const btn = document.getElementById('satRefreshBtn');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
@@ -927,6 +1050,7 @@
|
|||||||
}
|
}
|
||||||
selectedSatellite = parseInt(select.value);
|
selectedSatellite = parseInt(select.value);
|
||||||
clearTelemetry();
|
clearTelemetry();
|
||||||
|
restoreSatelliteStateFromCache();
|
||||||
updateMissionDrawerInfo();
|
updateMissionDrawerInfo();
|
||||||
loadTransmitters(selectedSatellite);
|
loadTransmitters(selectedSatellite);
|
||||||
calculatePasses();
|
calculatePasses();
|
||||||
@@ -985,6 +1109,7 @@
|
|||||||
|
|
||||||
clearTelemetry();
|
clearTelemetry();
|
||||||
updateMapTrackSummary();
|
updateMapTrackSummary();
|
||||||
|
restoreSatelliteStateFromCache();
|
||||||
updateMissionDrawerInfo();
|
updateMissionDrawerInfo();
|
||||||
loadTransmitters(selectedSatellite);
|
loadTransmitters(selectedSatellite);
|
||||||
calculatePasses();
|
calculatePasses();
|
||||||
@@ -1065,31 +1190,7 @@
|
|||||||
if (!pos) {
|
if (!pos) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
applyTelemetryPosition({ ...pos, visibleCount }, { updateVisible: true });
|
||||||
latestLivePosition = pos;
|
|
||||||
|
|
||||||
// Update telemetry panel
|
|
||||||
const telLat = document.getElementById('telLat');
|
|
||||||
const telLon = document.getElementById('telLon');
|
|
||||||
const telAlt = document.getElementById('telAlt');
|
|
||||||
const telEl = document.getElementById('telEl');
|
|
||||||
const telAz = document.getElementById('telAz');
|
|
||||||
const telDist = document.getElementById('telDist');
|
|
||||||
if (telLat) telLat.textContent = (pos.lat ?? 0).toFixed(4) + '°';
|
|
||||||
if (telLon) telLon.textContent = (pos.lon ?? 0).toFixed(4) + '°';
|
|
||||||
if (telAlt) telAlt.textContent = (pos.altitude ?? 0).toFixed(0) + ' km';
|
|
||||||
if (telEl) telEl.textContent = (pos.elevation ?? 0).toFixed(1) + '°';
|
|
||||||
if (telAz) telAz.textContent = (pos.azimuth ?? 0).toFixed(1) + '°';
|
|
||||||
if (telDist) telDist.textContent = (pos.distance ?? 0).toFixed(0) + ' km';
|
|
||||||
if (selectedPass == null && pos.azimuth != null && pos.elevation != null) {
|
|
||||||
drawPolarPlotWithPosition(
|
|
||||||
pos.azimuth,
|
|
||||||
pos.elevation,
|
|
||||||
satellites[selectedSatellite]?.color || '#00d4ff'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
renderMapTrackOverlays();
|
|
||||||
updateMapTrackSummary();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function findSelectedPosition(positions) {
|
function findSelectedPosition(positions) {
|
||||||
@@ -1144,10 +1245,6 @@
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.status !== 'success' || !Array.isArray(data.positions)) return;
|
if (data.status !== 'success' || !Array.isArray(data.positions)) return;
|
||||||
if (!findSelectedPosition(data.positions)) {
|
if (!findSelectedPosition(data.positions)) {
|
||||||
latestLivePosition = null;
|
|
||||||
clearTelemetry();
|
|
||||||
renderMapTrackOverlays();
|
|
||||||
updateMapTrackSummary();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleLivePositions(data.positions);
|
handleLivePositions(data.positions);
|
||||||
@@ -1694,8 +1791,14 @@
|
|||||||
const requestId = ++_passRequestId;
|
const requestId = ++_passRequestId;
|
||||||
const lat = parseFloat(document.getElementById('obsLat').value);
|
const lat = parseFloat(document.getElementById('obsLat').value);
|
||||||
const lon = parseFloat(document.getElementById('obsLon').value);
|
const lon = parseFloat(document.getElementById('obsLon').value);
|
||||||
|
const requestKey = getActivePassRequestKey(selectedSatellite);
|
||||||
const container = document.getElementById('passList');
|
const container = document.getElementById('passList');
|
||||||
const button = document.querySelector('.controls-bar .btn.primary');
|
const button = document.querySelector('.controls-bar .btn.primary');
|
||||||
|
|
||||||
|
if (_passAbortController && _activePassRequestKey === requestKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (container) {
|
if (container) {
|
||||||
container.innerHTML = '<div style="text-align:center;color:var(--text-secondary);padding:20px;">Calculating passes...</div>';
|
container.innerHTML = '<div style="text-align:center;color:var(--text-secondary);padding:20px;">Calculating passes...</div>';
|
||||||
}
|
}
|
||||||
@@ -1712,11 +1815,12 @@
|
|||||||
clearTimeout(_passTimeoutId);
|
clearTimeout(_passTimeoutId);
|
||||||
_passTimeoutId = null;
|
_passTimeoutId = null;
|
||||||
}
|
}
|
||||||
|
_activePassRequestKey = requestKey;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
_passAbortController = controller;
|
_passAbortController = controller;
|
||||||
_passTimeoutId = setTimeout(() => controller.abort('timeout'), DASHBOARD_FETCH_TIMEOUT_MS);
|
_passTimeoutId = setTimeout(() => controller.abort('timeout'), PASS_FETCH_TIMEOUT_MS);
|
||||||
const response = await fetch('/satellite/predict', {
|
const response = await fetch('/satellite/predict', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
@@ -1737,6 +1841,9 @@
|
|||||||
if (_passAbortController === controller) {
|
if (_passAbortController === controller) {
|
||||||
_passAbortController = null;
|
_passAbortController = null;
|
||||||
}
|
}
|
||||||
|
if (_activePassRequestKey === requestKey) {
|
||||||
|
_activePassRequestKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
const contentType = response.headers.get('Content-Type') || '';
|
const contentType = response.headers.get('Content-Type') || '';
|
||||||
if (!contentType.includes('application/json')) {
|
if (!contentType.includes('application/json')) {
|
||||||
@@ -1746,6 +1853,7 @@
|
|||||||
if (requestId !== _passRequestId) return;
|
if (requestId !== _passRequestId) return;
|
||||||
if (data.status === 'success') {
|
if (data.status === 'success') {
|
||||||
passes = data.passes;
|
passes = data.passes;
|
||||||
|
cacheCurrentPasses(selectedSatellite, passes);
|
||||||
renderPassList();
|
renderPassList();
|
||||||
updateStats();
|
updateStats();
|
||||||
updateMissionDrawerInfo();
|
updateMissionDrawerInfo();
|
||||||
@@ -1766,14 +1874,25 @@
|
|||||||
|
|
||||||
document.getElementById('trackingStatus').textContent = 'TRACKING';
|
document.getElementById('trackingStatus').textContent = 'TRACKING';
|
||||||
document.getElementById('trackingDot').style.background = 'var(--accent-green)';
|
document.getElementById('trackingDot').style.background = 'var(--accent-green)';
|
||||||
|
_dashboardRetryAttempts = 0;
|
||||||
} else {
|
} else {
|
||||||
passes = [];
|
const cached = getCachedPasses(selectedSatellite);
|
||||||
renderPassList();
|
if (cached?.passes?.length) {
|
||||||
updateMissionDrawerInfo();
|
passes = cached.passes;
|
||||||
document.getElementById('trackingStatus').textContent = 'ERROR';
|
renderPassList();
|
||||||
document.getElementById('trackingDot').style.background = 'var(--accent-red)';
|
updateStats();
|
||||||
if (container) {
|
updateMissionDrawerInfo();
|
||||||
container.innerHTML = `<div style="text-align:center;color:var(--text-secondary);padding:20px;">${data.message || 'Failed to calculate passes'}</div>`;
|
document.getElementById('trackingStatus').textContent = 'TRACKING';
|
||||||
|
document.getElementById('trackingDot').style.background = 'var(--accent-green)';
|
||||||
|
} else {
|
||||||
|
passes = [];
|
||||||
|
renderPassList();
|
||||||
|
updateMissionDrawerInfo();
|
||||||
|
document.getElementById('trackingStatus').textContent = 'ERROR';
|
||||||
|
document.getElementById('trackingDot').style.background = 'var(--accent-red)';
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = `<div style="text-align:center;color:var(--text-secondary);padding:20px;">${data.message || 'Failed to calculate passes'}</div>`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1785,22 +1904,37 @@
|
|||||||
if (_passAbortController && _passAbortController.signal.aborted) {
|
if (_passAbortController && _passAbortController.signal.aborted) {
|
||||||
_passAbortController = null;
|
_passAbortController = null;
|
||||||
}
|
}
|
||||||
|
if (_activePassRequestKey === requestKey) {
|
||||||
|
_activePassRequestKey = null;
|
||||||
|
}
|
||||||
if (requestId !== _passRequestId) return;
|
if (requestId !== _passRequestId) return;
|
||||||
if (isAbort) {
|
if (isAbort) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.error('Pass calculation error:', err);
|
console.error('Pass calculation error:', err);
|
||||||
passes = [];
|
const cached = getCachedPasses(selectedSatellite);
|
||||||
updateMissionDrawerInfo();
|
if (cached?.passes?.length) {
|
||||||
if (container) {
|
passes = cached.passes;
|
||||||
container.innerHTML = '<div style="text-align:center;color:var(--text-secondary);padding:20px;">Failed to calculate passes</div>';
|
|
||||||
} else {
|
|
||||||
renderPassList();
|
renderPassList();
|
||||||
|
updateStats();
|
||||||
|
document.getElementById('trackingStatus').textContent = 'TRACKING';
|
||||||
|
document.getElementById('trackingDot').style.background = 'var(--accent-green)';
|
||||||
|
} else {
|
||||||
|
passes = [];
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = '<div style="text-align:center;color:var(--text-secondary);padding:20px;">Failed to calculate passes</div>';
|
||||||
|
} else {
|
||||||
|
renderPassList();
|
||||||
|
}
|
||||||
|
document.getElementById('trackingStatus').textContent = 'OFFLINE';
|
||||||
|
document.getElementById('trackingDot').style.background = 'var(--accent-red)';
|
||||||
}
|
}
|
||||||
document.getElementById('trackingStatus').textContent = 'OFFLINE';
|
updateMissionDrawerInfo();
|
||||||
document.getElementById('trackingDot').style.background = 'var(--accent-red)';
|
|
||||||
scheduleDashboardDataRetry(3500);
|
scheduleDashboardDataRetry(3500);
|
||||||
} finally {
|
} finally {
|
||||||
|
if (_activePassRequestKey === requestKey) {
|
||||||
|
_activePassRequestKey = null;
|
||||||
|
}
|
||||||
if (requestId === _passRequestId && button) {
|
if (requestId === _passRequestId && button) {
|
||||||
button.disabled = false;
|
button.disabled = false;
|
||||||
button.textContent = 'CALCULATE';
|
button.textContent = 'CALCULATE';
|
||||||
@@ -2252,12 +2386,20 @@
|
|||||||
const txList = (data.transmitters && data.transmitters.length)
|
const txList = (data.transmitters && data.transmitters.length)
|
||||||
? data.transmitters
|
? data.transmitters
|
||||||
: (BUILTIN_TX_FALLBACK[noradId] || []);
|
: (BUILTIN_TX_FALLBACK[noradId] || []);
|
||||||
|
cacheTransmitters(noradId, txList);
|
||||||
renderTransmitters(txList);
|
renderTransmitters(txList);
|
||||||
updateMissionDrawerInfo();
|
updateMissionDrawerInfo();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (requestId !== _txRequestId) return;
|
if (requestId !== _txRequestId) return;
|
||||||
|
const cached = getCachedTransmitters(noradId);
|
||||||
|
if (cached?.transmitters?.length) {
|
||||||
|
renderTransmitters(cached.transmitters);
|
||||||
|
updateMissionDrawerInfo();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const fallback = BUILTIN_TX_FALLBACK[noradId] || [];
|
const fallback = BUILTIN_TX_FALLBACK[noradId] || [];
|
||||||
if (fallback.length) {
|
if (fallback.length) {
|
||||||
|
cacheTransmitters(noradId, fallback);
|
||||||
renderTransmitters(fallback);
|
renderTransmitters(fallback);
|
||||||
updateMissionDrawerInfo();
|
updateMissionDrawerInfo();
|
||||||
return;
|
return;
|
||||||
@@ -2641,7 +2783,7 @@
|
|||||||
if (_dashboardRetryTimer) clearTimeout(_dashboardRetryTimer);
|
if (_dashboardRetryTimer) clearTimeout(_dashboardRetryTimer);
|
||||||
_dashboardRetryTimer = setTimeout(() => {
|
_dashboardRetryTimer = setTimeout(() => {
|
||||||
const txReady = !!document.querySelector('#transmittersList .tx-item');
|
const txReady = !!document.querySelector('#transmittersList .tx-item');
|
||||||
const needsPassRetry = passes.length === 0;
|
const needsPassRetry = passes.length === 0 && !_passAbortController;
|
||||||
const needsTelemetryRetry = !latestLivePosition;
|
const needsTelemetryRetry = !latestLivePosition;
|
||||||
const needsTxRetry = !txReady;
|
const needsTxRetry = !txReady;
|
||||||
if (!(needsPassRetry || needsTelemetryRetry || needsTxRetry)) {
|
if (!(needsPassRetry || needsTelemetryRetry || needsTxRetry)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user