diff --git a/templates/satellite_dashboard.html b/templates/satellite_dashboard.html index 2e2173b..605cb5e 100644 --- a/templates/satellite_dashboard.html +++ b/templates/satellite_dashboard.html @@ -785,6 +785,7 @@ let _dashboardRetryAttempts = 0; const RECEIVER_STORAGE_KEY = 'satellite.dashboard.receiver'; const DASHBOARD_FETCH_TIMEOUT_MS = 30000; + const SAT_DRAWER_FETCH_TIMEOUT_MS = 8000; const BUILTIN_TX_FALLBACK = { 25544: [ { description: 'APRS digipeater', downlink_low: 145.825, downlink_high: 145.825, uplink_low: null, uplink_high: null, mode: 'FM AX.25', baud: 1200, status: 'active', type: 'beacon', service: 'Packet' }, @@ -806,6 +807,22 @@ const satColors = ['#00ffff', '#9370DB', '#ff00ff', '#00ff00', '#ff6600', '#ffff00', '#ff69b4', '#7b68ee']; + async function fetchJsonWithTimeout(url, options = {}, timeoutMs = SAT_DRAWER_FETCH_TIMEOUT_MS) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + try { + const response = await fetch(url, { + credentials: 'same-origin', + ...options, + signal: controller.signal, + }); + const data = await response.json(); + return { response, data }; + } finally { + clearTimeout(timeoutId); + } + } + function loadDashboardSatellites() { const btn = document.getElementById('satRefreshBtn'); if (btn) { @@ -813,9 +830,8 @@ void btn.offsetWidth; // force reflow to restart animation btn.classList.add('spinning'); } - fetch('/satellite/tracked?enabled=true', { credentials: 'same-origin' }) - .then(r => r.json()) - .then(data => { + fetchJsonWithTimeout('/satellite/tracked?enabled=true') + .then(({ data }) => { const prevSelected = selectedSatellite; const newSats = { 25544: { name: 'ISS (ZARYA)', color: satellites[25544]?.color || satColors[0] }, @@ -855,10 +871,28 @@ fetchCurrentTelemetry(); if (window.gsLoadOutputs) window.gsLoadOutputs(); if (window.gsOnSatelliteChange) window.gsOnSatelliteChange(); + if (!trackedSatelliteCatalog.length) { + trackedSatelliteCatalog = Object.entries(newSats).map(([norad, sat]) => ({ + norad_id: parseInt(norad, 10), + name: sat.name, + builtin: true, + enabled: true, + })); + renderTrackedSatelliteCatalog(); + } scheduleDashboardDataRetry(2500); }) .catch(() => { showSatelliteCommandStatus('Tracked-satellite refresh failed. Retrying with the current selection.', 'warn'); + if (!trackedSatelliteCatalog.length) { + trackedSatelliteCatalog = Object.entries(satellites).map(([norad, sat]) => ({ + norad_id: parseInt(norad, 10), + name: sat.name, + builtin: true, + enabled: true, + })); + renderTrackedSatelliteCatalog(); + } calculatePasses(); fetchCurrentTelemetry(); loadTransmitters(selectedSatellite); @@ -1497,7 +1531,41 @@ return layer; } + async function upgradeGroundTilesFromSettings(fallbackTiles) { + if (typeof Settings === 'undefined' || !groundMap) return; + + try { + await Settings.init(); + if (!groundMap) return; + + const configuredLayer = Settings.createTileLayer(); + let tileLoaded = false; + + configuredLayer.once('load', () => { + tileLoaded = true; + if (groundMap && fallbackTiles && groundMap.hasLayer(fallbackTiles)) { + groundMap.removeLayer(fallbackTiles); + } + groundMap.invalidateSize(false); + }); + + configuredLayer.on('tileerror', () => { + if (!tileLoaded) { + console.warn('Satellite tile layer failed to load, keeping fallback grid'); + } + }); + + configuredLayer.addTo(groundMap); + Settings.registerMap(groundMap); + } catch (e) { + console.warn('Satellite: Settings/tile upgrade failed, using fallback grid:', e); + } + } + async function initGroundMap() { + const container = document.getElementById('groundMap'); + if (!container || container._leaflet_id) return; + groundMap = L.map('groundMap', { center: [20, 0], zoom: 2, @@ -1512,26 +1580,15 @@ // when internet map providers are slow or unreachable. const fallbackTiles = createFallbackGridLayer().addTo(groundMap); - // Upgrade tiles in background via Settings (with timeout fallback) - if (typeof Settings !== 'undefined') { - try { - await Promise.race([ - Settings.init(), - new Promise((_, reject) => setTimeout(() => reject(new Error('Settings timeout')), 5000)) - ]); - groundMap.removeLayer(fallbackTiles); - Settings.createTileLayer().addTo(groundMap); - Settings.registerMap(groundMap); - } catch (e) { - console.warn('Satellite: Settings init failed/timed out, using fallback tiles:', e); - } - } + upgradeGroundTilesFromSettings(fallbackTiles); const lat = parseFloat(document.getElementById('obsLat')?.value); const lon = parseFloat(document.getElementById('obsLon')?.value); if (!Number.isNaN(lat) && !Number.isNaN(lon)) { groundMap.setView([lat, lon], 3); } + requestAnimationFrame(() => groundMap?.invalidateSize(false)); + setTimeout(() => groundMap?.invalidateSize(false), 250); updateMapModeButtons(); updateMapTrackSummary(); } @@ -2246,8 +2303,7 @@ const noteEl = document.getElementById('gsReceiverNote'); if (!select) return; try { - const response = await fetch('/devices', { credentials: 'same-origin' }); - const devices = await response.json(); + const { data: devices } = await fetchJsonWithTimeout('/devices'); receiverDevices = Array.isArray(devices) ? devices : []; if (!receiverDevices.length) { select.innerHTML = ''; @@ -2270,9 +2326,18 @@ } onReceiverSelectionChange(); } catch (_) { - select.innerHTML = ''; - if (typeEl) typeEl.textContent = 'RTLSDR'; - if (noteEl) noteEl.textContent = 'Device detection failed. Ground station will try receiver 0.'; + const saved = localStorage.getItem(RECEIVER_STORAGE_KEY); + if (saved) { + const [savedType, savedIndex] = saved.split(':'); + select.innerHTML = ``; + if (typeEl) typeEl.textContent = String(savedType || 'rtlsdr').toUpperCase(); + if (noteEl) noteEl.textContent = 'Live SDR detection is taking too long. Using the last saved receiver for now.'; + } else { + select.innerHTML = ''; + if (typeEl) typeEl.textContent = 'RTLSDR'; + if (noteEl) noteEl.textContent = 'Live SDR detection timed out. Ground station will try receiver 0 until the next refresh.'; + } + setTimeout(loadReceiverDevices, 6000); updateMissionDrawerInfo(); } } @@ -2300,12 +2365,21 @@ async function loadTrackedSatelliteCatalog() { try { - const response = await fetch('/satellite/tracked', { credentials: 'same-origin' }); - const data = await response.json(); + const { data } = await fetchJsonWithTimeout('/satellite/tracked'); if (data.status === 'success' && Array.isArray(data.satellites)) { trackedSatelliteCatalog = data.satellites; } - } catch (_) {} + } catch (_) { + if (!trackedSatelliteCatalog.length) { + trackedSatelliteCatalog = Object.entries(satellites).map(([norad, sat]) => ({ + norad_id: parseInt(norad, 10), + name: sat.name, + builtin: true, + enabled: true, + })); + } + setTimeout(loadTrackedSatelliteCatalog, 6000); + } renderTrackedSatelliteCatalog(); }