Fix dashboard startup regressions and mode utilities

This commit is contained in:
James Smith
2026-03-19 10:37:21 +00:00
parent 5f34d20287
commit 18b442eb21
15 changed files with 283 additions and 96 deletions
+1 -1
View File
@@ -3502,7 +3502,7 @@ class ModeManager:
stations_url = 'https://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=tle' stations_url = 'https://celestrak.org/NORAD/elements/gp.php?GROUP=weather&FORMAT=tle'
satellites = load.tle_file(stations_url) satellites = load.tle_file(stations_url)
ts = load.timescale() ts = load.timescale(builtin=True)
observer = Topos(latitude_degrees=lat, longitude_degrees=lon) observer = Topos(latitude_degrees=lat, longitude_degrees=lon)
logger.info(f"Satellite predictor: {len(satellites)} satellites loaded") logger.info(f"Satellite predictor: {len(satellites)} satellites loaded")
+2 -1
View File
@@ -729,7 +729,8 @@ def agent_management_page():
def network_monitor_page(): def network_monitor_page():
"""Render the network monitor page for multi-agent aggregated view.""" """Render the network monitor page for multi-agent aggregated view."""
from flask import render_template from flask import render_template
return render_template('network_monitor.html') from config import VERSION
return render_template('network_monitor.html', version=VERSION)
# ============================================================================= # =============================================================================
+73 -19
View File
@@ -83,6 +83,62 @@ def find_auto_rx() -> str | None:
return None return None
def _iter_auto_rx_python_candidates(auto_rx_path: str):
"""Yield plausible Python interpreters for radiosonde_auto_rx."""
auto_rx_abs = os.path.abspath(auto_rx_path)
auto_rx_dir = os.path.dirname(auto_rx_abs)
install_root = os.path.dirname(auto_rx_dir)
candidates = [
sys.executable,
os.path.join(install_root, 'venv', 'bin', 'python'),
os.path.join(install_root, '.venv', 'bin', 'python'),
os.path.join(auto_rx_dir, 'venv', 'bin', 'python'),
os.path.join(auto_rx_dir, '.venv', 'bin', 'python'),
shutil.which('python3'),
]
seen: set[str] = set()
for candidate in candidates:
if not candidate:
continue
candidate_abs = os.path.abspath(candidate)
if candidate_abs in seen:
continue
seen.add(candidate_abs)
if os.path.isfile(candidate_abs) and os.access(candidate_abs, os.X_OK):
yield candidate_abs
def _resolve_auto_rx_python(auto_rx_path: str) -> tuple[str | None, str, list[str]]:
"""Pick a Python interpreter that can import autorx.scan successfully."""
auto_rx_dir = os.path.dirname(os.path.abspath(auto_rx_path))
checked: list[str] = []
last_error = 'No usable Python interpreter found'
for python_bin in _iter_auto_rx_python_candidates(auto_rx_path):
checked.append(python_bin)
try:
dep_check = subprocess.run(
[python_bin, '-c', 'import autorx.scan'],
cwd=auto_rx_dir,
capture_output=True,
timeout=10,
)
except Exception as exc:
last_error = str(exc)
continue
if dep_check.returncode == 0:
return python_bin, '', checked
stderr_output = dep_check.stderr.decode('utf-8', errors='ignore').strip()
stdout_output = dep_check.stdout.decode('utf-8', errors='ignore').strip()
last_error = stderr_output or stdout_output or f'Interpreter exited with code {dep_check.returncode}'
return None, last_error, checked
def generate_station_cfg( def generate_station_cfg(
freq_min: float = 400.0, freq_min: float = 400.0,
freq_max: float = 406.0, freq_max: float = 406.0,
@@ -547,31 +603,29 @@ def start_radiosonde():
# Build command - auto_rx -c expects the path to station.cfg # Build command - auto_rx -c expects the path to station.cfg
cfg_abs = os.path.abspath(cfg_path) cfg_abs = os.path.abspath(cfg_path)
if auto_rx_path.endswith('.py'): if auto_rx_path.endswith('.py'):
cmd = [sys.executable, auto_rx_path, '-c', cfg_abs] selected_python, dep_error, checked_interpreters = _resolve_auto_rx_python(auto_rx_path)
if not selected_python:
logger.error(
"radiosonde_auto_rx dependency check failed across interpreters %s: %s",
checked_interpreters,
dep_error,
)
app_module.release_sdr_device(device_int, sdr_type_str)
checked_msg = ', '.join(checked_interpreters) if checked_interpreters else 'none'
return api_error(
'radiosonde_auto_rx dependencies not satisfied. '
'Install or repair its Python environment (missing packages such as semver). '
f'Checked interpreters: {checked_msg}. '
f'Last error: {dep_error[:500]}',
500,
)
cmd = [selected_python, auto_rx_path, '-c', cfg_abs]
else: else:
cmd = [auto_rx_path, '-c', cfg_abs] cmd = [auto_rx_path, '-c', cfg_abs]
# Set cwd to the auto_rx directory so 'from autorx.scan import ...' works # Set cwd to the auto_rx directory so 'from autorx.scan import ...' works
auto_rx_dir = os.path.dirname(os.path.abspath(auto_rx_path)) auto_rx_dir = os.path.dirname(os.path.abspath(auto_rx_path))
# Quick dependency check before launching the full process
if auto_rx_path.endswith('.py'):
dep_check = subprocess.run(
[sys.executable, '-c', 'import autorx.scan'],
cwd=auto_rx_dir,
capture_output=True,
timeout=10,
)
if dep_check.returncode != 0:
dep_error = dep_check.stderr.decode('utf-8', errors='ignore').strip()
logger.error(f"radiosonde_auto_rx dependency check failed:\n{dep_error}")
app_module.release_sdr_device(device_int, sdr_type_str)
return api_error(
'radiosonde_auto_rx dependencies not satisfied. '
f'Re-run setup.sh to install. Error: {dep_error[:500]}',
500,
)
try: try:
logger.info(f"Starting radiosonde_auto_rx: {' '.join(cmd)}") logger.info(f"Starting radiosonde_auto_rx: {' '.join(cmd)}")
app_module.radiosonde_process = subprocess.Popen( app_module.radiosonde_process = subprocess.Popen(
+2 -1
View File
@@ -34,7 +34,8 @@ def _get_timescale():
global _cached_timescale global _cached_timescale
if _cached_timescale is None: if _cached_timescale is None:
from skyfield.api import load from skyfield.api import load
_cached_timescale = load.timescale() # Use bundled timescale data so the first request does not block on network I/O.
_cached_timescale = load.timescale(builtin=True)
return _cached_timescale return _cached_timescale
# Maximum response size for external requests (1MB) # Maximum response size for external requests (1MB)
+1 -1
View File
@@ -478,7 +478,7 @@ def _get_timescale():
with _timescale_lock: with _timescale_lock:
if _timescale is None: if _timescale is None:
from skyfield.api import load from skyfield.api import load
_timescale = load.timescale() _timescale = load.timescale(builtin=True)
return _timescale return _timescale
+1
View File
@@ -19,6 +19,7 @@ const CheatSheets = (function () {
sstv_general:{ title: 'HF SSTV', icon: '📷', hardware: 'RTL-SDR + HF upconverter', description: 'Receives HF SSTV transmissions.', whatToExpect: 'Amateur radio images on 14.230 MHz (USB mode).', tips: ['14.230 MHz USB is primary HF SSTV frequency', 'Scottie 1 and Martin 1 most common', 'Best during daylight hours'] }, sstv_general:{ title: 'HF SSTV', icon: '📷', hardware: 'RTL-SDR + HF upconverter', description: 'Receives HF SSTV transmissions.', whatToExpect: 'Amateur radio images on 14.230 MHz (USB mode).', tips: ['14.230 MHz USB is primary HF SSTV frequency', 'Scottie 1 and Martin 1 most common', 'Best during daylight hours'] },
gps: { title: 'GPS Receiver', icon: '🗺️', hardware: 'USB GPS receiver (NMEA)', description: 'Streams GPS position and feeds location to other modes.', whatToExpect: 'Lat/lon, altitude, speed, heading, satellite count.', tips: ['BT Locate uses GPS for trail logging', 'Set observer location for satellite prediction', 'Verify a 3D fix before relying on altitude'] }, gps: { title: 'GPS Receiver', icon: '🗺️', hardware: 'USB GPS receiver (NMEA)', description: 'Streams GPS position and feeds location to other modes.', whatToExpect: 'Lat/lon, altitude, speed, heading, satellite count.', tips: ['BT Locate uses GPS for trail logging', 'Set observer location for satellite prediction', 'Verify a 3D fix before relying on altitude'] },
spaceweather:{ title: 'Space Weather', icon: '☀️', hardware: 'None (NOAA/SpaceWeatherLive data)', description: 'Monitors solar activity and geomagnetic storm indices.', whatToExpect: 'Kp index, solar flux, X-ray flare alerts, CME tracking.', tips: ['High Kp (≥5) = geomagnetic storm', 'X-class flares cause HF radio blackouts', 'Check before HF or satellite operations'] }, spaceweather:{ title: 'Space Weather', icon: '☀️', hardware: 'None (NOAA/SpaceWeatherLive data)', description: 'Monitors solar activity and geomagnetic storm indices.', whatToExpect: 'Kp index, solar flux, X-ray flare alerts, CME tracking.', tips: ['High Kp (≥5) = geomagnetic storm', 'X-class flares cause HF radio blackouts', 'Check before HF or satellite operations'] },
controller_monitor: { title: 'Controller Monitor', icon: '🖧', hardware: 'Optional remote agents', description: 'Aggregated controller view across connected agents and local sources.', whatToExpect: 'Combined device activity, logs, and agent health in one place.', tips: ['Use it to compare what each agent is seeing', 'Check agent status before remote starts', 'Open Manage to add or troubleshoot agents'] },
tscm: { title: 'TSCM Counter-Surveillance', icon: '🔍', hardware: 'WiFi + Bluetooth adapters', description: 'Technical Surveillance Countermeasures — detects hidden devices.', whatToExpect: 'RF baseline comparison, rogue device alerts, tracker detection.', tips: ['Take baseline in a known-clean environment', 'New strong signals = potential bug', 'Correlate WiFi + Bluetooth observations'] }, tscm: { title: 'TSCM Counter-Surveillance', icon: '🔍', hardware: 'WiFi + Bluetooth adapters', description: 'Technical Surveillance Countermeasures — detects hidden devices.', whatToExpect: 'RF baseline comparison, rogue device alerts, tracker detection.', tips: ['Take baseline in a known-clean environment', 'New strong signals = potential bug', 'Correlate WiFi + Bluetooth observations'] },
spystations: { title: 'Spy Stations', icon: '🕵️', hardware: 'RTL-SDR + HF antenna', description: 'Database of known number stations, military, and diplomatic HF signals.', whatToExpect: 'Scheduled broadcasts, frequency database, tune-to links.', tips: ['Numbers stations often broadcast on the hour', 'Use Spectrum Waterfall to tune directly', 'STANAG and HF mil signals are common'] }, spystations: { title: 'Spy Stations', icon: '🕵️', hardware: 'RTL-SDR + HF antenna', description: 'Database of known number stations, military, and diplomatic HF signals.', whatToExpect: 'Scheduled broadcasts, frequency database, tune-to links.', tips: ['Numbers stations often broadcast on the hour', 'Use Spectrum Waterfall to tune directly', 'STANAG and HF mil signals are common'] },
websdr: { title: 'WebSDR', icon: '🌐', hardware: 'None (uses remote SDR servers)', description: 'Access remote WebSDR receivers worldwide for HF shortwave listening.', whatToExpect: 'Live audio from global HF receivers, waterfall display.', tips: ['websdr.org lists available servers', 'Good for HF when local antenna is lacking', 'Use in-app player for seamless experience'] }, websdr: { title: 'WebSDR', icon: '🌐', hardware: 'None (uses remote SDR servers)', description: 'Access remote WebSDR receivers worldwide for HF shortwave listening.', whatToExpect: 'Live audio from global HF receivers, waterfall display.', tips: ['websdr.org lists available servers', 'Good for HF when local antenna is lacking', 'Use in-app player for seamless experience'] },
+49 -10
View File
@@ -18,6 +18,11 @@ const SpaceWeather = (function () {
// Current image selections // Current image selections
let _solarImageKey = 'sdo_193'; let _solarImageKey = 'sdo_193';
let _drapFreq = 'drap_global'; let _drapFreq = 'drap_global';
const SOLAR_IMAGE_FALLBACKS = {
sdo_193: 'https://sdo.gsfc.nasa.gov/assets/img/latest/latest_512_0193.jpg',
sdo_304: 'https://sdo.gsfc.nasa.gov/assets/img/latest/latest_512_0304.jpg',
sdo_magnetogram: 'https://sdo.gsfc.nasa.gov/assets/img/latest/latest_512_HMIBC.jpg',
};
/** Stable cache-bust key that rotates every 5 minutes (matches backend max-age). */ /** Stable cache-bust key that rotates every 5 minutes (matches backend max-age). */
function _cacheBust() { function _cacheBust() {
@@ -54,11 +59,12 @@ const SpaceWeather = (function () {
const frame = document.getElementById('swSolarImageFrame'); const frame = document.getElementById('swSolarImageFrame');
if (frame) { if (frame) {
frame.innerHTML = '<div class="sw-loading">Loading</div>'; frame.innerHTML = '<div class="sw-loading">Loading</div>';
const img = new Image(); _loadImageWithFallback(
img.onload = function () { frame.innerHTML = ''; frame.appendChild(img); }; frame,
img.onerror = function () { frame.innerHTML = '<div class="sw-empty">Failed to load image</div>'; }; ['/space-weather/image/' + key + '?' + _cacheBust(), _directImageUrlForKey(key)],
img.src = '/space-weather/image/' + key + '?' + _cacheBust(); key,
img.alt = key; '<div class="sw-empty">NASA SDO image is temporarily unavailable</div>'
);
} }
} }
@@ -68,11 +74,12 @@ const SpaceWeather = (function () {
const frame = document.getElementById('swDrapImageFrame'); const frame = document.getElementById('swDrapImageFrame');
if (frame) { if (frame) {
frame.innerHTML = '<div class="sw-loading">Loading</div>'; frame.innerHTML = '<div class="sw-loading">Loading</div>';
const img = new Image(); _loadImageWithFallback(
img.onload = function () { frame.innerHTML = ''; frame.appendChild(img); }; frame,
img.onerror = function () { frame.innerHTML = '<div class="sw-empty">Failed to load image</div>'; }; ['/space-weather/image/' + key + '?' + _cacheBust()],
img.src = '/space-weather/image/' + key + '?' + _cacheBust(); key,
img.alt = key; '<div class="sw-empty">Failed to load image</div>'
);
} }
} }
@@ -98,6 +105,38 @@ const SpaceWeather = (function () {
if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; } if (_pollTimer) { clearInterval(_pollTimer); _pollTimer = null; }
} }
function _directImageUrlForKey(key) {
const base = SOLAR_IMAGE_FALLBACKS[key];
if (!base) return null;
return base + '?' + _cacheBust();
}
function _loadImageWithFallback(frame, urls, alt, failureHtml) {
const candidates = (urls || []).filter(Boolean);
if (!frame || candidates.length === 0) {
if (frame) frame.innerHTML = failureHtml;
return;
}
let index = 0;
const img = new Image();
img.alt = alt;
img.referrerPolicy = 'no-referrer';
img.onload = function () {
frame.innerHTML = '';
frame.appendChild(img);
};
img.onerror = function () {
index += 1;
if (index < candidates.length) {
img.src = candidates[index];
return;
}
frame.innerHTML = failureHtml;
};
img.src = candidates[index];
}
function _fetchData() { function _fetchData() {
fetch('/space-weather/data') fetch('/space-weather/data')
.then(function (r) { return r.json(); }) .then(function (r) { return r.json(); })
+28 -2
View File
@@ -25,7 +25,7 @@
<script defer src="{{ url_for('static', filename='vendor/leaflet/leaflet.js') }}"></script> <script defer src="{{ url_for('static', filename='vendor/leaflet/leaflet.js') }}"></script>
<script defer src="{{ url_for('static', filename='js/core/observer-location.js') }}"></script> <script defer src="{{ url_for('static', filename='js/core/observer-location.js') }}"></script>
</head> </head>
<body> <body data-mode="adsb">
<div class="radar-bg"></div> <div class="radar-bg"></div>
<div class="scanline"></div> <div class="scanline"></div>
@@ -422,6 +422,7 @@
let eventSource = null; let eventSource = null;
let agentPollTimer = null; // Polling fallback for agent mode let agentPollTimer = null; // Polling fallback for agent mode
let isTracking = false; let isTracking = false;
let isTrackingStarting = false;
let currentFilter = 'all'; let currentFilter = 'all';
// ICAO -> { emergency: bool, watchlist: bool, military: bool } // ICAO -> { emergency: bool, watchlist: bool, military: bool }
let alertedAircraft = {}; let alertedAircraft = {};
@@ -2367,6 +2368,10 @@ sudo make install</code>
const btn = document.getElementById('startBtn'); const btn = document.getElementById('startBtn');
const useAgent = typeof adsbCurrentAgent !== 'undefined' && adsbCurrentAgent !== 'local'; const useAgent = typeof adsbCurrentAgent !== 'undefined' && adsbCurrentAgent !== 'local';
if (isTrackingStarting) {
return;
}
if (!isTracking) { if (!isTracking) {
// Check for remote dump1090 config (only for local mode) // Check for remote dump1090 config (only for local mode)
const remoteConfig = !useAgent ? getRemoteDump1090Config() : null; const remoteConfig = !useAgent ? getRemoteDump1090Config() : null;
@@ -2444,6 +2449,10 @@ sudo make install</code>
requestBody.remote_sbs_host = remoteConfig.host; requestBody.remote_sbs_host = remoteConfig.host;
requestBody.remote_sbs_port = remoteConfig.port; requestBody.remote_sbs_port = remoteConfig.port;
} }
isTrackingStarting = true;
btn.disabled = true;
btn.textContent = 'STARTING...';
updateTrackingStatusDisplay();
try { try {
// Route through agent proxy if using remote agent // Route through agent proxy if using remote agent
const url = useAgent const url = useAgent
@@ -2470,10 +2479,12 @@ sudo make install</code>
drawRangeRings(); drawRangeRings();
startSessionTimer(); startSessionTimer();
isTracking = true; isTracking = true;
isTrackingStarting = false;
adsbActiveDevice = adsbDevice; // Track which device is being used adsbActiveDevice = adsbDevice; // Track which device is being used
adsbTrackingSource = useAgent ? adsbCurrentAgent : 'local'; // Track which source started tracking adsbTrackingSource = useAgent ? adsbCurrentAgent : 'local'; // Track which source started tracking
btn.textContent = 'STOP'; btn.textContent = 'STOP';
btn.classList.add('active'); btn.classList.add('active');
btn.disabled = false;
document.getElementById('trackingDot').classList.remove('inactive'); document.getElementById('trackingDot').classList.remove('inactive');
updateTrackingStatusDisplay(); updateTrackingStatusDisplay();
// Disable ADS-B device selector while tracking // Disable ADS-B device selector while tracking
@@ -2493,6 +2504,14 @@ sudo make install</code>
} }
} catch (err) { } catch (err) {
alert('Error: ' + err.message); alert('Error: ' + err.message);
} finally {
if (!isTracking) {
isTrackingStarting = false;
btn.disabled = false;
btn.textContent = 'START';
btn.classList.remove('active');
updateTrackingStatusDisplay();
}
} }
} else { } else {
try { try {
@@ -5697,10 +5716,14 @@ sudo make install</code>
{% include 'partials/help-modal.html' %} {% include 'partials/help-modal.html' %}
<script src="{{ url_for('static', filename='js/core/voice-alerts.js') }}?v={{ version }}&r=adsbvoice1"></script> <script src="{{ url_for('static', filename='js/core/voice-alerts.js') }}?v={{ version }}&r=adsbvoice1"></script>
<script src="{{ url_for('static', filename='js/core/keyboard-shortcuts.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/cheat-sheets.js') }}"></script>
{% include 'partials/nav-utility-modals.html' %}
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script> <script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script>
<script> <script>
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('DOMContentLoaded', () => {
if (typeof VoiceAlerts !== 'undefined') VoiceAlerts.init(); if (typeof VoiceAlerts !== 'undefined') VoiceAlerts.init();
if (typeof KeyboardShortcuts !== 'undefined') KeyboardShortcuts.init();
}); });
</script> </script>
@@ -5738,7 +5761,10 @@ sudo make install</code>
const statusEl = document.getElementById('trackingStatus'); const statusEl = document.getElementById('trackingStatus');
if (!statusEl) return; if (!statusEl) return;
if (!isTracking) { if (isTrackingStarting && !isTracking) {
statusEl.textContent = 'INITIALIZING';
statusEl.title = 'Starting ADS-B receiver';
} else if (!isTracking) {
statusEl.textContent = 'STANDBY'; statusEl.textContent = 'STANDBY';
statusEl.title = 'Select source and click START'; statusEl.title = 'Select source and click START';
} else { } else {
+14 -1
View File
@@ -24,7 +24,7 @@
<script defer src="{{ url_for('static', filename='vendor/leaflet/leaflet.js') }}"></script> <script defer src="{{ url_for('static', filename='vendor/leaflet/leaflet.js') }}"></script>
<script defer src="{{ url_for('static', filename='js/core/observer-location.js') }}"></script> <script defer src="{{ url_for('static', filename='js/core/observer-location.js') }}"></script>
</head> </head>
<body> <body data-mode="ais">
<!-- Radar background effects --> <!-- Radar background effects -->
<div class="radar-bg"></div> <div class="radar-bg"></div>
<div class="scanline"></div> <div class="scanline"></div>
@@ -1632,7 +1632,20 @@
<!-- Help Modal --> <!-- Help Modal -->
{% include 'partials/help-modal.html' %} {% include 'partials/help-modal.html' %}
<script src="{{ url_for('static', filename='js/core/voice-alerts.js') }}?v={{ version }}&r=voicefix2"></script>
<script src="{{ url_for('static', filename='js/core/keyboard-shortcuts.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/cheat-sheets.js') }}"></script>
{% include 'partials/nav-utility-modals.html' %}
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script> <script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script>
<script>
window.addEventListener('DOMContentLoaded', () => {
if (typeof VoiceAlerts !== 'undefined') {
VoiceAlerts.init({ startStreams: false });
VoiceAlerts.scheduleStreamStart(20000);
}
if (typeof KeyboardShortcuts !== 'undefined') KeyboardShortcuts.init();
});
</script>
<!-- Agent Manager --> <!-- Agent Manager -->
<script src="{{ url_for('static', filename='js/core/agents.js') }}"></script> <script src="{{ url_for('static', filename='js/core/agents.js') }}"></script>
+14 -1
View File
@@ -518,7 +518,7 @@
} }
</style> </style>
</head> </head>
<body> <body data-mode="controller_monitor">
<header class="header"> <header class="header">
<div class="logo"> <div class="logo">
NETWORK MONITOR NETWORK MONITOR
@@ -1117,7 +1117,20 @@
<!-- Help Modal --> <!-- Help Modal -->
{% include 'partials/help-modal.html' %} {% include 'partials/help-modal.html' %}
<script src="{{ url_for('static', filename='js/core/voice-alerts.js') }}?v={{ version }}&r=voicefix2"></script>
<script src="{{ url_for('static', filename='js/core/keyboard-shortcuts.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/cheat-sheets.js') }}"></script>
{% include 'partials/nav-utility-modals.html' %}
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script> <script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script>
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script> <script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
<script>
window.addEventListener('DOMContentLoaded', () => {
if (typeof VoiceAlerts !== 'undefined') {
VoiceAlerts.init({ startStreams: false });
VoiceAlerts.scheduleStreamStart(20000);
}
if (typeof KeyboardShortcuts !== 'undefined') KeyboardShortcuts.init();
});
</script>
</body> </body>
</html> </html>
@@ -0,0 +1,26 @@
<!-- Cheat Sheet Modal -->
<div id="cheatSheetModal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.7); z-index:10000; align-items:center; justify-content:center; padding:20px;" onclick="if(event.target===this)CheatSheets.hide()">
<div style="background:var(--bg-card, #1a1f2e); border:1px solid rgba(255,255,255,0.15); border-radius:12px; max-width:480px; width:100%; max-height:80vh; overflow-y:auto; padding:20px; position:relative;">
<button onclick="CheatSheets.hide()" style="position:absolute; top:12px; right:12px; background:none; border:none; color:var(--text-dim); cursor:pointer; font-size:18px; line-height:1;"></button>
<div id="cheatSheetContent"></div>
</div>
</div>
<!-- Keyboard Shortcuts Modal -->
<div id="kbShortcutsModal" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.7); z-index:10000; align-items:center; justify-content:center; padding:20px;" onclick="if(event.target===this)KeyboardShortcuts.hideHelp()">
<div style="background:var(--bg-card, #1a1f2e); border:1px solid rgba(255,255,255,0.15); border-radius:12px; max-width:520px; width:100%; max-height:80vh; overflow-y:auto; padding:20px; position:relative;">
<button onclick="KeyboardShortcuts.hideHelp()" style="position:absolute; top:12px; right:12px; background:none; border:none; color:var(--text-dim); cursor:pointer; font-size:18px; line-height:1;"></button>
<h2 style="margin:0 0 16px; font-size:16px; color:var(--accent-cyan, #4aa3ff); font-family:var(--font-mono);">Keyboard Shortcuts</h2>
<table style="width:100%; border-collapse:collapse; font-family:var(--font-mono); font-size:12px;">
<tbody>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);"><td style="padding:6px 8px; color:var(--accent-cyan);">Alt+W</td><td style="padding:6px 8px; color:var(--text-secondary);">Switch to Waterfall</td></tr>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);"><td style="padding:6px 8px; color:var(--accent-cyan);">Alt+M</td><td style="padding:6px 8px; color:var(--text-secondary);">Toggle voice mute</td></tr>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);"><td style="padding:6px 8px; color:var(--accent-cyan);">Alt+S</td><td style="padding:6px 8px; color:var(--text-secondary);">Toggle sidebar</td></tr>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);"><td style="padding:6px 8px; color:var(--accent-cyan);">Alt+K / ?</td><td style="padding:6px 8px; color:var(--text-secondary);">Show keyboard shortcuts</td></tr>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);"><td style="padding:6px 8px; color:var(--accent-cyan);">Alt+C</td><td style="padding:6px 8px; color:var(--text-secondary);">Show cheat sheet for current mode</td></tr>
<tr style="border-bottom:1px solid rgba(255,255,255,0.06);"><td style="padding:6px 8px; color:var(--accent-cyan);">Alt+1..9</td><td style="padding:6px 8px; color:var(--text-secondary);">Switch to Nth mode in current group</td></tr>
<tr><td style="padding:6px 8px; color:var(--accent-cyan);">Escape</td><td style="padding:6px 8px; color:var(--text-secondary);">Close modal</td></tr>
</tbody>
</table>
</div>
</div>
+14 -1
View File
@@ -21,7 +21,7 @@
</script> </script>
<script src="{{ url_for('static', filename='js/core/observer-location.js') }}"></script> <script src="{{ url_for('static', filename='js/core/observer-location.js') }}"></script>
</head> </head>
<body> <body data-mode="satellite">
<div class="grid-bg"></div> <div class="grid-bg"></div>
<div class="scanline"></div> <div class="scanline"></div>
@@ -1830,9 +1830,22 @@
<!-- Help Modal --> <!-- Help Modal -->
{% include 'partials/help-modal.html' %} {% include 'partials/help-modal.html' %}
<script src="{{ url_for('static', filename='js/core/voice-alerts.js') }}?v={{ version }}&r=voicefix2"></script>
<script src="{{ url_for('static', filename='js/core/keyboard-shortcuts.js') }}"></script>
<script src="{{ url_for('static', filename='js/core/cheat-sheets.js') }}"></script>
{% include 'partials/nav-utility-modals.html' %}
<script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script> <script src="{{ url_for('static', filename='js/core/settings-manager.js') }}?v={{ version }}&r=maptheme17"></script>
<script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script> <script src="{{ url_for('static', filename='js/core/global-nav.js') }}"></script>
<script src="{{ url_for('static', filename='js/modes/ground_station_waterfall.js') }}"></script> <script src="{{ url_for('static', filename='js/modes/ground_station_waterfall.js') }}"></script>
<script>
window.addEventListener('DOMContentLoaded', () => {
if (typeof VoiceAlerts !== 'undefined') {
VoiceAlerts.init({ startStreams: false });
VoiceAlerts.scheduleStreamStart(20000);
}
if (typeof KeyboardShortcuts !== 'undefined') KeyboardShortcuts.init();
});
</script>
<script> <script>
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
+1 -1
View File
@@ -90,7 +90,7 @@ class DopplerTracker:
return False return False
try: try:
ts = load.timescale() ts = load.timescale(builtin=True)
satellite = EarthSatellite(tle[1], tle[2], tle[0], ts) satellite = EarthSatellite(tle[1], tle[2], tle[0], ts)
observer = wgs84.latlon(latitude, longitude) observer = wgs84.latlon(latitude, longitude)
except Exception as e: except Exception as e:
+2 -2
View File
@@ -286,10 +286,10 @@ class GroundStationScheduler:
from utils.satellite_predict import predict_passes as _predict_passes from utils.satellite_predict import predict_passes as _predict_passes
try: try:
ts = load.timescale() ts = load.timescale(builtin=True)
except Exception: except Exception:
from skyfield.api import load as _load from skyfield.api import load as _load
ts = _load.timescale() ts = _load.timescale(builtin=True)
observer = wgs84.latlon(self._lat, self._lon) observer = wgs84.latlon(self._lat, self._lon)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
+1 -1
View File
@@ -70,7 +70,7 @@ def predict_passes(
# patch sys.modules to simulate skyfield being unavailable). # patch sys.modules to simulate skyfield being unavailable).
import skyfield # noqa: F401 import skyfield # noqa: F401
ts = load.timescale() ts = load.timescale(builtin=True)
observer = wgs84.latlon(lat, lon) observer = wgs84.latlon(lat, lon)
t0 = ts.now() t0 = ts.now()
t1 = ts.utc(t0.utc_datetime() + datetime.timedelta(hours=hours)) t1 = ts.utc(t0.utc_datetime() + datetime.timedelta(hours=hours))