fix: Implement dynamic age calculation for aircraft tracking
Resolves issue #3 where aircraft age field was always showing 0 seconds. Backend Changes: - Fix MarshalJSON in merger.go to calculate age dynamically using time.Since(a.LastUpdate) - Add comprehensive tests for age calculation in merger_test.go Frontend Changes: - Add calculateAge() utility method to both aircraft-manager.js and ui-manager.js - Update popup content and aircraft table to use client-side age calculation - Implement real-time age updates every second in app.js - Add updateOpenPopupAges() function to refresh popup displays Key Benefits: - Aircraft age now shows actual seconds since last transmission - Real-time updates in UI without requiring new WebSocket data - Proper data staleness indicators for aviation tracking - No additional server load - client-side calculation using last_update timestamp 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
6efd673507
commit
10508c2dc6
5 changed files with 145 additions and 3 deletions
|
|
@ -469,10 +469,40 @@ class SkyView {
|
||||||
this.radar3d.renderer.render(this.radar3d.scene, this.radar3d.camera);
|
this.radar3d.renderer.render(this.radar3d.scene, this.radar3d.camera);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateOpenPopupAges() {
|
||||||
|
// Find any open aircraft popups and update their age displays
|
||||||
|
if (!this.aircraftManager) return;
|
||||||
|
|
||||||
|
this.aircraftManager.aircraftMarkers.forEach((marker, icao) => {
|
||||||
|
if (marker.isPopupOpen()) {
|
||||||
|
const aircraft = this.aircraftManager.aircraftData.get(icao);
|
||||||
|
if (aircraft) {
|
||||||
|
// Refresh the popup content with current age
|
||||||
|
marker.setPopupContent(this.aircraftManager.createPopupContent(aircraft));
|
||||||
|
|
||||||
|
// Re-enhance callsign display for the updated popup
|
||||||
|
const popupElement = marker.getPopup().getElement();
|
||||||
|
if (popupElement) {
|
||||||
|
this.aircraftManager.enhanceCallsignDisplay(popupElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
startPeriodicTasks() {
|
startPeriodicTasks() {
|
||||||
// Update clocks every second
|
// Update clocks every second
|
||||||
setInterval(() => this.uiManager.updateClocks(), 1000);
|
setInterval(() => this.uiManager.updateClocks(), 1000);
|
||||||
|
|
||||||
|
// Update aircraft ages and refresh displays every second
|
||||||
|
setInterval(() => {
|
||||||
|
// Update aircraft table to show current ages
|
||||||
|
this.uiManager.updateAircraftTable();
|
||||||
|
|
||||||
|
// Update any open aircraft popups with current ages
|
||||||
|
this.updateOpenPopupAges();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
// Update charts every 10 seconds
|
// Update charts every 10 seconds
|
||||||
setInterval(() => this.updateCharts(), 10000);
|
setInterval(() => this.updateCharts(), 10000);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -491,7 +491,7 @@ export class AircraftManager {
|
||||||
<strong>Messages:</strong> ${aircraft.TotalMessages || 0}
|
<strong>Messages:</strong> ${aircraft.TotalMessages || 0}
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<strong>Age:</strong> ${aircraft.Age ? aircraft.Age.toFixed(1) : '0'}s
|
<strong>Age:</strong> ${this.calculateAge(aircraft).toFixed(1)}s
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -513,6 +513,16 @@ export class AircraftManager {
|
||||||
return minDistance === Infinity ? null : minDistance;
|
return minDistance === Infinity ? null : minDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
calculateAge(aircraft) {
|
||||||
|
if (!aircraft.last_update) return 0;
|
||||||
|
|
||||||
|
const lastUpdate = new Date(aircraft.last_update);
|
||||||
|
const now = new Date();
|
||||||
|
const ageMs = now.getTime() - lastUpdate.getTime();
|
||||||
|
|
||||||
|
return Math.max(0, ageMs / 1000); // Return age in seconds, minimum 0
|
||||||
|
}
|
||||||
|
|
||||||
// Enhance callsign display in popup after it's created
|
// Enhance callsign display in popup after it's created
|
||||||
async enhanceCallsignDisplay(popupElement) {
|
async enhanceCallsignDisplay(popupElement) {
|
||||||
if (!this.callsignManager) return;
|
if (!this.callsignManager) return;
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ export class UIManager {
|
||||||
<td>${aircraft.Track || '-'}°</td>
|
<td>${aircraft.Track || '-'}°</td>
|
||||||
<td>${sources}</td>
|
<td>${sources}</td>
|
||||||
<td><span class="${this.getSignalClass(bestSignal)}">${bestSignal ? bestSignal.toFixed(1) : '-'}</span></td>
|
<td><span class="${this.getSignalClass(bestSignal)}">${bestSignal ? bestSignal.toFixed(1) : '-'}</span></td>
|
||||||
<td>${aircraft.Age ? aircraft.Age.toFixed(0) : '0'}s</td>
|
<td>${this.calculateAge(aircraft).toFixed(0)}s</td>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
row.addEventListener('click', () => {
|
row.addEventListener('click', () => {
|
||||||
|
|
@ -161,6 +161,16 @@ export class UIManager {
|
||||||
return 'commercial';
|
return 'commercial';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
calculateAge(aircraft) {
|
||||||
|
if (!aircraft.last_update) return 0;
|
||||||
|
|
||||||
|
const lastUpdate = new Date(aircraft.last_update);
|
||||||
|
const now = new Date();
|
||||||
|
const ageMs = now.getTime() - lastUpdate.getTime();
|
||||||
|
|
||||||
|
return Math.max(0, ageMs / 1000); // Return age in seconds, minimum 0
|
||||||
|
}
|
||||||
|
|
||||||
getBestSignalFromSources(sources) {
|
getBestSignalFromSources(sources) {
|
||||||
if (!sources) return null;
|
if (!sources) return null;
|
||||||
let bestSignal = -999;
|
let bestSignal = -999;
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) {
|
||||||
SpeedHistory: a.SpeedHistory,
|
SpeedHistory: a.SpeedHistory,
|
||||||
Distance: a.Distance,
|
Distance: a.Distance,
|
||||||
Bearing: a.Bearing,
|
Bearing: a.Bearing,
|
||||||
Age: a.Age,
|
Age: time.Since(a.LastUpdate).Seconds(),
|
||||||
MLATSources: a.MLATSources,
|
MLATSources: a.MLATSources,
|
||||||
PositionSource: a.PositionSource,
|
PositionSource: a.PositionSource,
|
||||||
UpdateRate: a.UpdateRate,
|
UpdateRate: a.UpdateRate,
|
||||||
|
|
|
||||||
92
internal/merger/merger_test.go
Normal file
92
internal/merger/merger_test.go
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
package merger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"skyview/internal/modes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAircraftState_MarshalJSON_AgeCalculation(t *testing.T) {
|
||||||
|
// Create a test aircraft state with a known LastUpdate time
|
||||||
|
pastTime := time.Now().Add(-30 * time.Second) // 30 seconds ago
|
||||||
|
|
||||||
|
state := &AircraftState{
|
||||||
|
Aircraft: &modes.Aircraft{
|
||||||
|
ICAO24: 0x123456,
|
||||||
|
Callsign: "TEST123",
|
||||||
|
},
|
||||||
|
LastUpdate: pastTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal to JSON
|
||||||
|
jsonData, err := json.Marshal(state)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal AircraftState: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the JSON to check the age field
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.Unmarshal(jsonData, &result); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that age is not 0 and is approximately 30 seconds
|
||||||
|
age, ok := result["age"].(float64)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Age field not found or not a number")
|
||||||
|
}
|
||||||
|
|
||||||
|
if age < 25 || age > 35 {
|
||||||
|
t.Errorf("Expected age to be around 30 seconds, got %f", age)
|
||||||
|
}
|
||||||
|
|
||||||
|
if age == 0 {
|
||||||
|
t.Error("Age should not be 0 - this indicates the bug is still present")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Aircraft age calculated correctly: %f seconds", age)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAircraftState_MarshalJSON_ZeroAge(t *testing.T) {
|
||||||
|
// Test with very recent time to ensure age is very small but not exactly 0
|
||||||
|
recentTime := time.Now()
|
||||||
|
|
||||||
|
state := &AircraftState{
|
||||||
|
Aircraft: &modes.Aircraft{
|
||||||
|
ICAO24: 0x123456,
|
||||||
|
Callsign: "TEST123",
|
||||||
|
},
|
||||||
|
LastUpdate: recentTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal to JSON
|
||||||
|
jsonData, err := json.Marshal(state)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to marshal AircraftState: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the JSON to check the age field
|
||||||
|
var result map[string]interface{}
|
||||||
|
if err := json.Unmarshal(jsonData, &result); err != nil {
|
||||||
|
t.Fatalf("Failed to unmarshal JSON: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that age is very small but should not be exactly 0 (unless extremely fast)
|
||||||
|
age, ok := result["age"].(float64)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("Age field not found or not a number")
|
||||||
|
}
|
||||||
|
|
||||||
|
if age < 0 {
|
||||||
|
t.Errorf("Age should not be negative, got %f", age)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Age should be very small (less than 1 second) but might not be exactly 0
|
||||||
|
if age > 1.0 {
|
||||||
|
t.Errorf("Age should be very small for recent update, got %f", age)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Recent aircraft age calculated correctly: %f seconds", age)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue