Merge main into misc-fixes and address PR #202 review

Sync with upstream main and fix required items from review:

- updateTimelineLabels() now uses InterceptTime API (getTimezone/getIANA)
  instead of the stale selectedTimezone/TZ_MAP globals that were removed
  during the earlier InterceptTime refactor — fixes ReferenceError on TZ
  change and pass refresh.

- Remove profiles: [basic] from the intercept service in
  docker-compose.yml so bare `docker compose up -d` still starts the
  main service. Profile-gated services (intercept-history, adsb_db)
  stay as-is.
This commit is contained in:
Mitch Ross
2026-04-24 16:34:09 -04:00
60 changed files with 6969 additions and 5301 deletions
+29 -1
View File
@@ -18,6 +18,23 @@
</div>
</div>
<div class="section">
<h3>NMEA UDP Forward</h3>
<p class="info-text" style="font-size: 11px; color: var(--text-dim); margin-bottom: 8px;">
Forward NMEA 0183 sentences to an external app (e.g. OpenCPN). Leave host blank to disable.
</p>
<div style="display: flex; gap: 8px;">
<div style="flex: 2;">
<label style="font-size: 10px; color: var(--text-dim);">Host</label>
<input type="text" id="aisUdpHost" placeholder="e.g. 192.168.1.10" style="width: 100%;">
</div>
<div style="flex: 1;">
<label style="font-size: 10px; color: var(--text-dim);">Port</label>
<input type="number" id="aisUdpPort" value="10110" min="1" max="65535" style="width: 100%;">
</div>
</div>
</div>
<div class="section">
<h3>Status</h3>
<div id="aisStatusDisplay" class="info-text">
@@ -110,11 +127,22 @@
function startAisTracking() {
const gain = document.getElementById('aisGainInput').value || '40';
const device = document.getElementById('deviceSelect')?.value || '0';
const udpHost = document.getElementById('aisUdpHost').value.trim();
const udpPort = parseInt(document.getElementById('aisUdpPort').value) || 10110;
const body = {
device, gain,
bias_t: typeof getBiasTEnabled === 'function' ? getBiasTEnabled() : false,
};
if (udpHost) {
body.udp_host = udpHost;
body.udp_port = udpPort;
}
fetch('/ais/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ device, gain, bias_t: typeof getBiasTEnabled === 'function' ? getBiasTEnabled() : false })
body: JSON.stringify(body)
})
.then(r => r.json())
.then(data => {
+1 -1
View File
@@ -32,7 +32,7 @@
<h3>Settings</h3>
<div class="form-group">
<label for="gain">Gain (dB, 0 = auto)</label>
<input type="text" id="gain" value="0" placeholder="0-49 or 0 for auto">
<input type="number" id="gain" value="0" min="0" max="50" step="0.5" placeholder="0 = auto">
</div>
<div class="form-group">
<label for="squelch">Squelch Level</label>
+16 -8
View File
@@ -278,6 +278,7 @@
// Map management
let radiosondeMap = null;
let radiosondeMapOverlays = null;
let radiosondeMarkers = new Map();
let radiosondeTracks = new Map();
let radiosondeTrackPoints = new Map();
@@ -295,16 +296,23 @@
}
const hasLocation = radiosondeStationLocation.lat !== 0 || radiosondeStationLocation.lon !== 0;
radiosondeMap = L.map('radiosondeMapContainer', {
center: hasLocation ? [radiosondeStationLocation.lat, radiosondeStationLocation.lon] : [40, -95],
zoom: hasLocation ? 7 : 4,
zoomControl: true,
});
const observerLocation = hasLocation
? { lat: radiosondeStationLocation.lat, lon: radiosondeStationLocation.lon }
: { lat: 40, lon: -95 };
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; OpenStreetMap &copy; CARTO',
radiosondeMap = MapUtils.init('radiosondeMapContainer', {
center: [observerLocation.lat, observerLocation.lon],
zoom: hasLocation ? 7 : 4,
minZoom: 2,
maxZoom: 18,
}).addTo(radiosondeMap);
});
if (radiosondeMap) {
window.radiosondeMap = radiosondeMap;
radiosondeMapOverlays = MapUtils.addTacticalOverlays(radiosondeMap, {
hudPanels: { modeName: 'RADIOSONDE', getContactCount: () => 0 },
scaleBar: true,
});
}
// Add station marker if we have a location
if (hasLocation) {
+1 -1
View File
@@ -18,7 +18,7 @@
<h3>Settings</h3>
<div class="form-group">
<label for="sensorGain">Gain (dB, 0 = auto)</label>
<input type="text" id="sensorGain" value="0" placeholder="0-49 or 0 for auto">
<input type="number" id="sensorGain" value="0" min="0" max="50" step="0.5" placeholder="0 = auto">
</div>
<div class="form-group">
<label for="sensorPpm">PPM Correction</label>
+15 -1
View File
@@ -6,14 +6,28 @@
<div class="form-group">
<label>Sweep Type</label>
<select id="tscmSweepType">
<select id="tscmSweepType" onchange="document.getElementById('tscmCustomRangeControls').style.display = this.value === 'custom' ? 'block' : 'none'">
<option value="quick">Quick Scan (2 min)</option>
<option value="standard" selected>Standard (5 min)</option>
<option value="full">Full Sweep (15 min)</option>
<option value="wireless_cameras">Wireless Cameras</option>
<option value="body_worn">Body-Worn Devices</option>
<option value="gps_trackers">GPS Trackers</option>
<option value="custom">Custom Range</option>
</select>
<div id="tscmCustomRangeControls" style="display: none; margin-top: 8px;">
<div style="display: flex; gap: 8px;">
<div style="flex: 1;">
<label style="font-size: 10px; color: var(--text-dim);">Start (MHz)</label>
<input type="number" id="tscmCustomStartMhz" value="400" min="1" max="6000" step="1">
</div>
<div style="flex: 1;">
<label style="font-size: 10px; color: var(--text-dim);">End (MHz)</label>
<input type="number" id="tscmCustomEndMhz" value="500" min="1" max="6000" step="1">
</div>
</div>
<p class="info-text" style="font-size: 10px; color: var(--text-dim); margin-top: 3px;">Step: 100 kHz. Duration: ~5 min.</p>
</div>
</div>
<div class="form-group">
@@ -92,7 +92,8 @@
</div>
<div class="form-group">
<label>Gain (dB)</label>
<input type="number" id="weatherSatGain" value="40" step="0.1" min="0" max="50">
<input type="number" id="weatherSatGain" value="30" step="0.1" min="0" max="50">
<p class="info-text" style="font-size: 10px; color: var(--text-dim); margin-top: 3px;">Reduce if decoding fails on strong passes (ADC saturation).</p>
</div>
<div class="form-group">
<label style="display: flex; align-items: center; gap: 6px;">
+11
View File
@@ -77,6 +77,8 @@
<option value="openstreetmap">OpenStreetMap</option>
<option value="cartodb_light">CartoDB Positron</option>
<option value="esri_world">ESRI World Imagery</option>
<option value="stadia_dark">Stadia Alidade Dark</option>
<option value="tactical">Stadia Tactical (minimal)</option>
<option value="custom">Custom URL</option>
</select>
</div>
@@ -90,6 +92,15 @@
onchange="Settings.setCustomTileUrl(this.value)">
</div>
</div>
<div class="settings-row" id="stadiaKeyRow" style="display: none;">
<div class="settings-label" style="width: 100%;">
<span class="settings-label-text">Stadia API Key</span>
<span class="settings-label-desc">Free at <a href="https://client.stadiamaps.com/signup/" target="_blank" style="color: var(--accent-cyan);">stadiamaps.com</a></span>
<input type="text" id="stadiaKeyInput" class="settings-input"
placeholder="your-api-key-here"
onchange="Settings.setStadiaKey(this.value)">
</div>
</div>
</div>
<div class="settings-group">