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:
Ole-Morten Duesund 2025-09-01 10:29:35 +02:00
commit 10508c2dc6
5 changed files with 145 additions and 3 deletions

View file

@ -202,7 +202,7 @@ func (a *AircraftState) MarshalJSON() ([]byte, error) {
SpeedHistory: a.SpeedHistory,
Distance: a.Distance,
Bearing: a.Bearing,
Age: a.Age,
Age: time.Since(a.LastUpdate).Seconds(),
MLATSources: a.MLATSources,
PositionSource: a.PositionSource,
UpdateRate: a.UpdateRate,

View 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)
}