diff --git a/CHANGELOG.md b/CHANGELOG.md index dfff270..253ee03 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to iNTERCEPT will be documented in this file. +## [2.21.1] - 2026-02-20 + +### Fixed +- BT Locate map first-load rendering race that could cause blank/late map initialization +- BT Locate mode switch timing so Leaflet invalidation runs after panel visibility settles +- BT Locate trail restore startup latency by batching historical GPS point rendering + +--- + ## [2.21.0] - 2026-02-20 ### Added diff --git a/config.py b/config.py index a5561d1..f8d6287 100644 --- a/config.py +++ b/config.py @@ -7,10 +7,19 @@ import os import sys # Application version -VERSION = "2.21.0" - -# Changelog - latest release notes (shown on welcome screen) +VERSION = "2.21.1" + +# Changelog - latest release notes (shown on welcome screen) CHANGELOG = [ + { + "version": "2.21.1", + "date": "February 2026", + "highlights": [ + "BT Locate map first-load fix with render stabilization retries during initial mode open", + "BT Locate trail restore optimization for faster startup when historical GPS points exist", + "BT Locate mode-switch map invalidation timing fix to prevent delayed/blank map render", + ] + }, { "version": "2.21.0", "date": "February 2026", diff --git a/pyproject.toml b/pyproject.toml index 061a597..e1229b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "intercept" -version = "2.21.0" +version = "2.21.1" description = "Signal Intelligence Platform - Pager/433MHz/ADS-B/Satellite/WiFi/Bluetooth" readme = "README.md" requires-python = ">=3.9" diff --git a/static/js/modes/bt_locate.js b/static/js/modes/bt_locate.js index 7689fa5..eb8a802 100644 --- a/static/js/modes/bt_locate.js +++ b/static/js/modes/bt_locate.js @@ -37,6 +37,7 @@ const BtLocate = (function() { let smoothingEnabled = true; let lastRenderedDetectionKey = null; let pendingHeatSync = false; + let mapStabilizeTimer = null; const MAX_HEAT_POINTS = 1200; const MAX_TRAIL_POINTS = 1200; @@ -44,6 +45,8 @@ const BtLocate = (function() { const OUTLIER_HARD_JUMP_METERS = 2000; const OUTLIER_SOFT_JUMP_METERS = 450; const OUTLIER_MAX_SPEED_MPS = 50; + const MAP_STABILIZE_INTERVAL_MS = 150; + const MAP_STABILIZE_ATTEMPTS = 28; const OVERLAY_STORAGE_KEYS = { heatmap: 'btLocateHeatmapEnabled', movement: 'btLocateMovementEnabled', @@ -99,6 +102,7 @@ const BtLocate = (function() { Settings.createTileLayer().addTo(map); } flushPendingHeatSync(); + scheduleMapStabilization(10); }, 150); } checkStatus(); @@ -111,17 +115,25 @@ const BtLocate = (function() { map = L.map('btLocateMap', { center: [0, 0], zoom: 2, - zoomControl: true, - }); - // Use tile provider from user settings - if (typeof Settings !== 'undefined' && Settings.createTileLayer) { - Settings.createTileLayer().addTo(map); - Settings.registerMap(map); - } else { - L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { - maxZoom: 19, - attribution: '© OSM © CARTO' - }).addTo(map); + zoomControl: true, + }); + let tileLayer = null; + // Use tile provider from user settings + if (typeof Settings !== 'undefined' && Settings.createTileLayer) { + tileLayer = Settings.createTileLayer(); + tileLayer.addTo(map); + Settings.registerMap(map); + } else { + tileLayer = L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { + maxZoom: 19, + attribution: '© OSM © CARTO' + }); + tileLayer.addTo(map); + } + if (tileLayer && typeof tileLayer.on === 'function') { + tileLayer.on('load', () => { + scheduleMapStabilization(8); + }); } ensureHeatLayer(); syncMovementLayer(); @@ -133,6 +145,7 @@ const BtLocate = (function() { safeInvalidateMap(); flushPendingHeatSync(); }, 100); + scheduleMapStabilization(); } // Init RSSI chart canvas @@ -524,6 +537,8 @@ const BtLocate = (function() { const lon = Number(point.lon); if (!isFinite(lat) || !isFinite(lon)) return false; if (!shouldAcceptMapPoint(point, lat, lon)) return false; + const suppressFollow = options.suppressFollow === true; + const bulkLoad = options.bulkLoad === true; const trailPoint = normalizeTrailPoint(point, lat, lon); const band = (trailPoint.proximity_band || 'FAR').toLowerCase(); @@ -563,13 +578,17 @@ const BtLocate = (function() { if (heatPoints.length > MAX_HEAT_POINTS) { heatPoints.splice(0, heatPoints.length - MAX_HEAT_POINTS); } + if (bulkLoad) { + pendingHeatSync = true; + return true; + } syncHeatLayer(); if (!isMapRenderable()) { safeInvalidateMap(); } const canFollowMap = isMapRenderable(); - if (autoFollowEnabled && !options.suppressFollow && canFollowMap) { + if (autoFollowEnabled && !suppressFollow && canFollowMap) { if (!gpsLocked) { gpsLocked = true; map.setView([lat, lon], Math.max(map.getZoom(), 16)); @@ -645,8 +664,13 @@ const BtLocate = (function() { const gpsTrail = Array.isArray(trail.gps_trail) ? trail.gps_trail : []; const allTrail = Array.isArray(trail.trail) ? trail.trail : []; + const recentGpsTrail = gpsTrail.slice(-MAX_TRAIL_POINTS); - gpsTrail.forEach(p => addMapMarker(p, { suppressFollow: true })); + recentGpsTrail.forEach(p => addMapMarker(p, { + suppressFollow: true, + bulkLoad: true, + })); + syncHeatLayer(); if (allTrail.length > 0) { rssiHistory = allTrail.map(p => p.rssi).filter(v => typeof v === 'number' && isFinite(v)).slice(-MAX_RSSI_POINTS); @@ -659,7 +683,7 @@ const BtLocate = (function() { drawRssiChart(); } - updateStats(allTrail.length, gpsTrail.length); + updateStats(allTrail.length, recentGpsTrail.length); if (trailPoints.length > 0 && map) { const latestGps = trailPoints[trailPoints.length - 1]; @@ -675,6 +699,7 @@ const BtLocate = (function() { syncStrongestMarker(); updateConfidenceLayer(); updateMovementStats(); + scheduleMapStabilization(12); }) .catch(() => {}); } @@ -908,6 +933,45 @@ const BtLocate = (function() { return true; } + function stopMapStabilization() { + if (mapStabilizeTimer) { + clearInterval(mapStabilizeTimer); + mapStabilizeTimer = null; + } + } + + function scheduleMapStabilization(attempts = MAP_STABILIZE_ATTEMPTS) { + if (!map) return; + stopMapStabilization(); + let remaining = Math.max(1, Number(attempts) || MAP_STABILIZE_ATTEMPTS); + + const tick = () => { + if (!map) { + stopMapStabilization(); + return; + } + if (safeInvalidateMap()) { + flushPendingHeatSync(); + syncMovementLayer(); + syncStrongestMarker(); + updateConfidenceLayer(); + if (isMapRenderable()) { + stopMapStabilization(); + return; + } + } + remaining -= 1; + if (remaining <= 0) { + stopMapStabilization(); + } + }; + + tick(); + if (map && !mapStabilizeTimer && !isMapRenderable()) { + mapStabilizeTimer = setInterval(tick, MAP_STABILIZE_INTERVAL_MS); + } + } + function flushPendingHeatSync() { if (!pendingHeatSync) return; syncHeatLayer(); @@ -1566,6 +1630,7 @@ const BtLocate = (function() { syncStrongestMarker(); updateConfidenceLayer(); } + scheduleMapStabilization(8); } return { diff --git a/templates/index.html b/templates/index.html index 8d5eb1d..477c053 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4242,6 +4242,12 @@ SubGhz.init(); } else if (mode === 'bt_locate') { BtLocate.init(); + setTimeout(() => { + if (typeof BtLocate !== 'undefined' && BtLocate.invalidateMap) BtLocate.invalidateMap(); + }, 100); + setTimeout(() => { + if (typeof BtLocate !== 'undefined' && BtLocate.invalidateMap) BtLocate.invalidateMap(); + }, 320); } else if (mode === 'spaceweather') { SpaceWeather.init(); }