Release v2.22.0

Waterfall overhaul, new modes (fingerprint, RF heatmap, SignalID, voice
alerts), PWA support, mode stop responsiveness improvements, ADS-B MSG2
surface tracking, WebSDR overhaul, and full documentation audit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-02-23 19:31:10 +00:00
parent 3acdab816a
commit 9705e58691
15 changed files with 647 additions and 448 deletions
+66
View File
@@ -560,3 +560,69 @@
font-size: 9px;
}
}
/* ── Crosshair sweep animation ───────────────────────────────────── */
.btl-crosshair-overlay {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
z-index: 1200;
--btl-crosshair-x-start: 100%;
--btl-crosshair-y-start: 100%;
--btl-crosshair-x-end: 50%;
--btl-crosshair-y-end: 50%;
--btl-crosshair-duration: 1500ms;
}
.btl-crosshair-line {
position: absolute;
opacity: 0;
background: var(--accent-cyan);
will-change: transform, opacity;
}
.btl-crosshair-vertical {
top: 0;
bottom: 0;
width: 1px;
left: 0;
transform: translateX(var(--btl-crosshair-x-start));
}
.btl-crosshair-horizontal {
left: 0;
right: 0;
height: 1px;
top: 0;
transform: translateY(var(--btl-crosshair-y-start));
}
.btl-crosshair-overlay.active .btl-crosshair-vertical {
animation: btlCrosshairSweepX var(--btl-crosshair-duration) cubic-bezier(0.2, 0.85, 0.28, 1) forwards;
}
.btl-crosshair-overlay.active .btl-crosshair-horizontal {
animation: btlCrosshairSweepY var(--btl-crosshair-duration) cubic-bezier(0.2, 0.85, 0.28, 1) forwards;
}
@keyframes btlCrosshairSweepX {
0% { transform: translateX(var(--btl-crosshair-x-start)); opacity: 0; }
12% { opacity: 1; }
85% { opacity: 1; }
100% { transform: translateX(var(--btl-crosshair-x-end)); opacity: 0; }
}
@keyframes btlCrosshairSweepY {
0% { transform: translateY(var(--btl-crosshair-y-start)); opacity: 0; }
12% { opacity: 1; }
85% { opacity: 1; }
100% { transform: translateY(var(--btl-crosshair-y-end)); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
.btl-crosshair-overlay.active .btl-crosshair-vertical,
.btl-crosshair-overlay.active .btl-crosshair-horizontal {
animation-duration: 220ms;
}
}
+9 -9
View File
@@ -8,17 +8,17 @@ const KeyboardShortcuts = (function () {
function _handle(e) {
if (e.target.matches(GUARD_SELECTOR)) return;
if (e.altKey) {
switch (e.key.toLowerCase()) {
case 'w': e.preventDefault(); window.switchMode && switchMode('waterfall'); break;
case 'm': e.preventDefault(); window.VoiceAlerts && VoiceAlerts.toggleMute(); break;
case 's': e.preventDefault(); _toggleSidebar(); break;
case 'k': e.preventDefault(); showHelp(); break;
case 'c': e.preventDefault(); window.CheatSheets && CheatSheets.showForCurrentMode(); break;
if (e.altKey) {
switch (e.code) {
case 'KeyW': e.preventDefault(); window.switchMode && switchMode('waterfall'); break;
case 'KeyM': e.preventDefault(); window.VoiceAlerts && VoiceAlerts.toggleMute(); break;
case 'KeyS': e.preventDefault(); _toggleSidebar(); break;
case 'KeyK': e.preventDefault(); showHelp(); break;
case 'KeyC': e.preventDefault(); window.CheatSheets && CheatSheets.showForCurrentMode(); break;
default:
if (e.key >= '1' && e.key <= '9') {
if (e.code >= 'Digit1' && e.code <= 'Digit9') {
e.preventDefault();
_switchToNthMode(parseInt(e.key) - 1);
_switchToNthMode(parseInt(e.code.replace('Digit', '')) - 1);
}
}
} else if (!e.ctrlKey && !e.metaKey) {
+39 -12
View File
@@ -44,6 +44,7 @@ const BtLocate = (function() {
let queuedDetectionTimer = null;
let lastDetectionRenderAt = 0;
let startRequestInFlight = false;
let crosshairResetTimer = null;
const MAX_HEAT_POINTS = 1200;
const MAX_TRAIL_POINTS = 1200;
@@ -349,19 +350,18 @@ const BtLocate = (function() {
}
function stop() {
// Update UI immediately — don't wait for the backend response.
if (queuedDetectionTimer) {
clearTimeout(queuedDetectionTimer);
queuedDetectionTimer = null;
}
queuedDetection = null;
queuedDetectionOptions = null;
showIdleUI();
disconnectSSE();
stopAudio();
// Notify backend asynchronously.
fetch('/bt_locate/stop', { method: 'POST' })
.then(r => r.json())
.then(() => {
if (queuedDetectionTimer) {
clearTimeout(queuedDetectionTimer);
queuedDetectionTimer = null;
}
queuedDetection = null;
queuedDetectionOptions = null;
showIdleUI();
disconnectSSE();
stopAudio();
})
.catch(err => console.error('[BtLocate] Stop error:', err));
}
@@ -703,6 +703,32 @@ const BtLocate = (function() {
if (gpsCountEl) gpsCountEl.textContent = gpsPoints || 0;
}
function triggerCrosshairAnimation(lat, lon) {
if (!map) return;
const overlay = document.getElementById('btLocateCrosshairOverlay');
if (!overlay) return;
const size = map.getSize();
const point = map.latLngToContainerPoint([lat, lon]);
const targetX = Math.max(0, Math.min(size.x, point.x));
const targetY = Math.max(0, Math.min(size.y, point.y));
const startX = size.x + 8;
const startY = size.y + 8;
const duration = 1500;
overlay.style.setProperty('--btl-crosshair-x-start', `${startX}px`);
overlay.style.setProperty('--btl-crosshair-y-start', `${startY}px`);
overlay.style.setProperty('--btl-crosshair-x-end', `${targetX}px`);
overlay.style.setProperty('--btl-crosshair-y-end', `${targetY}px`);
overlay.style.setProperty('--btl-crosshair-duration', `${duration}ms`);
overlay.classList.remove('active');
void overlay.offsetWidth;
overlay.classList.add('active');
if (crosshairResetTimer) clearTimeout(crosshairResetTimer);
crosshairResetTimer = setTimeout(() => {
overlay.classList.remove('active');
crosshairResetTimer = null;
}, duration + 100);
}
function addMapMarker(point, options = {}) {
if (!map || point.lat == null || point.lon == null) return false;
const lat = Number(point.lat);
@@ -737,6 +763,7 @@ const BtLocate = (function() {
'Time: ' + formatPointTimestamp(trailPoint.timestamp) +
'</div>'
);
marker.on('click', () => triggerCrosshairAnimation(lat, lon));
trailPoints.push(trailPoint);
mapMarkers.push(marker);