2025-08-31 16:48:28 +02:00
|
|
|
package database
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"database/sql"
|
|
|
|
|
"fmt"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type HistoryManager struct {
|
|
|
|
|
db *sql.DB
|
|
|
|
|
mutex sync.RWMutex
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
// Configuration
|
|
|
|
|
maxHistoryDays int
|
|
|
|
|
cleanupTicker *time.Ticker
|
|
|
|
|
stopCleanup chan bool
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func NewHistoryManager(db *sql.DB, maxHistoryDays int) *HistoryManager {
|
|
|
|
|
hm := &HistoryManager{
|
|
|
|
|
db: db,
|
|
|
|
|
maxHistoryDays: maxHistoryDays,
|
|
|
|
|
stopCleanup: make(chan bool),
|
|
|
|
|
}
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
// Start periodic cleanup (every hour)
|
|
|
|
|
hm.cleanupTicker = time.NewTicker(1 * time.Hour)
|
|
|
|
|
go hm.periodicCleanup()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
return hm
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (hm *HistoryManager) Close() {
|
|
|
|
|
if hm.cleanupTicker != nil {
|
|
|
|
|
hm.cleanupTicker.Stop()
|
|
|
|
|
}
|
|
|
|
|
if hm.stopCleanup != nil {
|
|
|
|
|
close(hm.stopCleanup)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (hm *HistoryManager) periodicCleanup() {
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-hm.cleanupTicker.C:
|
|
|
|
|
if err := hm.CleanupOldHistory(); err != nil {
|
|
|
|
|
fmt.Printf("Warning: failed to cleanup old history: %v\n", err)
|
|
|
|
|
}
|
|
|
|
|
case <-hm.stopCleanup:
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (hm *HistoryManager) RecordAircraft(record *AircraftHistoryRecord) error {
|
|
|
|
|
hm.mutex.Lock()
|
|
|
|
|
defer hm.mutex.Unlock()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
query := `
|
|
|
|
|
INSERT INTO aircraft_history
|
|
|
|
|
(icao, callsign, squawk, latitude, longitude, altitude,
|
|
|
|
|
vertical_rate, speed, track, source_id, signal_strength, timestamp)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
`
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
_, err := hm.db.Exec(query,
|
|
|
|
|
record.ICAO,
|
|
|
|
|
record.Callsign,
|
|
|
|
|
record.Squawk,
|
|
|
|
|
record.Latitude,
|
|
|
|
|
record.Longitude,
|
|
|
|
|
record.Altitude,
|
|
|
|
|
record.VerticalRate,
|
|
|
|
|
record.Speed,
|
|
|
|
|
record.Track,
|
|
|
|
|
record.SourceID,
|
|
|
|
|
record.SignalStrength,
|
|
|
|
|
record.Timestamp,
|
|
|
|
|
)
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (hm *HistoryManager) RecordAircraftBatch(records []AircraftHistoryRecord) error {
|
|
|
|
|
if len(records) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
hm.mutex.Lock()
|
|
|
|
|
defer hm.mutex.Unlock()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
tx, err := hm.db.Begin()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer tx.Rollback()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
stmt, err := tx.Prepare(`
|
|
|
|
|
INSERT INTO aircraft_history
|
|
|
|
|
(icao, callsign, squawk, latitude, longitude, altitude,
|
|
|
|
|
vertical_rate, speed, track, source_id, signal_strength, timestamp)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
|
|
|
`)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
defer stmt.Close()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
for _, record := range records {
|
|
|
|
|
_, err := stmt.Exec(
|
|
|
|
|
record.ICAO,
|
|
|
|
|
record.Callsign,
|
|
|
|
|
record.Squawk,
|
|
|
|
|
record.Latitude,
|
|
|
|
|
record.Longitude,
|
|
|
|
|
record.Altitude,
|
|
|
|
|
record.VerticalRate,
|
|
|
|
|
record.Speed,
|
|
|
|
|
record.Track,
|
|
|
|
|
record.SourceID,
|
|
|
|
|
record.SignalStrength,
|
|
|
|
|
record.Timestamp,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("failed to insert record for ICAO %s: %w", record.ICAO, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
return tx.Commit()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (hm *HistoryManager) GetAircraftHistory(icao string, hours int) ([]AircraftHistoryRecord, error) {
|
|
|
|
|
hm.mutex.RLock()
|
|
|
|
|
defer hm.mutex.RUnlock()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
query := `
|
|
|
|
|
SELECT icao, callsign, squawk, latitude, longitude, altitude,
|
|
|
|
|
vertical_rate, speed, track, source_id, signal_strength, timestamp
|
|
|
|
|
FROM aircraft_history
|
|
|
|
|
WHERE icao = ? AND timestamp >= ?
|
|
|
|
|
ORDER BY timestamp DESC
|
|
|
|
|
LIMIT 1000
|
|
|
|
|
`
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
rows, err := hm.db.Query(query, icao, since)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
var records []AircraftHistoryRecord
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var record AircraftHistoryRecord
|
|
|
|
|
err := rows.Scan(
|
|
|
|
|
&record.ICAO,
|
|
|
|
|
&record.Callsign,
|
|
|
|
|
&record.Squawk,
|
|
|
|
|
&record.Latitude,
|
|
|
|
|
&record.Longitude,
|
|
|
|
|
&record.Altitude,
|
|
|
|
|
&record.VerticalRate,
|
|
|
|
|
&record.Speed,
|
|
|
|
|
&record.Track,
|
|
|
|
|
&record.SourceID,
|
|
|
|
|
&record.SignalStrength,
|
|
|
|
|
&record.Timestamp,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
records = append(records, record)
|
|
|
|
|
}
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
return records, rows.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (hm *HistoryManager) GetAircraftTrack(icao string, hours int) ([]TrackPoint, error) {
|
|
|
|
|
hm.mutex.RLock()
|
|
|
|
|
defer hm.mutex.RUnlock()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
query := `
|
|
|
|
|
SELECT latitude, longitude, altitude, timestamp
|
|
|
|
|
FROM aircraft_history
|
|
|
|
|
WHERE icao = ? AND timestamp >= ?
|
|
|
|
|
AND latitude IS NOT NULL AND longitude IS NOT NULL
|
|
|
|
|
ORDER BY timestamp ASC
|
|
|
|
|
LIMIT 500
|
|
|
|
|
`
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
rows, err := hm.db.Query(query, icao, since)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
var track []TrackPoint
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var point TrackPoint
|
|
|
|
|
err := rows.Scan(
|
|
|
|
|
&point.Latitude,
|
|
|
|
|
&point.Longitude,
|
|
|
|
|
&point.Altitude,
|
|
|
|
|
&point.Timestamp,
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
track = append(track, point)
|
|
|
|
|
}
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
return track, rows.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (hm *HistoryManager) GetRecentAircraft(hours int, limit int) ([]string, error) {
|
|
|
|
|
hm.mutex.RLock()
|
|
|
|
|
defer hm.mutex.RUnlock()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
query := `
|
|
|
|
|
SELECT DISTINCT icao
|
|
|
|
|
FROM aircraft_history
|
|
|
|
|
WHERE timestamp >= ?
|
|
|
|
|
ORDER BY MAX(timestamp) DESC
|
|
|
|
|
LIMIT ?
|
|
|
|
|
`
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
rows, err := hm.db.Query(query, since, limit)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
var icaos []string
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var icao string
|
|
|
|
|
err := rows.Scan(&icao)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
icaos = append(icaos, icao)
|
|
|
|
|
}
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
return icaos, rows.Err()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (hm *HistoryManager) GetAircraftLastSeen(icao string) (time.Time, error) {
|
|
|
|
|
hm.mutex.RLock()
|
|
|
|
|
defer hm.mutex.RUnlock()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
query := `
|
|
|
|
|
SELECT MAX(timestamp)
|
|
|
|
|
FROM aircraft_history
|
|
|
|
|
WHERE icao = ?
|
|
|
|
|
`
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
var lastSeen time.Time
|
|
|
|
|
err := hm.db.QueryRow(query, icao).Scan(&lastSeen)
|
|
|
|
|
return lastSeen, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (hm *HistoryManager) CleanupOldHistory() error {
|
|
|
|
|
hm.mutex.Lock()
|
|
|
|
|
defer hm.mutex.Unlock()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
if hm.maxHistoryDays <= 0 {
|
|
|
|
|
return nil // No cleanup if maxHistoryDays is 0 or negative
|
|
|
|
|
}
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
cutoff := time.Now().AddDate(0, 0, -hm.maxHistoryDays)
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
query := `DELETE FROM aircraft_history WHERE timestamp < ?`
|
|
|
|
|
result, err := hm.db.Exec(query, cutoff)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
|
|
|
if err == nil && rowsAffected > 0 {
|
|
|
|
|
fmt.Printf("Cleaned up %d old aircraft history records\n", rowsAffected)
|
|
|
|
|
}
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (hm *HistoryManager) GetStatistics() (map[string]interface{}, error) {
|
|
|
|
|
return hm.GetHistoryStats()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (hm *HistoryManager) GetHistoryStats() (map[string]interface{}, error) {
|
|
|
|
|
hm.mutex.RLock()
|
|
|
|
|
defer hm.mutex.RUnlock()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
stats := make(map[string]interface{})
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
// Total records
|
|
|
|
|
var totalRecords int
|
|
|
|
|
err := hm.db.QueryRow(`SELECT COUNT(*) FROM aircraft_history`).Scan(&totalRecords)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
stats["total_records"] = totalRecords
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
// Unique aircraft
|
|
|
|
|
var uniqueAircraft int
|
|
|
|
|
err = hm.db.QueryRow(`SELECT COUNT(DISTINCT icao) FROM aircraft_history`).Scan(&uniqueAircraft)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
stats["unique_aircraft"] = uniqueAircraft
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
// Recent records (last 24 hours)
|
|
|
|
|
var recentRecords int
|
|
|
|
|
since := time.Now().Add(-24 * time.Hour)
|
|
|
|
|
err = hm.db.QueryRow(`SELECT COUNT(*) FROM aircraft_history WHERE timestamp >= ?`, since).Scan(&recentRecords)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
stats["recent_records_24h"] = recentRecords
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
// Oldest and newest record timestamps (only if records exist)
|
|
|
|
|
if totalRecords > 0 {
|
|
|
|
|
var oldestTimestamp, newestTimestamp time.Time
|
|
|
|
|
err = hm.db.QueryRow(`SELECT MIN(timestamp), MAX(timestamp) FROM aircraft_history`).Scan(&oldestTimestamp, &newestTimestamp)
|
|
|
|
|
if err == nil {
|
|
|
|
|
stats["oldest_record"] = oldestTimestamp
|
|
|
|
|
stats["newest_record"] = newestTimestamp
|
|
|
|
|
stats["history_days"] = int(time.Since(oldestTimestamp).Hours() / 24)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
return stats, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (hm *HistoryManager) GetActivitySummary(hours int) (map[string]interface{}, error) {
|
|
|
|
|
hm.mutex.RLock()
|
|
|
|
|
defer hm.mutex.RUnlock()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
summary := make(map[string]interface{})
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
// Aircraft count in time period
|
|
|
|
|
var aircraftCount int
|
|
|
|
|
err := hm.db.QueryRow(`
|
|
|
|
|
SELECT COUNT(DISTINCT icao)
|
|
|
|
|
FROM aircraft_history
|
|
|
|
|
WHERE timestamp >= ?
|
|
|
|
|
`, since).Scan(&aircraftCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
summary["aircraft_count"] = aircraftCount
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
// Message count in time period
|
|
|
|
|
var messageCount int
|
|
|
|
|
err = hm.db.QueryRow(`
|
|
|
|
|
SELECT COUNT(*)
|
|
|
|
|
FROM aircraft_history
|
|
|
|
|
WHERE timestamp >= ?
|
|
|
|
|
`, since).Scan(&messageCount)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
summary["message_count"] = messageCount
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
// Most active sources
|
|
|
|
|
query := `
|
|
|
|
|
SELECT source_id, COUNT(*) as count
|
|
|
|
|
FROM aircraft_history
|
|
|
|
|
WHERE timestamp >= ?
|
|
|
|
|
GROUP BY source_id
|
|
|
|
|
ORDER BY count DESC
|
|
|
|
|
LIMIT 5
|
|
|
|
|
`
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
rows, err := hm.db.Query(query, since)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
sources := make([]map[string]interface{}, 0)
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
var sourceID string
|
|
|
|
|
var count int
|
|
|
|
|
err := rows.Scan(&sourceID, &count)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
sources = append(sources, map[string]interface{}{
|
|
|
|
|
"source_id": sourceID,
|
|
|
|
|
"count": count,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
summary["top_sources"] = sources
|
2025-09-01 10:05:29 +02:00
|
|
|
|
2025-08-31 16:48:28 +02:00
|
|
|
return summary, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type TrackPoint struct {
|
|
|
|
|
Latitude float64 `json:"latitude"`
|
|
|
|
|
Longitude float64 `json:"longitude"`
|
|
|
|
|
Altitude *int `json:"altitude,omitempty"`
|
|
|
|
|
Timestamp time.Time `json:"timestamp"`
|
2025-09-01 10:05:29 +02:00
|
|
|
}
|