diff --git a/static/css/satellite_dashboard.css b/static/css/satellite_dashboard.css index 4702b0d..96fd2de 100644 --- a/static/css/satellite_dashboard.css +++ b/static/css/satellite_dashboard.css @@ -560,12 +560,24 @@ body { /* Pass list */ .pass-list { - flex: 1; - min-height: 0; + flex: 0 0 auto; + min-height: 180px; display: flex; flex-direction: column; } +.gs-panel { + flex: 0 0 auto; + min-height: 240px; + display: flex; + flex-direction: column; +} + +.gs-panel .panel-content { + height: auto; + overflow-y: auto; +} + .pass-list-content { flex: 1; overflow-y: auto; diff --git a/templates/satellite_dashboard.html b/templates/satellite_dashboard.html index 08b077d..31d008c 100644 --- a/templates/satellite_dashboard.html +++ b/templates/satellite_dashboard.html @@ -602,6 +602,7 @@ let _txRequestId = 0; let _telemetryPollTimer = null; let _passRequestId = 0; + const DASHBOARD_FETCH_TIMEOUT_MS = 10000; 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' }, @@ -630,7 +631,7 @@ void btn.offsetWidth; // force reflow to restart animation btn.classList.add('spinning'); } - fetch('/satellite/tracked?enabled=true') + fetch('/satellite/tracked?enabled=true', { credentials: 'same-origin' }) .then(r => r.json()) .then(data => { const prevSelected = selectedSatellite; @@ -846,9 +847,13 @@ if (!Number.isFinite(lat) || !Number.isFinite(lon) || !selectedSatellite) return; try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), DASHBOARD_FETCH_TIMEOUT_MS); const response = await fetch('/satellite/position', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, + signal: controller.signal, body: JSON.stringify({ latitude: lat, longitude: lon, @@ -856,7 +861,10 @@ includeTrack: false }) }); + clearTimeout(timeout); if (!response.ok) return; + const contentType = response.headers.get('Content-Type') || ''; + if (!contentType.includes('application/json')) return; const data = await response.json(); if (data.status !== 'success' || !Array.isArray(data.positions)) return; if (!findSelectedPosition(data.positions)) { @@ -1124,9 +1132,13 @@ } try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), DASHBOARD_FETCH_TIMEOUT_MS); const response = await fetch('/satellite/predict', { method: 'POST', + credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, + signal: controller.signal, body: JSON.stringify({ latitude: lat, longitude: lon, @@ -1135,7 +1147,12 @@ satellites: [selectedSatellite] }) }); + clearTimeout(timeout); + const contentType = response.headers.get('Content-Type') || ''; + if (!contentType.includes('application/json')) { + throw new Error('Unexpected response while calculating passes'); + } const data = await response.json(); if (requestId !== _passRequestId) return; if (data.status === 'success') { @@ -1156,12 +1173,20 @@ renderPassList(); document.getElementById('trackingStatus').textContent = 'ERROR'; document.getElementById('trackingDot').style.background = 'var(--accent-red)'; + if (container) { + container.innerHTML = `
${data.message || 'Failed to calculate passes'}
`; + } } } catch (err) { if (requestId !== _passRequestId) return; console.error('Pass calculation error:', err); passes = []; - renderPassList(); + if (container) { + const timedOut = err && (err.name === 'AbortError' || String(err).includes('AbortError')); + container.innerHTML = `
${timedOut ? 'Timed out calculating passes' : 'Failed to calculate passes'}
`; + } else { + renderPassList(); + } document.getElementById('trackingStatus').textContent = 'OFFLINE'; document.getElementById('trackingDot').style.background = 'var(--accent-red)'; } finally { @@ -1651,7 +1676,10 @@ try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 7000); - const r = await fetch(`/satellite/transmitters/${noradId}`, { signal: controller.signal }); + const r = await fetch(`/satellite/transmitters/${noradId}`, { + signal: controller.signal, + credentials: 'same-origin' + }); clearTimeout(timeout); if (!r.ok) throw new Error(`HTTP ${r.status}`); const data = await r.json();