diff --git a/templates/adsb_dashboard.html b/templates/adsb_dashboard.html index 27062c1..965dcc7 100644 --- a/templates/adsb_dashboard.html +++ b/templates/adsb_dashboard.html @@ -553,6 +553,7 @@ 0 AIRCRAFT
--:--:-- UTC
+ ← Main Dashboard @@ -762,6 +763,7 @@ // Throttle expensive UI operations to prevent browser freeze let pendingUIUpdate = false; let pendingMarkerUpdates = new Set(); + const MAX_MARKER_UPDATES_PER_FRAME = 20; function scheduleUIUpdate() { if (pendingUIUpdate) return; @@ -770,11 +772,25 @@ updateStats(); renderAircraftList(); - // Batch marker updates + // Limit marker updates per frame to prevent jank + let updateCount = 0; + const toProcess = []; for (const icao of pendingMarkerUpdates) { - updateMarkerImmediate(icao); + if (updateCount < MAX_MARKER_UPDATES_PER_FRAME) { + updateMarkerImmediate(icao); + toProcess.push(icao); + updateCount++; + } + } + // Remove processed markers from pending set + toProcess.forEach(icao => pendingMarkerUpdates.delete(icao)); + + // If more markers pending, schedule another frame + if (pendingMarkerUpdates.size > 0) { + pendingUIUpdate = false; + scheduleUIUpdate(); + return; } - pendingMarkerUpdates.clear(); // Update selected aircraft panel if (selectedIcao && aircraft[selectedIcao]) { @@ -805,14 +821,59 @@ scheduleUIUpdate(); } + // Track marker state to avoid unnecessary updates + const markerState = {}; + function updateMarkerImmediate(icao) { const ac = aircraft[icao]; if (!ac || !ac.lat || !ac.lon) return; - const rotation = ac.heading || 0; + const rotation = Math.round((ac.heading || 0) / 5) * 5; // Round to 5 degrees const color = getAltitudeColor(ac.altitude); + const callsign = ac.callsign || icao; + const alt = ac.altitude ? ac.altitude + ' ft' : 'N/A'; - const icon = L.divIcon({ + const prevState = markerState[icao] || {}; + const iconChanged = prevState.rotation !== rotation || prevState.color !== color; + const tooltipChanged = prevState.callsign !== callsign || prevState.alt !== alt; + + if (markers[icao]) { + // Only update position (cheap operation) + markers[icao].setLatLng([ac.lat, ac.lon]); + + // Only update icon if heading/color actually changed + if (iconChanged) { + const icon = createMarkerIcon(rotation, color); + markers[icao].setIcon(icon); + } + + // Only update tooltip if content changed + if (tooltipChanged) { + markers[icao].unbindTooltip(); + markers[icao].bindTooltip(`${callsign}
${alt}`, { + permanent: false, + direction: 'top', + className: 'aircraft-tooltip' + }); + } + } else { + const icon = createMarkerIcon(rotation, color); + markers[icao] = L.marker([ac.lat, ac.lon], { icon }) + .addTo(radarMap) + .on('click', () => selectAircraft(icao)); + markers[icao].bindTooltip(`${callsign}
${alt}`, { + permanent: false, + direction: 'top', + className: 'aircraft-tooltip' + }); + } + + // Update state cache + markerState[icao] = { rotation, color, callsign, alt }; + } + + function createMarkerIcon(rotation, color) { + return L.divIcon({ className: 'aircraft-marker', html: `
No aircraft detected
Waiting for data...
`; + renderedAircraftOrder = []; return; } - container.innerHTML = sortedAircraft.map(ac => { - const callsign = ac.callsign || '------'; - const alt = ac.altitude ? ac.altitude.toLocaleString() : '---'; - const speed = ac.speed || '---'; - const heading = ac.heading ? ac.heading + '°' : '---'; + const newOrder = sortedAircraft.map(ac => ac.icao); + const orderChanged = newOrder.length !== renderedAircraftOrder.length || + newOrder.some((icao, i) => icao !== renderedAircraftOrder[i]); - return ` -
-
- ${callsign} - ${ac.icao} -
-
-
-
${alt}
-
ALT ft
-
-
-
${speed}
-
SPD kts
-
-
-
${heading}
-
HDG
-
-
+ const now = Date.now(); + const canRebuild = now - lastFullRebuild > MIN_REBUILD_INTERVAL; + + // Only do full rebuild if order changed AND enough time has passed + if (orderChanged && canRebuild) { + lastFullRebuild = now; + // Use DocumentFragment for efficient batch insert + const fragment = document.createDocumentFragment(); + + sortedAircraft.forEach(ac => { + const div = document.createElement('div'); + div.className = `aircraft-item ${selectedIcao === ac.icao ? 'selected' : ''}`; + div.setAttribute('data-icao', ac.icao); + div.onclick = () => selectAircraft(ac.icao); + div.innerHTML = buildAircraftItemHTML(ac); + fragment.appendChild(div); + }); + + container.innerHTML = ''; + container.appendChild(fragment); + renderedAircraftOrder = newOrder; + } else { + // Incremental update - only update existing items in place + // Build a map of existing items to avoid repeated querySelector calls + const existingItems = {}; + container.querySelectorAll('[data-icao]').forEach(el => { + existingItems[el.getAttribute('data-icao')] = el; + }); + + sortedAircraft.forEach(ac => { + const existingItem = existingItems[ac.icao]; + if (existingItem) { + // Update selection state + existingItem.className = `aircraft-item ${selectedIcao === ac.icao ? 'selected' : ''}`; + // Update inner content + existingItem.innerHTML = buildAircraftItemHTML(ac); + } + }); + } + } + + function buildAircraftItemHTML(ac) { + const callsign = ac.callsign || '------'; + const alt = ac.altitude ? ac.altitude.toLocaleString() : '---'; + const speed = ac.speed || '---'; + const heading = ac.heading ? ac.heading + '°' : '---'; + + return ` +
+ ${callsign} + ${ac.icao} +
+
+
+
${alt}
+
ALT ft
- `; - }).join(''); +
+
${speed}
+
SPD kts
+
+
+
${heading}
+
HDG
+
+
+ `; } function selectAircraft(icao) { @@ -990,6 +1083,7 @@ function cleanupOldAircraft() { const now = Date.now(); const timeout = 60000; // 60 seconds + let needsUpdate = false; Object.keys(aircraft).forEach(icao => { if (now - aircraft[icao].lastSeen > timeout) { @@ -1000,6 +1094,7 @@ } // Remove aircraft delete aircraft[icao]; + needsUpdate = true; // Clear selection if this was selected if (selectedIcao === icao) { @@ -1009,8 +1104,10 @@ } }); - updateStats(); - renderAircraftList(); + // Use batched update instead of direct calls + if (needsUpdate) { + scheduleUIUpdate(); + } } diff --git a/templates/index.html b/templates/index.html index ccdb964..13b6f8d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3142,6 +3142,7 @@
+ Full Screen Dashboard

ADS-B Receiver

@@ -3206,6 +3207,7 @@
+ Full Screen Dashboard
@@ -6150,6 +6152,37 @@ document.getElementById('stopWifiBtn').style.display = running ? 'block' : 'none'; } + // Batching state for WiFi updates + let pendingWifiUpdate = false; + let pendingWifiNetworks = []; + let pendingWifiClients = []; + + function scheduleWifiUIUpdate() { + if (pendingWifiUpdate) return; + pendingWifiUpdate = true; + requestAnimationFrame(() => { + // Process networks + pendingWifiNetworks.forEach(data => handleWifiNetworkImmediate(data)); + pendingWifiNetworks = []; + + // Process clients (limit to last 5 per frame) + const clientsToProcess = pendingWifiClients.slice(-5); + pendingWifiClients = []; + clientsToProcess.forEach(data => handleWifiClientImmediate(data)); + + // Update graphs once per frame instead of per-network + updateChannelGraph(); + updateChannel5gGraph(); + + // Update probe analysis (throttled) + if (clientsToProcess.length > 0) { + scheduleProbeAnalysisUpdate(); + } + + pendingWifiUpdate = false; + }); + } + // Start WiFi event stream function startWifiStream() { if (wifiEventSource) { @@ -6162,9 +6195,11 @@ const data = JSON.parse(e.data); if (data.type === 'network') { - handleWifiNetwork(data); + pendingWifiNetworks.push(data); + scheduleWifiUIUpdate(); } else if (data.type === 'client') { - handleWifiClient(data); + pendingWifiClients.push(data); + scheduleWifiUIUpdate(); } else if (data.type === 'info' || data.type === 'raw') { showInfo(data.text); } else if (data.type === 'error') { @@ -6181,8 +6216,8 @@ }; } - // Handle discovered WiFi network - function handleWifiNetwork(net) { + // Handle discovered WiFi network (called from batched update) + function handleWifiNetworkImmediate(net) { const isNew = !wifiNetworks[net.bssid]; wifiNetworks[net.bssid] = net; @@ -6229,14 +6264,11 @@ // Add to output addWifiNetworkCard(net, isNew); - - // Update both channel graphs - updateChannelGraph(); - updateChannel5gGraph(); + // Note: Channel graphs are updated in the batched scheduleWifiUIUpdate } - // Handle discovered WiFi client - function handleWifiClient(client) { + // Handle discovered WiFi client (called from batched update) + function handleWifiClientImmediate(client) { const isNew = !wifiClients[client.mac]; wifiClients[client.mac] = client; @@ -6260,9 +6292,17 @@ bssid: client.bssid, vendor: client.vendor }); + // Note: Probe analysis updated separately if needed + } - // Update probe analysis - updateProbeAnalysis(); + // Throttled probe analysis (called less frequently) + let lastProbeAnalysisUpdate = 0; + function scheduleProbeAnalysisUpdate() { + const now = Date.now(); + if (now - lastProbeAnalysisUpdate > 2000) { + lastProbeAnalysisUpdate = now; + updateProbeAnalysis(); + } } // Update client probe analysis panel @@ -7107,6 +7147,31 @@ document.getElementById('stopBtBtn').style.display = running ? 'block' : 'none'; } + // Batching state for Bluetooth updates + let pendingBtUpdate = false; + let pendingBtDevices = []; + + function scheduleBtUIUpdate() { + if (pendingBtUpdate) return; + pendingBtUpdate = true; + requestAnimationFrame(() => { + // Process devices (limit to 10 per frame) + const devicesToProcess = pendingBtDevices.slice(0, 10); + pendingBtDevices = pendingBtDevices.slice(10); + + devicesToProcess.forEach(data => handleBtDeviceImmediate(data)); + + // If more pending, schedule another frame + if (pendingBtDevices.length > 0) { + pendingBtUpdate = false; + scheduleBtUIUpdate(); + return; + } + + pendingBtUpdate = false; + }); + } + // Start Bluetooth event stream function startBtStream() { if (btEventSource) btEventSource.close(); @@ -7117,7 +7182,8 @@ const data = JSON.parse(e.data); if (data.type === 'device') { - handleBtDevice(data); + pendingBtDevices.push(data); + scheduleBtUIUpdate(); } else if (data.type === 'info' || data.type === 'raw') { showInfo(data.text); } else if (data.type === 'error') { @@ -7265,8 +7331,8 @@ } } - // Handle discovered Bluetooth device - function handleBtDevice(device) { + // Handle discovered Bluetooth device (called from batched update) + function handleBtDeviceImmediate(device) { const isNew = !btDevices[device.mac]; // Check for Find My network @@ -7643,6 +7709,51 @@ } let aircraftTrailLines = {}; // ICAO -> Leaflet polyline + let aircraftMarkerState = {}; // Cache marker state to avoid unnecessary updates + const MAX_AIRCRAFT_MARKERS = 150; // Limit markers to prevent browser freeze + + function buildTooltipText(aircraft, showLabels, showAltitude) { + if (!showLabels && !showAltitude) return ''; + let text = ''; + if (showLabels && aircraft.callsign) text = aircraft.callsign; + if (showAltitude && aircraft.altitude) { + if (text) text += ' '; + text += 'FL' + Math.round(aircraft.altitude / 100).toString().padStart(3, '0'); + } + return text; + } + + function buildPopupContent(icao) { + const aircraft = adsbAircraft[icao]; + if (!aircraft) return ''; + + const squawkInfo = checkSquawkCode(aircraft); + const militaryInfo = isMilitaryAircraft(icao, aircraft.callsign); + + let content = '
'; + if (militaryInfo.military) { + content += `
🎖️ MILITARY${militaryInfo.country ? ' (' + militaryInfo.country + ')' : ''}
`; + } + if (squawkInfo) { + content += `
⚠️ ${squawkInfo.name}
`; + } + content += `
${aircraft.callsign || icao}
`; + if (aircraft.altitude) { + content += `
Altitude:${aircraft.altitude.toLocaleString()} ft
`; + } + if (aircraft.speed) { + content += `
Speed:${aircraft.speed} kts
`; + } + if (aircraft.heading !== undefined) { + content += `
Heading:${aircraft.heading}°
`; + } + if (aircraft.squawk) { + const squawkStyle = squawkInfo ? `color: ${squawkInfo.color}; font-weight: bold;` : ''; + content += `
Squawk:${aircraft.squawk}
`; + } + content += '
'; + return content; + } function updateAircraftMarkers() { if (!aircraftMap) return; @@ -7652,10 +7763,14 @@ const showTrails = document.getElementById('adsbShowTrails')?.checked ?? true; const currentIds = new Set(); - // Update or create markers for each aircraft - Object.entries(adsbAircraft).forEach(([icao, aircraft]) => { - if (aircraft.lat == null || aircraft.lon == null) return; + // Sort aircraft by altitude and limit to prevent DOM explosion + const sortedAircraft = Object.entries(adsbAircraft) + .filter(([_, a]) => a.lat != null && a.lon != null) + .sort((a, b) => (b[1].altitude || 0) - (a[1].altitude || 0)) + .slice(0, MAX_AIRCRAFT_MARKERS); + // Update or create markers for each aircraft + sortedAircraft.forEach(([icao, aircraft]) => { currentIds.add(icao); // Update trail history @@ -7674,13 +7789,27 @@ else if (militaryInfo.military) iconColor = '#556b2f'; // Olive drab else if (aircraft.emergency) iconColor = '#ff4444'; - const icon = createAircraftIcon(aircraft.heading, squawkInfo || aircraft.emergency, iconColor); + // Round heading to reduce icon recreations + const roundedHeading = Math.round((aircraft.heading || 0) / 5) * 5; + + // Check if icon state actually changed + const prevState = aircraftMarkerState[icao] || {}; + const iconChanged = prevState.heading !== roundedHeading || + prevState.color !== iconColor || + prevState.emergency !== (squawkInfo || aircraft.emergency); if (aircraftMarkers[icao]) { - // Update existing marker + // Update existing marker - position is cheap aircraftMarkers[icao].setLatLng([aircraft.lat, aircraft.lon]); - aircraftMarkers[icao].setIcon(icon); + // Only update icon if it actually changed + if (iconChanged) { + const icon = createAircraftIcon(roundedHeading, squawkInfo || aircraft.emergency, iconColor); + aircraftMarkers[icao].setIcon(icon); + aircraftMarkerState[icao] = { heading: roundedHeading, color: iconColor, emergency: squawkInfo || aircraft.emergency }; + } } else { + const icon = createAircraftIcon(roundedHeading, squawkInfo || aircraft.emergency, iconColor); + aircraftMarkerState[icao] = { heading: roundedHeading, color: iconColor, emergency: squawkInfo || aircraft.emergency }; // Create new marker const marker = L.marker([aircraft.lat, aircraft.lon], { icon: icon }); if (clusteringEnabled && aircraftClusterGroup) { @@ -7710,46 +7839,14 @@ delete aircraftTrailLines[icao]; } - // Update popup content - let popupContent = '
'; + // Only update popup/tooltip if data changed (expensive operations) + const tooltipText = buildTooltipText(aircraft, showLabels, showAltitude); + const prevTooltip = prevState.tooltipText; - // Military badge - if (militaryInfo.military) { - popupContent += `
🎖️ MILITARY${militaryInfo.country ? ' (' + militaryInfo.country + ')' : ''}
`; - } - - // Squawk alert - if (squawkInfo) { - popupContent += `
⚠️ ${squawkInfo.name}
`; - } - - popupContent += `
${aircraft.callsign || icao}
`; - - if (aircraft.altitude) { - popupContent += `
Altitude:${aircraft.altitude.toLocaleString()} ft
`; - } - if (aircraft.speed) { - popupContent += `
Speed:${aircraft.speed} kts
`; - } - if (aircraft.heading !== undefined) { - popupContent += `
Heading:${aircraft.heading}°
`; - } - if (aircraft.squawk) { - const squawkStyle = squawkInfo ? `color: ${squawkInfo.color}; font-weight: bold;` : ''; - popupContent += `
Squawk:${aircraft.squawk}
`; - } - popupContent += '
'; - - aircraftMarkers[icao].bindPopup(popupContent); - - // Add tooltip if labels enabled - if (showLabels || showAltitude) { - let tooltipText = ''; - if (showLabels && aircraft.callsign) tooltipText = aircraft.callsign; - if (showAltitude && aircraft.altitude) { - if (tooltipText) tooltipText += ' '; - tooltipText += 'FL' + Math.round(aircraft.altitude / 100).toString().padStart(3, '0'); - } + // Only rebind tooltip if content changed + if (tooltipText !== prevTooltip) { + aircraftMarkerState[icao].tooltipText = tooltipText; + aircraftMarkers[icao].unbindTooltip(); if (tooltipText) { aircraftMarkers[icao].bindTooltip(tooltipText, { permanent: true, @@ -7757,8 +7854,12 @@ className: 'aircraft-tooltip' }); } - } else { - aircraftMarkers[icao].unbindTooltip(); + } + + // Bind popup lazily - content is built on open, not every update + if (!aircraftMarkers[icao]._hasPopupBound) { + aircraftMarkers[icao].bindPopup(() => buildPopupContent(icao)); + aircraftMarkers[icao]._hasPopupBound = true; } }); @@ -7777,6 +7878,7 @@ } delete aircraftTrails[icao]; delete aircraftMarkers[icao]; + delete aircraftMarkerState[icao]; delete activeSquawkAlerts[icao]; } }); @@ -7792,8 +7894,10 @@ document.getElementById('mapCenter').textContent = `${center.lat.toFixed(2)}, ${center.lng.toFixed(2)}`; - // Auto-fit bounds if we have aircraft - if (aircraftCount > 0 && !aircraftMap._userInteracted) { + // Auto-fit bounds if we have aircraft (throttled to avoid performance issues) + const now = Date.now(); + if (aircraftCount > 0 && !aircraftMap._userInteracted && + (!aircraftMap._lastFitBounds || now - aircraftMap._lastFitBounds > 5000)) { const bounds = []; Object.values(adsbAircraft).forEach(a => { if (a.lat !== undefined && a.lon !== undefined) { @@ -7802,6 +7906,7 @@ }); if (bounds.length > 0) { aircraftMap.fitBounds(bounds, { padding: [30, 30], maxZoom: 10 }); + aircraftMap._lastFitBounds = now; } } } @@ -7855,6 +7960,24 @@ }); } + // Batching state for aircraft updates to prevent browser freeze + let pendingAircraftUpdate = false; + let pendingAircraftData = []; + + function scheduleAircraftUIUpdate() { + if (pendingAircraftUpdate) return; + pendingAircraftUpdate = true; + requestAnimationFrame(() => { + updateAdsbStats(); + updateAircraftMarkers(); + // Batch output updates - only show last 10 to prevent DOM explosion + const toOutput = pendingAircraftData.slice(-10); + pendingAircraftData = []; + toOutput.forEach(data => addAircraftToOutput(data)); + pendingAircraftUpdate = false; + }); + } + function startAdsbStream() { if (adsbEventSource) adsbEventSource.close(); adsbEventSource = new EventSource('/adsb/stream'); @@ -7868,21 +7991,25 @@ lastSeen: Date.now() }; adsbMsgCount++; - updateAdsbStats(); - updateAircraftMarkers(); - addAircraftToOutput(data); + pendingAircraftData.push(data); + // Use batched update instead of immediate + scheduleAircraftUIUpdate(); } }; // Periodic cleanup of stale aircraft setInterval(() => { const now = Date.now(); + let needsUpdate = false; Object.keys(adsbAircraft).forEach(icao => { if (now - adsbAircraft[icao].lastSeen > 60000) { delete adsbAircraft[icao]; + needsUpdate = true; } }); - updateAircraftMarkers(); + if (needsUpdate) { + scheduleAircraftUIUpdate(); + } }, 5000); } diff --git a/templates/satellite_dashboard.html b/templates/satellite_dashboard.html index 1e45e9e..0e3dc9d 100644 --- a/templates/satellite_dashboard.html +++ b/templates/satellite_dashboard.html @@ -705,6 +705,7 @@ TRACKING ACTIVE
--:--:-- UTC
+ ← Main Dashboard