style: Apply code formatting with go fmt

- Run 'make format' to ensure all Go code follows standard formatting
- Maintains consistent code style across the entire codebase
- No functional changes, only whitespace and formatting improvements
This commit is contained in:
Ole-Morten Duesund 2025-09-01 10:05:29 +02:00
commit 2bffa2c418
19 changed files with 543 additions and 527 deletions

View file

@ -29,5 +29,6 @@ import "embed"
// external file deployment or complicated asset management. // external file deployment or complicated asset management.
// //
// Updated to include database.html for database status page // Updated to include database.html for database status page
//
//go:embed static //go:embed static
var Static embed.FS var Static embed.FS

View file

@ -19,28 +19,28 @@ import (
// Shared configuration structures (should match main skyview) // Shared configuration structures (should match main skyview)
type Config struct { type Config struct {
Server ServerConfig `json:"server"` Server ServerConfig `json:"server"`
Sources []SourceConfig `json:"sources"` Sources []SourceConfig `json:"sources"`
Settings Settings `json:"settings"` Settings Settings `json:"settings"`
Database *database.Config `json:"database,omitempty"` Database *database.Config `json:"database,omitempty"`
Callsign *CallsignConfig `json:"callsign,omitempty"` Callsign *CallsignConfig `json:"callsign,omitempty"`
Origin OriginConfig `json:"origin"` Origin OriginConfig `json:"origin"`
} }
type CallsignConfig struct { type CallsignConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
CacheHours int `json:"cache_hours"` CacheHours int `json:"cache_hours"`
PrivacyMode bool `json:"privacy_mode"` PrivacyMode bool `json:"privacy_mode"`
Sources map[string]CallsignSourceConfig `json:"sources"` Sources map[string]CallsignSourceConfig `json:"sources"`
ExternalAPIs map[string]ExternalAPIConfig `json:"external_apis,omitempty"` ExternalAPIs map[string]ExternalAPIConfig `json:"external_apis,omitempty"`
} }
type CallsignSourceConfig struct { type CallsignSourceConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Priority int `json:"priority"` Priority int `json:"priority"`
License string `json:"license"` License string `json:"license"`
RequiresConsent bool `json:"requires_consent,omitempty"` RequiresConsent bool `json:"requires_consent,omitempty"`
UserAcceptsTerms bool `json:"user_accepts_terms,omitempty"` UserAcceptsTerms bool `json:"user_accepts_terms,omitempty"`
} }
type ExternalAPIConfig struct { type ExternalAPIConfig struct {
@ -51,14 +51,14 @@ type ExternalAPIConfig struct {
} }
type OriginConfig struct { type OriginConfig struct {
Latitude float64 `json:"latitude"` Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"` Longitude float64 `json:"longitude"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
} }
type ServerConfig struct { type ServerConfig struct {
Host string `json:"host"` Host string `json:"host"`
Port int `json:"port"` Port int `json:"port"`
} }
type SourceConfig struct { type SourceConfig struct {
@ -152,7 +152,7 @@ OPTIONS:
if err != nil { if err != nil {
log.Fatalf("Configuration loading failed: %v", err) log.Fatalf("Configuration loading failed: %v", err)
} }
// Initialize database connection using shared config // Initialize database connection using shared config
db, err := initDatabaseFromConfig(config, *dbPath) db, err := initDatabaseFromConfig(config, *dbPath)
if err != nil { if err != nil {
@ -215,7 +215,7 @@ func initDatabase(dbPath string) (*database.Database, error) {
// cmdInit initializes an empty database // cmdInit initializes an empty database
func cmdInit(db *database.Database, force bool) error { func cmdInit(db *database.Database, force bool) error {
dbPath := db.GetConfig().Path dbPath := db.GetConfig().Path
// Check if database already exists and has data // Check if database already exists and has data
if !force { if !force {
if stats, err := db.GetHistoryManager().GetStatistics(); err == nil { if stats, err := db.GetHistoryManager().GetStatistics(); err == nil {
@ -271,19 +271,19 @@ func cmdList(db *database.Database) error {
func cmdStatus(db *database.Database) error { func cmdStatus(db *database.Database) error {
fmt.Println("SkyView Database Status") fmt.Println("SkyView Database Status")
fmt.Println("======================") fmt.Println("======================")
dbPath := db.GetConfig().Path dbPath := db.GetConfig().Path
fmt.Printf("Database: %s\n", dbPath) fmt.Printf("Database: %s\n", dbPath)
// Check if file exists and get size // Check if file exists and get size
if stat, err := os.Stat(dbPath); err == nil { if stat, err := os.Stat(dbPath); err == nil {
fmt.Printf("Size: %.2f MB\n", float64(stat.Size())/(1024*1024)) fmt.Printf("Size: %.2f MB\n", float64(stat.Size())/(1024*1024))
fmt.Printf("Modified: %s\n", stat.ModTime().Format(time.RFC3339)) fmt.Printf("Modified: %s\n", stat.ModTime().Format(time.RFC3339))
// Add database optimization stats // Add database optimization stats
optimizer := database.NewOptimizationManager(db, db.GetConfig()) optimizer := database.NewOptimizationManager(db, db.GetConfig())
if stats, err := optimizer.GetOptimizationStats(); err == nil { if stats, err := optimizer.GetOptimizationStats(); err == nil {
fmt.Printf("Efficiency: %.1f%% (%d used pages, %d free pages)\n", fmt.Printf("Efficiency: %.1f%% (%d used pages, %d free pages)\n",
stats.Efficiency, stats.UsedPages, stats.FreePages) stats.Efficiency, stats.UsedPages, stats.FreePages)
if stats.AutoVacuumEnabled { if stats.AutoVacuumEnabled {
fmt.Printf("Auto-VACUUM: Enabled\n") fmt.Printf("Auto-VACUUM: Enabled\n")
@ -320,7 +320,7 @@ func cmdStatus(db *database.Database) error {
var airportCount, airlineCount int var airportCount, airlineCount int
db.GetConnection().QueryRow(`SELECT COUNT(*) FROM airports`).Scan(&airportCount) db.GetConnection().QueryRow(`SELECT COUNT(*) FROM airports`).Scan(&airportCount)
db.GetConnection().QueryRow(`SELECT COUNT(*) FROM airlines`).Scan(&airlineCount) db.GetConnection().QueryRow(`SELECT COUNT(*) FROM airlines`).Scan(&airlineCount)
// Get data source update information // Get data source update information
var lastUpdate time.Time var lastUpdate time.Time
var updateCount int var updateCount int
@ -329,7 +329,7 @@ func cmdStatus(db *database.Database) error {
FROM data_sources FROM data_sources
WHERE imported_at IS NOT NULL WHERE imported_at IS NOT NULL
`).Scan(&updateCount, &lastUpdate) `).Scan(&updateCount, &lastUpdate)
fmt.Printf("📊 Database Statistics:\n") fmt.Printf("📊 Database Statistics:\n")
fmt.Printf(" Reference Data:\n") fmt.Printf(" Reference Data:\n")
if airportCount > 0 { if airportCount > 0 {
@ -344,7 +344,7 @@ func cmdStatus(db *database.Database) error {
fmt.Printf(" • Last Updated: %s\n", lastUpdate.Format("2006-01-02 15:04:05")) fmt.Printf(" • Last Updated: %s\n", lastUpdate.Format("2006-01-02 15:04:05"))
} }
} }
fmt.Printf(" Flight History:\n") fmt.Printf(" Flight History:\n")
if totalRecords, ok := stats["total_records"].(int); ok { if totalRecords, ok := stats["total_records"].(int); ok {
fmt.Printf(" • Aircraft Records: %d\n", totalRecords) fmt.Printf(" • Aircraft Records: %d\n", totalRecords)
@ -361,13 +361,13 @@ func cmdStatus(db *database.Database) error {
if hasOldest && hasNewest && oldestRecord != nil && newestRecord != nil { if hasOldest && hasNewest && oldestRecord != nil && newestRecord != nil {
if oldest, ok := oldestRecord.(time.Time); ok { if oldest, ok := oldestRecord.(time.Time); ok {
if newest, ok := newestRecord.(time.Time); ok { if newest, ok := newestRecord.(time.Time); ok {
fmt.Printf(" • Flight Data Range: %s to %s\n", fmt.Printf(" • Flight Data Range: %s to %s\n",
oldest.Format("2006-01-02"), oldest.Format("2006-01-02"),
newest.Format("2006-01-02")) newest.Format("2006-01-02"))
} }
} }
} }
// Show airport data sample if available // Show airport data sample if available
if airportCount > 0 { if airportCount > 0 {
var sampleAirports []string var sampleAirports []string
@ -398,7 +398,7 @@ func cmdStatus(db *database.Database) error {
// cmdUpdate updates data from specified sources (or safe sources by default) // cmdUpdate updates data from specified sources (or safe sources by default)
func cmdUpdate(db *database.Database, sources []string, force bool) error { func cmdUpdate(db *database.Database, sources []string, force bool) error {
availableSources := database.GetAvailableDataSources() availableSources := database.GetAvailableDataSources()
// If no sources specified, use safe (non-consent-required) sources // If no sources specified, use safe (non-consent-required) sources
if len(sources) == 0 { if len(sources) == 0 {
log.Println("Updating from safe data sources...") log.Println("Updating from safe data sources...")
@ -407,7 +407,7 @@ func cmdUpdate(db *database.Database, sources []string, force bool) error {
sources = append(sources, strings.ToLower(strings.ReplaceAll(source.Name, " ", ""))) sources = append(sources, strings.ToLower(strings.ReplaceAll(source.Name, " ", "")))
} }
} }
if len(sources) == 0 { if len(sources) == 0 {
log.Println("No safe data sources available for automatic update") log.Println("No safe data sources available for automatic update")
return nil return nil
@ -416,7 +416,7 @@ func cmdUpdate(db *database.Database, sources []string, force bool) error {
} }
loader := database.NewDataLoader(db.GetConnection()) loader := database.NewDataLoader(db.GetConnection())
for _, sourceName := range sources { for _, sourceName := range sources {
// Find matching source // Find matching source
var matchedSource *database.DataSource var matchedSource *database.DataSource
@ -426,13 +426,13 @@ func cmdUpdate(db *database.Database, sources []string, force bool) error {
break break
} }
} }
if matchedSource == nil { if matchedSource == nil {
log.Printf("⚠️ Unknown source: %s", sourceName) log.Printf("⚠️ Unknown source: %s", sourceName)
continue continue
} }
// Check for consent requirement // Check for consent requirement
if matchedSource.RequiresConsent && !force { if matchedSource.RequiresConsent && !force {
log.Printf("Skipping %s: requires license acceptance (%s)", matchedSource.Name, matchedSource.License) log.Printf("Skipping %s: requires license acceptance (%s)", matchedSource.Name, matchedSource.License)
log.Printf("Use --force to accept license terms, or 'skyview-data import %s' for interactive acceptance", sourceName) log.Printf("Use --force to accept license terms, or 'skyview-data import %s' for interactive acceptance", sourceName)
@ -446,7 +446,7 @@ func cmdUpdate(db *database.Database, sources []string, force bool) error {
} }
log.Printf("Loading %s...", matchedSource.Name) log.Printf("Loading %s...", matchedSource.Name)
result, err := loader.LoadDataSource(*matchedSource) result, err := loader.LoadDataSource(*matchedSource)
if err != nil { if err != nil {
log.Printf("Failed to load %s: %v", matchedSource.Name, err) log.Printf("Failed to load %s: %v", matchedSource.Name, err)
@ -454,11 +454,13 @@ func cmdUpdate(db *database.Database, sources []string, force bool) error {
} }
log.Printf("Loaded %s: %d records in %v", matchedSource.Name, result.RecordsNew, result.Duration) log.Printf("Loaded %s: %d records in %v", matchedSource.Name, result.RecordsNew, result.Duration)
if len(result.Errors) > 0 { if len(result.Errors) > 0 {
log.Printf(" %d errors occurred during import (first few):", len(result.Errors)) log.Printf(" %d errors occurred during import (first few):", len(result.Errors))
for i, errMsg := range result.Errors { for i, errMsg := range result.Errors {
if i >= 3 { break } if i >= 3 {
break
}
log.Printf(" %s", errMsg) log.Printf(" %s", errMsg)
} }
} }
@ -471,7 +473,7 @@ func cmdUpdate(db *database.Database, sources []string, force bool) error {
// cmdImport imports data from a specific source with interactive license acceptance // cmdImport imports data from a specific source with interactive license acceptance
func cmdImport(db *database.Database, sourceName string, force bool) error { func cmdImport(db *database.Database, sourceName string, force bool) error {
availableSources := database.GetAvailableDataSources() availableSources := database.GetAvailableDataSources()
var matchedSource *database.DataSource var matchedSource *database.DataSource
for _, available := range availableSources { for _, available := range availableSources {
if strings.EqualFold(strings.ReplaceAll(available.Name, " ", ""), sourceName) { if strings.EqualFold(strings.ReplaceAll(available.Name, " ", ""), sourceName) {
@ -479,7 +481,7 @@ func cmdImport(db *database.Database, sourceName string, force bool) error {
break break
} }
} }
if matchedSource == nil { if matchedSource == nil {
return fmt.Errorf("unknown data source: %s", sourceName) return fmt.Errorf("unknown data source: %s", sourceName)
} }
@ -491,17 +493,17 @@ func cmdImport(db *database.Database, sourceName string, force bool) error {
fmt.Printf(" URL: %s\n", matchedSource.URL) fmt.Printf(" URL: %s\n", matchedSource.URL)
fmt.Println() fmt.Println()
fmt.Printf("By importing this data, you agree to comply with the %s license terms.\n", matchedSource.License) fmt.Printf("By importing this data, you agree to comply with the %s license terms.\n", matchedSource.License)
if !askForConfirmation("Do you accept the license terms?") { if !askForConfirmation("Do you accept the license terms?") {
fmt.Println("Import cancelled.") fmt.Println("Import cancelled.")
return nil return nil
} }
matchedSource.UserAcceptedLicense = true matchedSource.UserAcceptedLicense = true
} }
fmt.Printf("📥 Importing %s...\n", matchedSource.Name) fmt.Printf("📥 Importing %s...\n", matchedSource.Name)
loader := database.NewDataLoader(db.GetConnection()) loader := database.NewDataLoader(db.GetConnection())
result, err := loader.LoadDataSource(*matchedSource) result, err := loader.LoadDataSource(*matchedSource)
if err != nil { if err != nil {
@ -550,10 +552,10 @@ func cmdReset(db *database.Database, force bool) error {
// askForConfirmation asks the user for yes/no confirmation // askForConfirmation asks the user for yes/no confirmation
func askForConfirmation(question string) bool { func askForConfirmation(question string) bool {
fmt.Printf("%s (y/N): ", question) fmt.Printf("%s (y/N): ", question)
var response string var response string
fmt.Scanln(&response) fmt.Scanln(&response)
response = strings.ToLower(strings.TrimSpace(response)) response = strings.ToLower(strings.TrimSpace(response))
return response == "y" || response == "yes" return response == "y" || response == "yes"
} }
@ -564,30 +566,30 @@ func loadConfig(configPath string) (*Config, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err) return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err)
} }
var config Config var config Config
if err := json.Unmarshal(data, &config); err != nil { if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("failed to parse config file %s: %w", configPath, err) return nil, fmt.Errorf("failed to parse config file %s: %w", configPath, err)
} }
return &config, nil return &config, nil
} }
// initDatabaseFromConfig initializes database using shared configuration // initDatabaseFromConfig initializes database using shared configuration
func initDatabaseFromConfig(config *Config, dbPathOverride string) (*database.Database, error) { func initDatabaseFromConfig(config *Config, dbPathOverride string) (*database.Database, error) {
var dbConfig *database.Config var dbConfig *database.Config
if config.Database != nil { if config.Database != nil {
dbConfig = config.Database dbConfig = config.Database
} else { } else {
dbConfig = database.DefaultConfig() dbConfig = database.DefaultConfig()
} }
// Allow command-line override of database path // Allow command-line override of database path
if dbPathOverride != "" { if dbPathOverride != "" {
dbConfig.Path = dbPathOverride dbConfig.Path = dbPathOverride
} }
// Resolve database path if empty // Resolve database path if empty
if dbConfig.Path == "" { if dbConfig.Path == "" {
resolvedPath, err := database.ResolveDatabasePath(dbConfig.Path) resolvedPath, err := database.ResolveDatabasePath(dbConfig.Path)
@ -596,18 +598,18 @@ func initDatabaseFromConfig(config *Config, dbPathOverride string) (*database.Da
} }
dbConfig.Path = resolvedPath dbConfig.Path = resolvedPath
} }
// Create and initialize database // Create and initialize database
db, err := database.NewDatabase(dbConfig) db, err := database.NewDatabase(dbConfig)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to create database: %w", err) return nil, fmt.Errorf("failed to create database: %w", err)
} }
if err := db.Initialize(); err != nil { if err := db.Initialize(); err != nil {
db.Close() db.Close()
return nil, fmt.Errorf("failed to initialize database: %w", err) return nil, fmt.Errorf("failed to initialize database: %w", err)
} }
return db, nil return db, nil
} }
@ -615,23 +617,23 @@ func initDatabaseFromConfig(config *Config, dbPathOverride string) (*database.Da
func cmdOptimize(db *database.Database, force bool) error { func cmdOptimize(db *database.Database, force bool) error {
fmt.Println("Database Storage Optimization") fmt.Println("Database Storage Optimization")
fmt.Println("============================") fmt.Println("============================")
// We need to get the database path from the config // We need to get the database path from the config
// For now, let's create a simple optimization manager // For now, let's create a simple optimization manager
config := &database.Config{ config := &database.Config{
Path: "./dev-skyview.db", // Default path - this should be configurable Path: "./dev-skyview.db", // Default path - this should be configurable
} }
// Create optimization manager // Create optimization manager
optimizer := database.NewOptimizationManager(db, config) optimizer := database.NewOptimizationManager(db, config)
// Get current stats // Get current stats
fmt.Println("📊 Current Database Statistics:") fmt.Println("📊 Current Database Statistics:")
stats, err := optimizer.GetOptimizationStats() stats, err := optimizer.GetOptimizationStats()
if err != nil { if err != nil {
return fmt.Errorf("failed to get database stats: %w", err) return fmt.Errorf("failed to get database stats: %w", err)
} }
fmt.Printf(" • Size: %.1f MB\n", float64(stats.DatabaseSize)/(1024*1024)) fmt.Printf(" • Size: %.1f MB\n", float64(stats.DatabaseSize)/(1024*1024))
fmt.Printf(" • Page Size: %d bytes\n", stats.PageSize) fmt.Printf(" • Page Size: %d bytes\n", stats.PageSize)
fmt.Printf(" • Total Pages: %d\n", stats.PageCount) fmt.Printf(" • Total Pages: %d\n", stats.PageCount)
@ -639,48 +641,47 @@ func cmdOptimize(db *database.Database, force bool) error {
fmt.Printf(" • Free Pages: %d\n", stats.FreePages) fmt.Printf(" • Free Pages: %d\n", stats.FreePages)
fmt.Printf(" • Efficiency: %.1f%%\n", stats.Efficiency) fmt.Printf(" • Efficiency: %.1f%%\n", stats.Efficiency)
fmt.Printf(" • Auto VACUUM: %v\n", stats.AutoVacuumEnabled) fmt.Printf(" • Auto VACUUM: %v\n", stats.AutoVacuumEnabled)
// Check if optimization is needed // Check if optimization is needed
needsOptimization := stats.FreePages > 0 || stats.Efficiency < 95.0 needsOptimization := stats.FreePages > 0 || stats.Efficiency < 95.0
if !needsOptimization && !force { if !needsOptimization && !force {
fmt.Println("✅ Database is already well optimized!") fmt.Println("✅ Database is already well optimized!")
fmt.Println(" Use --force to run optimization anyway") fmt.Println(" Use --force to run optimization anyway")
return nil return nil
} }
// Perform optimizations // Perform optimizations
if force && !needsOptimization { if force && !needsOptimization {
fmt.Println("\n🔧 Force optimization requested:") fmt.Println("\n🔧 Force optimization requested:")
} else { } else {
fmt.Println("\n🔧 Applying Optimizations:") fmt.Println("\n🔧 Applying Optimizations:")
} }
if err := optimizer.VacuumDatabase(); err != nil { if err := optimizer.VacuumDatabase(); err != nil {
return fmt.Errorf("VACUUM failed: %w", err) return fmt.Errorf("VACUUM failed: %w", err)
} }
if err := optimizer.OptimizeDatabase(); err != nil { if err := optimizer.OptimizeDatabase(); err != nil {
return fmt.Errorf("optimization failed: %w", err) return fmt.Errorf("optimization failed: %w", err)
} }
// Show final stats // Show final stats
fmt.Println("\n📈 Final Statistics:") fmt.Println("\n📈 Final Statistics:")
finalStats, err := optimizer.GetOptimizationStats() finalStats, err := optimizer.GetOptimizationStats()
if err != nil { if err != nil {
return fmt.Errorf("failed to get final stats: %w", err) return fmt.Errorf("failed to get final stats: %w", err)
} }
fmt.Printf(" • Size: %.1f MB\n", float64(finalStats.DatabaseSize)/(1024*1024)) fmt.Printf(" • Size: %.1f MB\n", float64(finalStats.DatabaseSize)/(1024*1024))
fmt.Printf(" • Efficiency: %.1f%%\n", finalStats.Efficiency) fmt.Printf(" • Efficiency: %.1f%%\n", finalStats.Efficiency)
fmt.Printf(" • Free Pages: %d\n", finalStats.FreePages) fmt.Printf(" • Free Pages: %d\n", finalStats.FreePages)
if stats.DatabaseSize > finalStats.DatabaseSize { if stats.DatabaseSize > finalStats.DatabaseSize {
saved := stats.DatabaseSize - finalStats.DatabaseSize saved := stats.DatabaseSize - finalStats.DatabaseSize
fmt.Printf(" • Space Saved: %.1f MB\n", float64(saved)/(1024*1024)) fmt.Printf(" • Space Saved: %.1f MB\n", float64(saved)/(1024*1024))
} }
fmt.Println("\n✅ Database optimization completed!") fmt.Println("\n✅ Database optimization completed!")
return nil return nil
} }

View file

@ -13,12 +13,12 @@ import (
type ExternalAPIClient struct { type ExternalAPIClient struct {
httpClient *http.Client httpClient *http.Client
mutex sync.RWMutex mutex sync.RWMutex
// Configuration // Configuration
timeout time.Duration timeout time.Duration
maxRetries int maxRetries int
userAgent string userAgent string
// Rate limiting // Rate limiting
lastRequest time.Time lastRequest time.Time
minInterval time.Duration minInterval time.Duration
@ -32,28 +32,28 @@ type APIClientConfig struct {
} }
type OpenSkyFlightInfo struct { type OpenSkyFlightInfo struct {
ICAO string `json:"icao"` ICAO string `json:"icao"`
Callsign string `json:"callsign"` Callsign string `json:"callsign"`
Origin string `json:"origin"` Origin string `json:"origin"`
Destination string `json:"destination"` Destination string `json:"destination"`
FirstSeen time.Time `json:"first_seen"` FirstSeen time.Time `json:"first_seen"`
LastSeen time.Time `json:"last_seen"` LastSeen time.Time `json:"last_seen"`
AircraftType string `json:"aircraft_type"` AircraftType string `json:"aircraft_type"`
Registration string `json:"registration"` Registration string `json:"registration"`
FlightNumber string `json:"flight_number"` FlightNumber string `json:"flight_number"`
Airline string `json:"airline"` Airline string `json:"airline"`
} }
type APIError struct { type APIError struct {
Operation string Operation string
StatusCode int StatusCode int
Message string Message string
Retryable bool Retryable bool
RetryAfter time.Duration RetryAfter time.Duration
} }
func (e *APIError) Error() string { func (e *APIError) Error() string {
return fmt.Sprintf("API error in %s: %s (status: %d, retryable: %v)", return fmt.Sprintf("API error in %s: %s (status: %d, retryable: %v)",
e.Operation, e.Message, e.StatusCode, e.Retryable) e.Operation, e.Message, e.StatusCode, e.Retryable)
} }
@ -70,7 +70,7 @@ func NewExternalAPIClient(config APIClientConfig) *ExternalAPIClient {
if config.MinInterval == 0 { if config.MinInterval == 0 {
config.MinInterval = 1 * time.Second // Default rate limit config.MinInterval = 1 * time.Second // Default rate limit
} }
return &ExternalAPIClient{ return &ExternalAPIClient{
httpClient: &http.Client{ httpClient: &http.Client{
Timeout: config.Timeout, Timeout: config.Timeout,
@ -85,7 +85,7 @@ func NewExternalAPIClient(config APIClientConfig) *ExternalAPIClient {
func (c *ExternalAPIClient) enforceRateLimit() { func (c *ExternalAPIClient) enforceRateLimit() {
c.mutex.Lock() c.mutex.Lock()
defer c.mutex.Unlock() defer c.mutex.Unlock()
elapsed := time.Since(c.lastRequest) elapsed := time.Since(c.lastRequest)
if elapsed < c.minInterval { if elapsed < c.minInterval {
time.Sleep(c.minInterval - elapsed) time.Sleep(c.minInterval - elapsed)
@ -95,18 +95,18 @@ func (c *ExternalAPIClient) enforceRateLimit() {
func (c *ExternalAPIClient) makeRequest(ctx context.Context, url string) (*http.Response, error) { func (c *ExternalAPIClient) makeRequest(ctx context.Context, url string) (*http.Response, error) {
c.enforceRateLimit() c.enforceRateLimit()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
req.Header.Set("User-Agent", c.userAgent) req.Header.Set("User-Agent", c.userAgent)
req.Header.Set("Accept", "application/json") req.Header.Set("Accept", "application/json")
var resp *http.Response var resp *http.Response
var lastErr error var lastErr error
for attempt := 0; attempt <= c.maxRetries; attempt++ { for attempt := 0; attempt <= c.maxRetries; attempt++ {
if attempt > 0 { if attempt > 0 {
// Exponential backoff // Exponential backoff
@ -117,16 +117,16 @@ func (c *ExternalAPIClient) makeRequest(ctx context.Context, url string) (*http.
case <-time.After(backoff): case <-time.After(backoff):
} }
} }
resp, lastErr = c.httpClient.Do(req) resp, lastErr = c.httpClient.Do(req)
if lastErr != nil { if lastErr != nil {
continue continue
} }
// Check for retryable status codes // Check for retryable status codes
if resp.StatusCode >= 500 || resp.StatusCode == 429 { if resp.StatusCode >= 500 || resp.StatusCode == 429 {
resp.Body.Close() resp.Body.Close()
// Handle rate limiting // Handle rate limiting
if resp.StatusCode == 429 { if resp.StatusCode == 429 {
retryAfter := parseRetryAfter(resp.Header.Get("Retry-After")) retryAfter := parseRetryAfter(resp.Header.Get("Retry-After"))
@ -140,15 +140,15 @@ func (c *ExternalAPIClient) makeRequest(ctx context.Context, url string) (*http.
} }
continue continue
} }
// Success or non-retryable error // Success or non-retryable error
break break
} }
if lastErr != nil { if lastErr != nil {
return nil, lastErr return nil, lastErr
} }
return resp, nil return resp, nil
} }
@ -156,14 +156,14 @@ func (c *ExternalAPIClient) GetFlightInfoFromOpenSky(ctx context.Context, icao s
if icao == "" { if icao == "" {
return nil, fmt.Errorf("empty ICAO code") return nil, fmt.Errorf("empty ICAO code")
} }
// OpenSky Network API endpoint for flight information // OpenSky Network API endpoint for flight information
apiURL := fmt.Sprintf("https://opensky-network.org/api/flights/aircraft?icao24=%s&begin=%d&end=%d", apiURL := fmt.Sprintf("https://opensky-network.org/api/flights/aircraft?icao24=%s&begin=%d&end=%d",
icao, icao,
time.Now().Add(-24*time.Hour).Unix(), time.Now().Add(-24*time.Hour).Unix(),
time.Now().Unix(), time.Now().Unix(),
) )
resp, err := c.makeRequest(ctx, apiURL) resp, err := c.makeRequest(ctx, apiURL)
if err != nil { if err != nil {
return nil, &APIError{ return nil, &APIError{
@ -173,7 +173,7 @@ func (c *ExternalAPIClient) GetFlightInfoFromOpenSky(ctx context.Context, icao s
} }
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)
return nil, &APIError{ return nil, &APIError{
@ -183,7 +183,7 @@ func (c *ExternalAPIClient) GetFlightInfoFromOpenSky(ctx context.Context, icao s
Retryable: resp.StatusCode >= 500 || resp.StatusCode == 429, Retryable: resp.StatusCode >= 500 || resp.StatusCode == 429,
} }
} }
var flights [][]interface{} var flights [][]interface{}
decoder := json.NewDecoder(resp.Body) decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(&flights); err != nil { if err := decoder.Decode(&flights); err != nil {
@ -193,11 +193,11 @@ func (c *ExternalAPIClient) GetFlightInfoFromOpenSky(ctx context.Context, icao s
Retryable: false, Retryable: false,
} }
} }
if len(flights) == 0 { if len(flights) == 0 {
return nil, nil // No flight information available return nil, nil // No flight information available
} }
// Parse the most recent flight // Parse the most recent flight
flight := flights[0] flight := flights[0]
if len(flight) < 10 { if len(flight) < 10 {
@ -207,11 +207,11 @@ func (c *ExternalAPIClient) GetFlightInfoFromOpenSky(ctx context.Context, icao s
Retryable: false, Retryable: false,
} }
} }
info := &OpenSkyFlightInfo{ info := &OpenSkyFlightInfo{
ICAO: icao, ICAO: icao,
} }
// Parse fields based on OpenSky API documentation // Parse fields based on OpenSky API documentation
if callsign, ok := flight[1].(string); ok { if callsign, ok := flight[1].(string); ok {
info.Callsign = callsign info.Callsign = callsign
@ -228,7 +228,7 @@ func (c *ExternalAPIClient) GetFlightInfoFromOpenSky(ctx context.Context, icao s
if destination, ok := flight[5].(string); ok { if destination, ok := flight[5].(string); ok {
info.Destination = destination info.Destination = destination
} }
return info, nil return info, nil
} }
@ -236,10 +236,10 @@ func (c *ExternalAPIClient) GetAircraftInfoFromOpenSky(ctx context.Context, icao
if icao == "" { if icao == "" {
return nil, fmt.Errorf("empty ICAO code") return nil, fmt.Errorf("empty ICAO code")
} }
// OpenSky Network metadata API // OpenSky Network metadata API
apiURL := fmt.Sprintf("https://opensky-network.org/api/metadata/aircraft/icao/%s", icao) apiURL := fmt.Sprintf("https://opensky-network.org/api/metadata/aircraft/icao/%s", icao)
resp, err := c.makeRequest(ctx, apiURL) resp, err := c.makeRequest(ctx, apiURL)
if err != nil { if err != nil {
return nil, &APIError{ return nil, &APIError{
@ -249,11 +249,11 @@ func (c *ExternalAPIClient) GetAircraftInfoFromOpenSky(ctx context.Context, icao
} }
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound { if resp.StatusCode == http.StatusNotFound {
return nil, nil // Aircraft not found return nil, nil // Aircraft not found
} }
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)
return nil, &APIError{ return nil, &APIError{
@ -263,7 +263,7 @@ func (c *ExternalAPIClient) GetAircraftInfoFromOpenSky(ctx context.Context, icao
Retryable: resp.StatusCode >= 500 || resp.StatusCode == 429, Retryable: resp.StatusCode >= 500 || resp.StatusCode == 429,
} }
} }
var aircraft map[string]interface{} var aircraft map[string]interface{}
decoder := json.NewDecoder(resp.Body) decoder := json.NewDecoder(resp.Body)
if err := decoder.Decode(&aircraft); err != nil { if err := decoder.Decode(&aircraft); err != nil {
@ -273,7 +273,7 @@ func (c *ExternalAPIClient) GetAircraftInfoFromOpenSky(ctx context.Context, icao
Retryable: false, Retryable: false,
} }
} }
return aircraft, nil return aircraft, nil
} }
@ -282,7 +282,7 @@ func (c *ExternalAPIClient) EnhanceCallsignWithExternalData(ctx context.Context,
enhancement["callsign"] = callsign enhancement["callsign"] = callsign
enhancement["icao"] = icao enhancement["icao"] = icao
enhancement["enhanced"] = false enhancement["enhanced"] = false
// Try to get flight information from OpenSky // Try to get flight information from OpenSky
if flightInfo, err := c.GetFlightInfoFromOpenSky(ctx, icao); err == nil && flightInfo != nil { if flightInfo, err := c.GetFlightInfoFromOpenSky(ctx, icao); err == nil && flightInfo != nil {
enhancement["flight_info"] = map[string]interface{}{ enhancement["flight_info"] = map[string]interface{}{
@ -295,53 +295,53 @@ func (c *ExternalAPIClient) EnhanceCallsignWithExternalData(ctx context.Context,
} }
enhancement["enhanced"] = true enhancement["enhanced"] = true
} }
// Try to get aircraft metadata // Try to get aircraft metadata
if aircraftInfo, err := c.GetAircraftInfoFromOpenSky(ctx, icao); err == nil && aircraftInfo != nil { if aircraftInfo, err := c.GetAircraftInfoFromOpenSky(ctx, icao); err == nil && aircraftInfo != nil {
enhancement["aircraft_info"] = aircraftInfo enhancement["aircraft_info"] = aircraftInfo
enhancement["enhanced"] = true enhancement["enhanced"] = true
} }
return enhancement, nil return enhancement, nil
} }
func (c *ExternalAPIClient) BatchEnhanceCallsigns(ctx context.Context, callsigns map[string]string) (map[string]map[string]interface{}, error) { func (c *ExternalAPIClient) BatchEnhanceCallsigns(ctx context.Context, callsigns map[string]string) (map[string]map[string]interface{}, error) {
results := make(map[string]map[string]interface{}) results := make(map[string]map[string]interface{})
for callsign, icao := range callsigns { for callsign, icao := range callsigns {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return results, ctx.Err() return results, ctx.Err()
default: default:
} }
enhanced, err := c.EnhanceCallsignWithExternalData(ctx, callsign, icao) enhanced, err := c.EnhanceCallsignWithExternalData(ctx, callsign, icao)
if err != nil { if err != nil {
// Log error but continue with other callsigns // Log error but continue with other callsigns
fmt.Printf("Warning: failed to enhance callsign %s (ICAO: %s): %v\n", callsign, icao, err) fmt.Printf("Warning: failed to enhance callsign %s (ICAO: %s): %v\n", callsign, icao, err)
continue continue
} }
results[callsign] = enhanced results[callsign] = enhanced
} }
return results, nil return results, nil
} }
func (c *ExternalAPIClient) TestConnection(ctx context.Context) error { func (c *ExternalAPIClient) TestConnection(ctx context.Context) error {
// Test with a simple API call // Test with a simple API call
testURL := "https://opensky-network.org/api/states?time=0&lamin=0&lomin=0&lamax=1&lomax=1" testURL := "https://opensky-network.org/api/states?time=0&lamin=0&lomin=0&lamax=1&lomax=1"
resp, err := c.makeRequest(ctx, testURL) resp, err := c.makeRequest(ctx, testURL)
if err != nil { if err != nil {
return fmt.Errorf("connection test failed: %w", err) return fmt.Errorf("connection test failed: %w", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return fmt.Errorf("connection test returned status %d", resp.StatusCode) return fmt.Errorf("connection test returned status %d", resp.StatusCode)
} }
return nil return nil
} }
@ -349,24 +349,24 @@ func parseRetryAfter(header string) time.Duration {
if header == "" { if header == "" {
return 0 return 0
} }
// Try parsing as seconds // Try parsing as seconds
if seconds, err := time.ParseDuration(header + "s"); err == nil { if seconds, err := time.ParseDuration(header + "s"); err == nil {
return seconds return seconds
} }
// Try parsing as HTTP date // Try parsing as HTTP date
if t, err := http.ParseTime(header); err == nil { if t, err := http.ParseTime(header); err == nil {
return time.Until(t) return time.Until(t)
} }
return 0 return 0
} }
// HealthCheck provides information about the client's health // HealthCheck provides information about the client's health
func (c *ExternalAPIClient) HealthCheck(ctx context.Context) map[string]interface{} { func (c *ExternalAPIClient) HealthCheck(ctx context.Context) map[string]interface{} {
health := make(map[string]interface{}) health := make(map[string]interface{})
// Test connection // Test connection
if err := c.TestConnection(ctx); err != nil { if err := c.TestConnection(ctx); err != nil {
health["status"] = "unhealthy" health["status"] = "unhealthy"
@ -374,16 +374,16 @@ func (c *ExternalAPIClient) HealthCheck(ctx context.Context) map[string]interfac
} else { } else {
health["status"] = "healthy" health["status"] = "healthy"
} }
// Add configuration info // Add configuration info
health["timeout"] = c.timeout.String() health["timeout"] = c.timeout.String()
health["max_retries"] = c.maxRetries health["max_retries"] = c.maxRetries
health["min_interval"] = c.minInterval.String() health["min_interval"] = c.minInterval.String()
health["user_agent"] = c.userAgent health["user_agent"] = c.userAgent
c.mutex.RLock() c.mutex.RLock()
health["last_request"] = c.lastRequest health["last_request"] = c.lastRequest
c.mutex.RUnlock() c.mutex.RUnlock()
return health return health
} }

View file

@ -19,35 +19,35 @@ import (
// Database represents the main database connection and operations // Database represents the main database connection and operations
type Database struct { type Database struct {
conn *sql.DB conn *sql.DB
config *Config config *Config
migrator *Migrator migrator *Migrator
callsign *CallsignManager callsign *CallsignManager
history *HistoryManager history *HistoryManager
} }
// Config holds database configuration options // Config holds database configuration options
type Config struct { type Config struct {
// Database file path (auto-resolved if empty) // Database file path (auto-resolved if empty)
Path string `json:"path"` Path string `json:"path"`
// Data retention settings // Data retention settings
MaxHistoryDays int `json:"max_history_days"` // 0 = unlimited MaxHistoryDays int `json:"max_history_days"` // 0 = unlimited
BackupOnUpgrade bool `json:"backup_on_upgrade"` BackupOnUpgrade bool `json:"backup_on_upgrade"`
// Connection settings // Connection settings
MaxOpenConns int `json:"max_open_conns"` // Default: 10 MaxOpenConns int `json:"max_open_conns"` // Default: 10
MaxIdleConns int `json:"max_idle_conns"` // Default: 5 MaxIdleConns int `json:"max_idle_conns"` // Default: 5
ConnMaxLifetime time.Duration `json:"conn_max_lifetime"` // Default: 1 hour ConnMaxLifetime time.Duration `json:"conn_max_lifetime"` // Default: 1 hour
// Maintenance settings // Maintenance settings
VacuumInterval time.Duration `json:"vacuum_interval"` // Default: 24 hours VacuumInterval time.Duration `json:"vacuum_interval"` // Default: 24 hours
CleanupInterval time.Duration `json:"cleanup_interval"` // Default: 1 hour CleanupInterval time.Duration `json:"cleanup_interval"` // Default: 1 hour
// Compression settings // Compression settings
EnableCompression bool `json:"enable_compression"` // Enable automatic compression EnableCompression bool `json:"enable_compression"` // Enable automatic compression
CompressionLevel int `json:"compression_level"` // Compression level (1-9, default: 6) CompressionLevel int `json:"compression_level"` // Compression level (1-9, default: 6)
PageSize int `json:"page_size"` // SQLite page size (default: 4096) PageSize int `json:"page_size"` // SQLite page size (default: 4096)
} }
// AircraftHistoryRecord represents a stored aircraft position update // AircraftHistoryRecord represents a stored aircraft position update
@ -93,18 +93,18 @@ type AirlineRecord struct {
// AirportRecord represents embedded airport data from OpenFlights // AirportRecord represents embedded airport data from OpenFlights
type AirportRecord struct { type AirportRecord struct {
ID int `json:"id"` ID int `json:"id"`
Name string `json:"name"` Name string `json:"name"`
City string `json:"city"` City string `json:"city"`
Country string `json:"country"` Country string `json:"country"`
IATA string `json:"iata"` IATA string `json:"iata"`
ICAO string `json:"icao"` ICAO string `json:"icao"`
Latitude float64 `json:"latitude"` Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"` Longitude float64 `json:"longitude"`
Altitude int `json:"altitude"` Altitude int `json:"altitude"`
TimezoneOffset float64 `json:"timezone_offset"` TimezoneOffset float64 `json:"timezone_offset"`
DST string `json:"dst"` DST string `json:"dst"`
Timezone string `json:"timezone"` Timezone string `json:"timezone"`
} }
// DatabaseError represents database operation errors // DatabaseError represents database operation errors
@ -131,7 +131,7 @@ func NewDatabase(config *Config) (*Database, error) {
if config == nil { if config == nil {
config = DefaultConfig() config = DefaultConfig()
} }
// Resolve database path // Resolve database path
dbPath, err := ResolveDatabasePath(config.Path) dbPath, err := ResolveDatabasePath(config.Path)
if err != nil { if err != nil {
@ -142,7 +142,7 @@ func NewDatabase(config *Config) (*Database, error) {
} }
} }
config.Path = dbPath config.Path = dbPath
// Open database connection // Open database connection
conn, err := sql.Open("sqlite3", buildConnectionString(dbPath)) conn, err := sql.Open("sqlite3", buildConnectionString(dbPath))
if err != nil { if err != nil {
@ -152,12 +152,12 @@ func NewDatabase(config *Config) (*Database, error) {
Retryable: true, Retryable: true,
} }
} }
// Configure connection pool // Configure connection pool
conn.SetMaxOpenConns(config.MaxOpenConns) conn.SetMaxOpenConns(config.MaxOpenConns)
conn.SetMaxIdleConns(config.MaxIdleConns) conn.SetMaxIdleConns(config.MaxIdleConns)
conn.SetConnMaxLifetime(config.ConnMaxLifetime) conn.SetConnMaxLifetime(config.ConnMaxLifetime)
// Test connection // Test connection
if err := conn.Ping(); err != nil { if err := conn.Ping(); err != nil {
conn.Close() conn.Close()
@ -167,17 +167,17 @@ func NewDatabase(config *Config) (*Database, error) {
Retryable: true, Retryable: true,
} }
} }
db := &Database{ db := &Database{
conn: conn, conn: conn,
config: config, config: config,
} }
// Initialize components // Initialize components
db.migrator = NewMigrator(conn) db.migrator = NewMigrator(conn)
db.callsign = NewCallsignManager(conn) db.callsign = NewCallsignManager(conn)
db.history = NewHistoryManager(conn, config.MaxHistoryDays) db.history = NewHistoryManager(conn, config.MaxHistoryDays)
return db, nil return db, nil
} }
@ -191,7 +191,7 @@ func (db *Database) Initialize() error {
Retryable: false, Retryable: false,
} }
} }
// Load embedded OpenFlights data if not already loaded // Load embedded OpenFlights data if not already loaded
if err := db.callsign.LoadEmbeddedData(); err != nil { if err := db.callsign.LoadEmbeddedData(); err != nil {
return &DatabaseError{ return &DatabaseError{
@ -200,7 +200,7 @@ func (db *Database) Initialize() error {
Retryable: false, Retryable: false,
} }
} }
return nil return nil
} }
@ -240,7 +240,6 @@ func (db *Database) Health() error {
return db.conn.Ping() return db.conn.Ping()
} }
// DefaultConfig returns the default database configuration // DefaultConfig returns the default database configuration
func DefaultConfig() *Config { func DefaultConfig() *Config {
return &Config{ return &Config{
@ -258,4 +257,4 @@ func DefaultConfig() *Config {
// buildConnectionString creates SQLite connection string with optimizations // buildConnectionString creates SQLite connection string with optimizations
func buildConnectionString(path string) string { func buildConnectionString(path string) string {
return fmt.Sprintf("%s?_journal_mode=WAL&_synchronous=NORMAL&_cache_size=-64000&_temp_store=MEMORY&_foreign_keys=ON", path) return fmt.Sprintf("%s?_journal_mode=WAL&_synchronous=NORMAL&_cache_size=-64000&_temp_store=MEMORY&_foreign_keys=ON", path)
} }

View file

@ -164,4 +164,4 @@ func TestDatabasePragmas(t *testing.T) {
if journalMode != "wal" { if journalMode != "wal" {
t.Errorf("Expected WAL journal mode, got: %s", journalMode) t.Errorf("Expected WAL journal mode, got: %s", journalMode)
} }
} }

View file

@ -37,32 +37,32 @@ type DataSource struct {
// LoadResult contains the results of a data loading operation // LoadResult contains the results of a data loading operation
type LoadResult struct { type LoadResult struct {
Source string `json:"source"` Source string `json:"source"`
RecordsTotal int `json:"records_total"` RecordsTotal int `json:"records_total"`
RecordsNew int `json:"records_new"` RecordsNew int `json:"records_new"`
RecordsError int `json:"records_error"` RecordsError int `json:"records_error"`
Duration time.Duration `json:"duration"` Duration time.Duration `json:"duration"`
Errors []string `json:"errors,omitempty"` Errors []string `json:"errors,omitempty"`
} }
// NewDataLoader creates a new data loader with HTTP client // NewDataLoader creates a new data loader with HTTP client
func NewDataLoader(conn *sql.DB) *DataLoader { func NewDataLoader(conn *sql.DB) *DataLoader {
// Check for insecure TLS environment variable // Check for insecure TLS environment variable
insecureTLS := os.Getenv("SKYVIEW_INSECURE_TLS") == "1" insecureTLS := os.Getenv("SKYVIEW_INSECURE_TLS") == "1"
transport := &http.Transport{ transport := &http.Transport{
MaxIdleConns: 10, MaxIdleConns: 10,
IdleConnTimeout: 90 * time.Second, IdleConnTimeout: 90 * time.Second,
DisableCompression: false, DisableCompression: false,
} }
// Allow insecure certificates if requested // Allow insecure certificates if requested
if insecureTLS { if insecureTLS {
transport.TLSClientConfig = &tls.Config{ transport.TLSClientConfig = &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,
} }
} }
return &DataLoader{ return &DataLoader{
conn: conn, conn: conn,
client: &http.Client{ client: &http.Client{
@ -85,7 +85,7 @@ func GetAvailableDataSources() []DataSource {
}, },
{ {
Name: "OpenFlights Airports", Name: "OpenFlights Airports",
License: "AGPL-3.0", License: "AGPL-3.0",
URL: "https://raw.githubusercontent.com/jpatokal/openflights/master/data/airports.dat", URL: "https://raw.githubusercontent.com/jpatokal/openflights/master/data/airports.dat",
RequiresConsent: false, // Runtime data consumption doesn't require explicit consent RequiresConsent: false, // Runtime data consumption doesn't require explicit consent
Format: "openflights", Format: "openflights",
@ -111,23 +111,23 @@ func (dl *DataLoader) LoadDataSource(source DataSource) (*LoadResult, error) {
defer func() { defer func() {
result.Duration = time.Since(startTime) result.Duration = time.Since(startTime)
}() }()
// Check license acceptance if required // Check license acceptance if required
if source.RequiresConsent && !source.UserAcceptedLicense { if source.RequiresConsent && !source.UserAcceptedLicense {
return nil, fmt.Errorf("user has not accepted license for source: %s (%s)", source.Name, source.License) return nil, fmt.Errorf("user has not accepted license for source: %s (%s)", source.Name, source.License)
} }
// Download data // Download data
resp, err := dl.client.Get(source.URL) resp, err := dl.client.Get(source.URL)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to download data from %s: %v", source.URL, err) return nil, fmt.Errorf("failed to download data from %s: %v", source.URL, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("HTTP error downloading data: %s", resp.Status) return nil, fmt.Errorf("HTTP error downloading data: %s", resp.Status)
} }
// Parse and load data based on format // Parse and load data based on format
switch source.Format { switch source.Format {
case "openflights": case "openflights":
@ -137,10 +137,10 @@ func (dl *DataLoader) LoadDataSource(source DataSource) (*LoadResult, error) {
return dl.loadOpenFlightsAirports(resp.Body, source, result) return dl.loadOpenFlightsAirports(resp.Body, source, result)
} }
return nil, fmt.Errorf("unknown OpenFlights data type: %s", source.Name) return nil, fmt.Errorf("unknown OpenFlights data type: %s", source.Name)
case "ourairports": case "ourairports":
return dl.loadOurAirports(resp.Body, source, result) return dl.loadOurAirports(resp.Body, source, result)
default: default:
return nil, fmt.Errorf("unsupported data format: %s", source.Format) return nil, fmt.Errorf("unsupported data format: %s", source.Format)
} }
@ -153,21 +153,21 @@ func (dl *DataLoader) loadOpenFlightsAirlines(reader io.Reader, source DataSourc
return nil, fmt.Errorf("failed to begin transaction: %v", err) return nil, fmt.Errorf("failed to begin transaction: %v", err)
} }
defer tx.Rollback() defer tx.Rollback()
// Record data source // Record data source
if err := dl.recordDataSource(tx, source); err != nil { if err := dl.recordDataSource(tx, source); err != nil {
return nil, err return nil, err
} }
// Clear existing data from this source // Clear existing data from this source
_, err = tx.Exec(`DELETE FROM airlines WHERE data_source = ?`, source.Name) _, err = tx.Exec(`DELETE FROM airlines WHERE data_source = ?`, source.Name)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to clear existing airline data: %v", err) return nil, fmt.Errorf("failed to clear existing airline data: %v", err)
} }
csvReader := csv.NewReader(reader) csvReader := csv.NewReader(reader)
csvReader.FieldsPerRecord = -1 // Variable number of fields csvReader.FieldsPerRecord = -1 // Variable number of fields
insertStmt, err := tx.Prepare(` insertStmt, err := tx.Prepare(`
INSERT OR REPLACE INTO airlines (id, name, alias, iata_code, icao_code, callsign, country, active, data_source) INSERT OR REPLACE INTO airlines (id, name, alias, iata_code, icao_code, callsign, country, active, data_source)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
@ -176,7 +176,7 @@ func (dl *DataLoader) loadOpenFlightsAirlines(reader io.Reader, source DataSourc
return nil, fmt.Errorf("failed to prepare insert statement: %v", err) return nil, fmt.Errorf("failed to prepare insert statement: %v", err)
} }
defer insertStmt.Close() defer insertStmt.Close()
for { for {
record, err := csvReader.Read() record, err := csvReader.Read()
if err == io.EOF { if err == io.EOF {
@ -187,15 +187,15 @@ func (dl *DataLoader) loadOpenFlightsAirlines(reader io.Reader, source DataSourc
result.Errors = append(result.Errors, fmt.Sprintf("CSV parse error: %v", err)) result.Errors = append(result.Errors, fmt.Sprintf("CSV parse error: %v", err))
continue continue
} }
if len(record) < 7 { if len(record) < 7 {
result.RecordsError++ result.RecordsError++
result.Errors = append(result.Errors, "insufficient fields in record") result.Errors = append(result.Errors, "insufficient fields in record")
continue continue
} }
result.RecordsTotal++ result.RecordsTotal++
// Parse OpenFlights airline format: // Parse OpenFlights airline format:
// ID, Name, Alias, IATA, ICAO, Callsign, Country, Active // ID, Name, Alias, IATA, ICAO, Callsign, Country, Active
id, _ := strconv.Atoi(record[0]) id, _ := strconv.Atoi(record[0])
@ -206,29 +206,37 @@ func (dl *DataLoader) loadOpenFlightsAirlines(reader io.Reader, source DataSourc
callsign := strings.Trim(record[5], `"`) callsign := strings.Trim(record[5], `"`)
country := strings.Trim(record[6], `"`) country := strings.Trim(record[6], `"`)
active := len(record) > 7 && strings.Trim(record[7], `"`) == "Y" active := len(record) > 7 && strings.Trim(record[7], `"`) == "Y"
// Convert \N to empty strings // Convert \N to empty strings
if alias == "\\N" { alias = "" } if alias == "\\N" {
if iata == "\\N" { iata = "" } alias = ""
if icao == "\\N" { icao = "" } }
if callsign == "\\N" { callsign = "" } if iata == "\\N" {
iata = ""
}
if icao == "\\N" {
icao = ""
}
if callsign == "\\N" {
callsign = ""
}
_, err = insertStmt.Exec(id, name, alias, iata, icao, callsign, country, active, source.Name) _, err = insertStmt.Exec(id, name, alias, iata, icao, callsign, country, active, source.Name)
if err != nil { if err != nil {
result.RecordsError++ result.RecordsError++
result.Errors = append(result.Errors, fmt.Sprintf("insert error for airline %s: %v", name, err)) result.Errors = append(result.Errors, fmt.Sprintf("insert error for airline %s: %v", name, err))
continue continue
} }
result.RecordsNew++ result.RecordsNew++
} }
// Update record count // Update record count
_, err = tx.Exec(`UPDATE data_sources SET record_count = ? WHERE name = ?`, result.RecordsNew, source.Name) _, err = tx.Exec(`UPDATE data_sources SET record_count = ? WHERE name = ?`, result.RecordsNew, source.Name)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to update record count: %v", err) return nil, fmt.Errorf("failed to update record count: %v", err)
} }
return result, tx.Commit() return result, tx.Commit()
} }
@ -239,21 +247,21 @@ func (dl *DataLoader) loadOpenFlightsAirports(reader io.Reader, source DataSourc
return nil, fmt.Errorf("failed to begin transaction: %v", err) return nil, fmt.Errorf("failed to begin transaction: %v", err)
} }
defer tx.Rollback() defer tx.Rollback()
// Record data source // Record data source
if err := dl.recordDataSource(tx, source); err != nil { if err := dl.recordDataSource(tx, source); err != nil {
return nil, err return nil, err
} }
// Clear existing data from this source // Clear existing data from this source
_, err = tx.Exec(`DELETE FROM airports WHERE data_source = ?`, source.Name) _, err = tx.Exec(`DELETE FROM airports WHERE data_source = ?`, source.Name)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to clear existing airport data: %v", err) return nil, fmt.Errorf("failed to clear existing airport data: %v", err)
} }
csvReader := csv.NewReader(reader) csvReader := csv.NewReader(reader)
csvReader.FieldsPerRecord = -1 csvReader.FieldsPerRecord = -1
insertStmt, err := tx.Prepare(` insertStmt, err := tx.Prepare(`
INSERT OR REPLACE INTO airports (id, name, city, country, iata_code, icao_code, latitude, longitude, INSERT OR REPLACE INTO airports (id, name, city, country, iata_code, icao_code, latitude, longitude,
elevation_ft, timezone_offset, dst_type, timezone, data_source) elevation_ft, timezone_offset, dst_type, timezone, data_source)
@ -263,7 +271,7 @@ func (dl *DataLoader) loadOpenFlightsAirports(reader io.Reader, source DataSourc
return nil, fmt.Errorf("failed to prepare insert statement: %v", err) return nil, fmt.Errorf("failed to prepare insert statement: %v", err)
} }
defer insertStmt.Close() defer insertStmt.Close()
for { for {
record, err := csvReader.Read() record, err := csvReader.Read()
if err == io.EOF { if err == io.EOF {
@ -274,15 +282,15 @@ func (dl *DataLoader) loadOpenFlightsAirports(reader io.Reader, source DataSourc
result.Errors = append(result.Errors, fmt.Sprintf("CSV parse error: %v", err)) result.Errors = append(result.Errors, fmt.Sprintf("CSV parse error: %v", err))
continue continue
} }
if len(record) < 12 { if len(record) < 12 {
result.RecordsError++ result.RecordsError++
result.Errors = append(result.Errors, "insufficient fields in airport record") result.Errors = append(result.Errors, "insufficient fields in airport record")
continue continue
} }
result.RecordsTotal++ result.RecordsTotal++
// Parse OpenFlights airport format // Parse OpenFlights airport format
id, _ := strconv.Atoi(record[0]) id, _ := strconv.Atoi(record[0])
name := strings.Trim(record[1], `"`) name := strings.Trim(record[1], `"`)
@ -296,29 +304,37 @@ func (dl *DataLoader) loadOpenFlightsAirports(reader io.Reader, source DataSourc
tzOffset, _ := strconv.ParseFloat(record[9], 64) tzOffset, _ := strconv.ParseFloat(record[9], 64)
dst := strings.Trim(record[10], `"`) dst := strings.Trim(record[10], `"`)
timezone := strings.Trim(record[11], `"`) timezone := strings.Trim(record[11], `"`)
// Convert \N to empty strings // Convert \N to empty strings
if iata == "\\N" { iata = "" } if iata == "\\N" {
if icao == "\\N" { icao = "" } iata = ""
if dst == "\\N" { dst = "" } }
if timezone == "\\N" { timezone = "" } if icao == "\\N" {
icao = ""
}
if dst == "\\N" {
dst = ""
}
if timezone == "\\N" {
timezone = ""
}
_, err = insertStmt.Exec(id, name, city, country, iata, icao, lat, lon, alt, tzOffset, dst, timezone, source.Name) _, err = insertStmt.Exec(id, name, city, country, iata, icao, lat, lon, alt, tzOffset, dst, timezone, source.Name)
if err != nil { if err != nil {
result.RecordsError++ result.RecordsError++
result.Errors = append(result.Errors, fmt.Sprintf("insert error for airport %s: %v", name, err)) result.Errors = append(result.Errors, fmt.Sprintf("insert error for airport %s: %v", name, err))
continue continue
} }
result.RecordsNew++ result.RecordsNew++
} }
// Update record count // Update record count
_, err = tx.Exec(`UPDATE data_sources SET record_count = ? WHERE name = ?`, result.RecordsNew, source.Name) _, err = tx.Exec(`UPDATE data_sources SET record_count = ? WHERE name = ?`, result.RecordsNew, source.Name)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to update record count: %v", err) return nil, fmt.Errorf("failed to update record count: %v", err)
} }
return result, tx.Commit() return result, tx.Commit()
} }
@ -330,9 +346,9 @@ func (dl *DataLoader) loadOurAirports(reader io.Reader, source DataSource, resul
return nil, fmt.Errorf("failed to begin transaction: %v", err) return nil, fmt.Errorf("failed to begin transaction: %v", err)
} }
defer tx.Rollback() defer tx.Rollback()
csvReader := csv.NewReader(reader) csvReader := csv.NewReader(reader)
// Read header row // Read header row
headers, err := csvReader.Read() headers, err := csvReader.Read()
if err != nil { if err != nil {
@ -340,13 +356,13 @@ func (dl *DataLoader) loadOurAirports(reader io.Reader, source DataSource, resul
result.Errors = []string{fmt.Sprintf("Failed to read CSV header: %v", err)} result.Errors = []string{fmt.Sprintf("Failed to read CSV header: %v", err)}
return result, err return result, err
} }
// Create header index map for easier field access // Create header index map for easier field access
headerIndex := make(map[string]int) headerIndex := make(map[string]int)
for i, header := range headers { for i, header := range headers {
headerIndex[strings.TrimSpace(header)] = i headerIndex[strings.TrimSpace(header)] = i
} }
// Prepare statement for airports // Prepare statement for airports
stmt, err := tx.Prepare(` stmt, err := tx.Prepare(`
INSERT OR REPLACE INTO airports ( INSERT OR REPLACE INTO airports (
@ -362,7 +378,7 @@ func (dl *DataLoader) loadOurAirports(reader io.Reader, source DataSource, resul
return result, err return result, err
} }
defer stmt.Close() defer stmt.Close()
// Process each row // Process each row
for { for {
record, err := csvReader.Read() record, err := csvReader.Read()
@ -374,13 +390,13 @@ func (dl *DataLoader) loadOurAirports(reader io.Reader, source DataSource, resul
result.Errors = append(result.Errors, fmt.Sprintf("CSV read error: %v", err)) result.Errors = append(result.Errors, fmt.Sprintf("CSV read error: %v", err))
continue continue
} }
// Skip rows with insufficient fields // Skip rows with insufficient fields
if len(record) < len(headerIndex) { if len(record) < len(headerIndex) {
result.RecordsError++ result.RecordsError++
continue continue
} }
// Extract fields using header index // Extract fields using header index
sourceID := getFieldByHeader(record, headerIndex, "id") sourceID := getFieldByHeader(record, headerIndex, "id")
ident := getFieldByHeader(record, headerIndex, "ident") ident := getFieldByHeader(record, headerIndex, "ident")
@ -394,7 +410,7 @@ func (dl *DataLoader) loadOurAirports(reader io.Reader, source DataSource, resul
homeLink := getFieldByHeader(record, headerIndex, "home_link") homeLink := getFieldByHeader(record, headerIndex, "home_link")
wikipediaLink := getFieldByHeader(record, headerIndex, "wikipedia_link") wikipediaLink := getFieldByHeader(record, headerIndex, "wikipedia_link")
keywords := getFieldByHeader(record, headerIndex, "keywords") keywords := getFieldByHeader(record, headerIndex, "keywords")
// Parse coordinates // Parse coordinates
var latitude, longitude float64 var latitude, longitude float64
if latStr := getFieldByHeader(record, headerIndex, "latitude_deg"); latStr != "" { if latStr := getFieldByHeader(record, headerIndex, "latitude_deg"); latStr != "" {
@ -407,7 +423,7 @@ func (dl *DataLoader) loadOurAirports(reader io.Reader, source DataSource, resul
longitude = lng longitude = lng
} }
} }
// Parse elevation // Parse elevation
var elevation int var elevation int
if elevStr := getFieldByHeader(record, headerIndex, "elevation_ft"); elevStr != "" { if elevStr := getFieldByHeader(record, headerIndex, "elevation_ft"); elevStr != "" {
@ -415,10 +431,10 @@ func (dl *DataLoader) loadOurAirports(reader io.Reader, source DataSource, resul
elevation = elev elevation = elev
} }
} }
// Parse scheduled service // Parse scheduled service
scheduledService := getFieldByHeader(record, headerIndex, "scheduled_service") == "yes" scheduledService := getFieldByHeader(record, headerIndex, "scheduled_service") == "yes"
// Insert airport record // Insert airport record
_, err = stmt.Exec( _, err = stmt.Exec(
sourceID, name, ident, airportType, icaoCode, iataCode, sourceID, name, ident, airportType, icaoCode, iataCode,
@ -432,7 +448,7 @@ func (dl *DataLoader) loadOurAirports(reader io.Reader, source DataSource, resul
result.RecordsNew++ result.RecordsNew++
} }
} }
// Update data source tracking // Update data source tracking
_, err = tx.Exec(` _, err = tx.Exec(`
INSERT OR REPLACE INTO data_sources (name, license, url, imported_at, record_count, user_accepted_license) INSERT OR REPLACE INTO data_sources (name, license, url, imported_at, record_count, user_accepted_license)
@ -441,7 +457,7 @@ func (dl *DataLoader) loadOurAirports(reader io.Reader, source DataSource, resul
if err != nil { if err != nil {
return result, fmt.Errorf("failed to update data source tracking: %v", err) return result, fmt.Errorf("failed to update data source tracking: %v", err)
} }
return result, tx.Commit() return result, tx.Commit()
} }
@ -460,13 +476,13 @@ func (dl *DataLoader) GetLoadedDataSources() ([]DataSource, error) {
FROM data_sources FROM data_sources
ORDER BY name ORDER BY name
` `
rows, err := dl.conn.Query(query) rows, err := dl.conn.Query(query)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var sources []DataSource var sources []DataSource
for rows.Next() { for rows.Next() {
var source DataSource var source DataSource
@ -482,7 +498,7 @@ func (dl *DataLoader) GetLoadedDataSources() ([]DataSource, error) {
} }
sources = append(sources, source) sources = append(sources, source)
} }
return sources, rows.Err() return sources, rows.Err()
} }
@ -493,11 +509,10 @@ func (dl *DataLoader) recordDataSource(tx *sql.Tx, source DataSource) error {
(name, license, url, version, user_accepted_license) (name, license, url, version, user_accepted_license)
VALUES (?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?)
`, source.Name, source.License, source.URL, source.Version, source.UserAcceptedLicense) `, source.Name, source.License, source.URL, source.Version, source.UserAcceptedLicense)
return err return err
} }
// ClearDataSource removes all data from a specific source // ClearDataSource removes all data from a specific source
func (dl *DataLoader) ClearDataSource(sourceName string) error { func (dl *DataLoader) ClearDataSource(sourceName string) error {
tx, err := dl.conn.Begin() tx, err := dl.conn.Begin()
@ -505,22 +520,22 @@ func (dl *DataLoader) ClearDataSource(sourceName string) error {
return fmt.Errorf("failed to begin transaction: %v", err) return fmt.Errorf("failed to begin transaction: %v", err)
} }
defer tx.Rollback() defer tx.Rollback()
// Clear from all tables // Clear from all tables
_, err = tx.Exec(`DELETE FROM airlines WHERE data_source = ?`, sourceName) _, err = tx.Exec(`DELETE FROM airlines WHERE data_source = ?`, sourceName)
if err != nil { if err != nil {
return fmt.Errorf("failed to clear airlines: %v", err) return fmt.Errorf("failed to clear airlines: %v", err)
} }
_, err = tx.Exec(`DELETE FROM airports WHERE data_source = ?`, sourceName) _, err = tx.Exec(`DELETE FROM airports WHERE data_source = ?`, sourceName)
if err != nil { if err != nil {
return fmt.Errorf("failed to clear airports: %v", err) return fmt.Errorf("failed to clear airports: %v", err)
} }
_, err = tx.Exec(`DELETE FROM data_sources WHERE name = ?`, sourceName) _, err = tx.Exec(`DELETE FROM data_sources WHERE name = ?`, sourceName)
if err != nil { if err != nil {
return fmt.Errorf("failed to clear data source record: %v", err) return fmt.Errorf("failed to clear data source record: %v", err)
} }
return tx.Commit() return tx.Commit()
} }

View file

@ -33,9 +33,9 @@ func TestDataLoader_LoadOpenFlightsAirlines(t *testing.T) {
result, err := loader.LoadDataSource(source) result, err := loader.LoadDataSource(source)
if err != nil { if err != nil {
// Network issues in tests are acceptable // Network issues in tests are acceptable
if strings.Contains(err.Error(), "connection") || if strings.Contains(err.Error(), "connection") ||
strings.Contains(err.Error(), "timeout") || strings.Contains(err.Error(), "timeout") ||
strings.Contains(err.Error(), "no such host") { strings.Contains(err.Error(), "no such host") {
t.Skipf("Skipping network test due to connectivity issue: %v", err) t.Skipf("Skipping network test due to connectivity issue: %v", err)
} }
t.Fatal("LoadDataSource failed:", err) t.Fatal("LoadDataSource failed:", err)
@ -45,7 +45,7 @@ func TestDataLoader_LoadOpenFlightsAirlines(t *testing.T) {
t.Fatal("Expected load result, got nil") t.Fatal("Expected load result, got nil")
} }
t.Logf("Loaded airlines: Total=%d, New=%d, Errors=%d, Duration=%v", t.Logf("Loaded airlines: Total=%d, New=%d, Errors=%d, Duration=%v",
result.RecordsTotal, result.RecordsNew, result.RecordsError, result.Duration) result.RecordsTotal, result.RecordsNew, result.RecordsError, result.Duration)
// Verify some data was processed // Verify some data was processed
@ -72,16 +72,16 @@ func TestDataLoader_LoadOurAirports(t *testing.T) {
result, err := loader.LoadDataSource(source) result, err := loader.LoadDataSource(source)
if err != nil { if err != nil {
// Network issues in tests are acceptable // Network issues in tests are acceptable
if strings.Contains(err.Error(), "connection") || if strings.Contains(err.Error(), "connection") ||
strings.Contains(err.Error(), "timeout") || strings.Contains(err.Error(), "timeout") ||
strings.Contains(err.Error(), "no such host") { strings.Contains(err.Error(), "no such host") {
t.Skipf("Skipping network test due to connectivity issue: %v", err) t.Skipf("Skipping network test due to connectivity issue: %v", err)
} }
t.Fatal("LoadDataSource failed:", err) t.Fatal("LoadDataSource failed:", err)
} }
if result != nil { if result != nil {
t.Logf("Loaded airports: Total=%d, New=%d, Errors=%d, Duration=%v", t.Logf("Loaded airports: Total=%d, New=%d, Errors=%d, Duration=%v",
result.RecordsTotal, result.RecordsNew, result.RecordsError, result.Duration) result.RecordsTotal, result.RecordsNew, result.RecordsError, result.Duration)
} }
} }
@ -174,4 +174,4 @@ func TestLoadResult_Struct(t *testing.T) {
if len(result.Errors) != 2 { if len(result.Errors) != 2 {
t.Error("Errors field not preserved") t.Error("Errors field not preserved")
} }
} }

View file

@ -12,7 +12,7 @@ import (
type CallsignManager struct { type CallsignManager struct {
db *sql.DB db *sql.DB
mutex sync.RWMutex mutex sync.RWMutex
// Compiled regex patterns for callsign parsing // Compiled regex patterns for callsign parsing
airlinePattern *regexp.Regexp airlinePattern *regexp.Regexp
flightPattern *regexp.Regexp flightPattern *regexp.Regexp
@ -42,14 +42,14 @@ func (cm *CallsignManager) ParseCallsign(callsign string) *CallsignParseResult {
ParsedTime: time.Now(), ParsedTime: time.Now(),
IsValid: false, IsValid: false,
} }
if callsign == "" { if callsign == "" {
return result return result
} }
// Clean and normalize the callsign // Clean and normalize the callsign
normalized := strings.TrimSpace(strings.ToUpper(callsign)) normalized := strings.TrimSpace(strings.ToUpper(callsign))
// Try airline pattern first (most common for commercial flights) // Try airline pattern first (most common for commercial flights)
if matches := cm.airlinePattern.FindStringSubmatch(normalized); len(matches) == 3 { if matches := cm.airlinePattern.FindStringSubmatch(normalized); len(matches) == 3 {
result.AirlineCode = matches[1] result.AirlineCode = matches[1]
@ -57,7 +57,7 @@ func (cm *CallsignManager) ParseCallsign(callsign string) *CallsignParseResult {
result.IsValid = true result.IsValid = true
return result return result
} }
// Fall back to general flight pattern // Fall back to general flight pattern
if matches := cm.flightPattern.FindStringSubmatch(normalized); len(matches) == 3 { if matches := cm.flightPattern.FindStringSubmatch(normalized); len(matches) == 3 {
result.AirlineCode = matches[1] result.AirlineCode = matches[1]
@ -65,24 +65,24 @@ func (cm *CallsignManager) ParseCallsign(callsign string) *CallsignParseResult {
result.IsValid = true result.IsValid = true
return result return result
} }
return result return result
} }
func (cm *CallsignManager) GetCallsignInfo(callsign string) (*CallsignInfo, error) { func (cm *CallsignManager) GetCallsignInfo(callsign string) (*CallsignInfo, error) {
cm.mutex.RLock() cm.mutex.RLock()
defer cm.mutex.RUnlock() defer cm.mutex.RUnlock()
if callsign == "" { if callsign == "" {
return nil, fmt.Errorf("empty callsign") return nil, fmt.Errorf("empty callsign")
} }
// First check the cache // First check the cache
cached, err := cm.getCallsignFromCache(callsign) cached, err := cm.getCallsignFromCache(callsign)
if err == nil && cached != nil { if err == nil && cached != nil {
return cached, nil return cached, nil
} }
// Parse the callsign // Parse the callsign
parsed := cm.ParseCallsign(callsign) parsed := cm.ParseCallsign(callsign)
if !parsed.IsValid { if !parsed.IsValid {
@ -91,13 +91,13 @@ func (cm *CallsignManager) GetCallsignInfo(callsign string) (*CallsignInfo, erro
IsValid: false, IsValid: false,
}, nil }, nil
} }
// Look up airline information // Look up airline information
airline, err := cm.getAirlineByCode(parsed.AirlineCode) airline, err := cm.getAirlineByCode(parsed.AirlineCode)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
return nil, fmt.Errorf("failed to lookup airline %s: %w", parsed.AirlineCode, err) return nil, fmt.Errorf("failed to lookup airline %s: %w", parsed.AirlineCode, err)
} }
// Build the result // Build the result
info := &CallsignInfo{ info := &CallsignInfo{
OriginalCallsign: callsign, OriginalCallsign: callsign,
@ -106,7 +106,7 @@ func (cm *CallsignManager) GetCallsignInfo(callsign string) (*CallsignInfo, erro
IsValid: true, IsValid: true,
LastUpdated: time.Now(), LastUpdated: time.Now(),
} }
if airline != nil { if airline != nil {
info.AirlineName = airline.Name info.AirlineName = airline.Name
info.AirlineCountry = airline.Country info.AirlineCountry = airline.Country
@ -114,7 +114,7 @@ func (cm *CallsignManager) GetCallsignInfo(callsign string) (*CallsignInfo, erro
} else { } else {
info.DisplayName = fmt.Sprintf("%s %s", parsed.AirlineCode, parsed.FlightNumber) info.DisplayName = fmt.Sprintf("%s %s", parsed.AirlineCode, parsed.FlightNumber)
} }
// Cache the result (fire and forget) // Cache the result (fire and forget)
go func() { go func() {
if err := cm.cacheCallsignInfo(info); err != nil { if err := cm.cacheCallsignInfo(info); err != nil {
@ -122,7 +122,7 @@ func (cm *CallsignManager) GetCallsignInfo(callsign string) (*CallsignInfo, erro
fmt.Printf("Warning: failed to cache callsign info for %s: %v\n", callsign, err) fmt.Printf("Warning: failed to cache callsign info for %s: %v\n", callsign, err)
} }
}() }()
return info, nil return info, nil
} }
@ -133,10 +133,10 @@ func (cm *CallsignManager) getCallsignFromCache(callsign string) (*CallsignInfo,
FROM callsign_cache FROM callsign_cache
WHERE callsign = ? AND expires_at > datetime('now') WHERE callsign = ? AND expires_at > datetime('now')
` `
var info CallsignInfo var info CallsignInfo
var cacheExpires time.Time var cacheExpires time.Time
err := cm.db.QueryRow(query, callsign).Scan( err := cm.db.QueryRow(query, callsign).Scan(
&info.OriginalCallsign, &info.OriginalCallsign,
&info.AirlineCode, &info.AirlineCode,
@ -148,25 +148,25 @@ func (cm *CallsignManager) getCallsignFromCache(callsign string) (*CallsignInfo,
&info.LastUpdated, &info.LastUpdated,
&cacheExpires, &cacheExpires,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &info, nil return &info, nil
} }
func (cm *CallsignManager) cacheCallsignInfo(info *CallsignInfo) error { func (cm *CallsignManager) cacheCallsignInfo(info *CallsignInfo) error {
// Cache for 24 hours by default // Cache for 24 hours by default
cacheExpires := time.Now().Add(24 * time.Hour) cacheExpires := time.Now().Add(24 * time.Hour)
query := ` query := `
INSERT OR REPLACE INTO callsign_cache INSERT OR REPLACE INTO callsign_cache
(callsign, airline_icao, flight_number, airline_name, (callsign, airline_icao, flight_number, airline_name,
airline_country, cached_at, expires_at) airline_country, cached_at, expires_at)
VALUES (?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?)
` `
_, err := cm.db.Exec(query, _, err := cm.db.Exec(query,
info.OriginalCallsign, info.OriginalCallsign,
info.AirlineCode, info.AirlineCode,
@ -176,7 +176,7 @@ func (cm *CallsignManager) cacheCallsignInfo(info *CallsignInfo) error {
info.LastUpdated, info.LastUpdated,
cacheExpires, cacheExpires,
) )
return err return err
} }
@ -190,7 +190,7 @@ func (cm *CallsignManager) getAirlineByCode(code string) (*AirlineRecord, error)
name name
LIMIT 1 LIMIT 1
` `
var airline AirlineRecord var airline AirlineRecord
err := cm.db.QueryRow(query, code, code, code).Scan( err := cm.db.QueryRow(query, code, code, code).Scan(
&airline.ICAOCode, &airline.ICAOCode,
@ -199,31 +199,31 @@ func (cm *CallsignManager) getAirlineByCode(code string) (*AirlineRecord, error)
&airline.Country, &airline.Country,
&airline.Active, &airline.Active,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &airline, nil return &airline, nil
} }
func (cm *CallsignManager) GetAirlinesByCountry(country string) ([]AirlineRecord, error) { func (cm *CallsignManager) GetAirlinesByCountry(country string) ([]AirlineRecord, error) {
cm.mutex.RLock() cm.mutex.RLock()
defer cm.mutex.RUnlock() defer cm.mutex.RUnlock()
query := ` query := `
SELECT icao_code, iata_code, name, country, active SELECT icao_code, iata_code, name, country, active
FROM airlines FROM airlines
WHERE country = ? AND active = 1 WHERE country = ? AND active = 1
ORDER BY name ORDER BY name
` `
rows, err := cm.db.Query(query, country) rows, err := cm.db.Query(query, country)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var airlines []AirlineRecord var airlines []AirlineRecord
for rows.Next() { for rows.Next() {
var airline AirlineRecord var airline AirlineRecord
@ -239,14 +239,14 @@ func (cm *CallsignManager) GetAirlinesByCountry(country string) ([]AirlineRecord
} }
airlines = append(airlines, airline) airlines = append(airlines, airline)
} }
return airlines, rows.Err() return airlines, rows.Err()
} }
func (cm *CallsignManager) SearchAirlines(query string) ([]AirlineRecord, error) { func (cm *CallsignManager) SearchAirlines(query string) ([]AirlineRecord, error) {
cm.mutex.RLock() cm.mutex.RLock()
defer cm.mutex.RUnlock() defer cm.mutex.RUnlock()
searchQuery := ` searchQuery := `
SELECT icao_code, iata_code, name, country, active SELECT icao_code, iata_code, name, country, active
FROM airlines FROM airlines
@ -265,11 +265,11 @@ func (cm *CallsignManager) SearchAirlines(query string) ([]AirlineRecord, error)
name name
LIMIT 50 LIMIT 50
` `
searchTerm := "%" + strings.ToUpper(query) + "%" searchTerm := "%" + strings.ToUpper(query) + "%"
exactTerm := strings.ToUpper(query) exactTerm := strings.ToUpper(query)
rows, err := cm.db.Query(searchQuery, rows, err := cm.db.Query(searchQuery,
searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm, searchTerm,
exactTerm, exactTerm, exactTerm, exactTerm, exactTerm, exactTerm,
) )
@ -277,7 +277,7 @@ func (cm *CallsignManager) SearchAirlines(query string) ([]AirlineRecord, error)
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var airlines []AirlineRecord var airlines []AirlineRecord
for rows.Next() { for rows.Next() {
var airline AirlineRecord var airline AirlineRecord
@ -293,14 +293,14 @@ func (cm *CallsignManager) SearchAirlines(query string) ([]AirlineRecord, error)
} }
airlines = append(airlines, airline) airlines = append(airlines, airline)
} }
return airlines, rows.Err() return airlines, rows.Err()
} }
func (cm *CallsignManager) ClearExpiredCache() error { func (cm *CallsignManager) ClearExpiredCache() error {
cm.mutex.Lock() cm.mutex.Lock()
defer cm.mutex.Unlock() defer cm.mutex.Unlock()
query := `DELETE FROM callsign_cache WHERE expires_at <= datetime('now')` query := `DELETE FROM callsign_cache WHERE expires_at <= datetime('now')`
_, err := cm.db.Exec(query) _, err := cm.db.Exec(query)
return err return err
@ -309,9 +309,9 @@ func (cm *CallsignManager) ClearExpiredCache() error {
func (cm *CallsignManager) GetCacheStats() (map[string]interface{}, error) { func (cm *CallsignManager) GetCacheStats() (map[string]interface{}, error) {
cm.mutex.RLock() cm.mutex.RLock()
defer cm.mutex.RUnlock() defer cm.mutex.RUnlock()
stats := make(map[string]interface{}) stats := make(map[string]interface{})
// Total cached entries // Total cached entries
var totalCached int var totalCached int
err := cm.db.QueryRow(`SELECT COUNT(*) FROM callsign_cache`).Scan(&totalCached) err := cm.db.QueryRow(`SELECT COUNT(*) FROM callsign_cache`).Scan(&totalCached)
@ -319,7 +319,7 @@ func (cm *CallsignManager) GetCacheStats() (map[string]interface{}, error) {
return nil, err return nil, err
} }
stats["total_cached"] = totalCached stats["total_cached"] = totalCached
// Valid (non-expired) entries // Valid (non-expired) entries
var validCached int var validCached int
err = cm.db.QueryRow(`SELECT COUNT(*) FROM callsign_cache WHERE expires_at > datetime('now')`).Scan(&validCached) err = cm.db.QueryRow(`SELECT COUNT(*) FROM callsign_cache WHERE expires_at > datetime('now')`).Scan(&validCached)
@ -327,10 +327,10 @@ func (cm *CallsignManager) GetCacheStats() (map[string]interface{}, error) {
return nil, err return nil, err
} }
stats["valid_cached"] = validCached stats["valid_cached"] = validCached
// Expired entries // Expired entries
stats["expired_cached"] = totalCached - validCached stats["expired_cached"] = totalCached - validCached
// Total airlines in database // Total airlines in database
var totalAirlines int var totalAirlines int
err = cm.db.QueryRow(`SELECT COUNT(*) FROM airlines WHERE active = 1`).Scan(&totalAirlines) err = cm.db.QueryRow(`SELECT COUNT(*) FROM airlines WHERE active = 1`).Scan(&totalAirlines)
@ -338,7 +338,7 @@ func (cm *CallsignManager) GetCacheStats() (map[string]interface{}, error) {
return nil, err return nil, err
} }
stats["total_airlines"] = totalAirlines stats["total_airlines"] = totalAirlines
return stats, nil return stats, nil
} }
@ -349,14 +349,14 @@ func (cm *CallsignManager) LoadEmbeddedData() error {
if err != nil { if err != nil {
return err return err
} }
if count > 0 { if count > 0 {
// Data already loaded // Data already loaded
return nil return nil
} }
// For now, we'll implement this as a placeholder // For now, we'll implement this as a placeholder
// In a full implementation, this would load embedded airline data // In a full implementation, this would load embedded airline data
// from embedded files or resources // from embedded files or resources
return nil return nil
} }

View file

@ -21,15 +21,15 @@ func TestCallsignManager_ParseCallsign(t *testing.T) {
manager := NewCallsignManager(db.GetConnection()) manager := NewCallsignManager(db.GetConnection())
testCases := []struct { testCases := []struct {
callsign string callsign string
expectedValid bool expectedValid bool
expectedAirline string expectedAirline string
expectedFlight string expectedFlight string
}{ }{
{"UAL123", true, "UAL", "123"}, {"UAL123", true, "UAL", "123"},
{"BA4567", true, "BA", "4567"}, {"BA4567", true, "BA", "4567"},
{"AFR89", true, "AFR", "89"}, {"AFR89", true, "AFR", "89"},
{"N123AB", false, "", ""}, // Aircraft registration, not callsign {"N123AB", false, "", ""}, // Aircraft registration, not callsign
{"INVALID", false, "", ""}, // No numbers {"INVALID", false, "", ""}, // No numbers
{"123", false, "", ""}, // Only numbers {"123", false, "", ""}, // Only numbers
{"A", false, "", ""}, // Too short {"A", false, "", ""}, // Too short
@ -39,15 +39,15 @@ func TestCallsignManager_ParseCallsign(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
result := manager.ParseCallsign(tc.callsign) result := manager.ParseCallsign(tc.callsign)
if result.IsValid != tc.expectedValid { if result.IsValid != tc.expectedValid {
t.Errorf("ParseCallsign(%s): expected valid=%v, got %v", t.Errorf("ParseCallsign(%s): expected valid=%v, got %v",
tc.callsign, tc.expectedValid, result.IsValid) tc.callsign, tc.expectedValid, result.IsValid)
} }
if result.IsValid && result.AirlineCode != tc.expectedAirline { if result.IsValid && result.AirlineCode != tc.expectedAirline {
t.Errorf("ParseCallsign(%s): expected airline=%s, got %s", t.Errorf("ParseCallsign(%s): expected airline=%s, got %s",
tc.callsign, tc.expectedAirline, result.AirlineCode) tc.callsign, tc.expectedAirline, result.AirlineCode)
} }
if result.IsValid && result.FlightNumber != tc.expectedFlight { if result.IsValid && result.FlightNumber != tc.expectedFlight {
t.Errorf("ParseCallsign(%s): expected flight=%s, got %s", t.Errorf("ParseCallsign(%s): expected flight=%s, got %s",
tc.callsign, tc.expectedFlight, result.FlightNumber) tc.callsign, tc.expectedFlight, result.FlightNumber)
} }
} }
@ -101,7 +101,7 @@ func TestCallsignManager_GetCallsignInfo_InvalidCallsign(t *testing.T) {
defer cleanup() defer cleanup()
manager := NewCallsignManager(db.GetConnection()) manager := NewCallsignManager(db.GetConnection())
// Test with invalid callsign format // Test with invalid callsign format
info, err := manager.GetCallsignInfo("INVALID") info, err := manager.GetCallsignInfo("INVALID")
if err != nil { if err != nil {
@ -129,7 +129,7 @@ func TestCallsignManager_GetCallsignInfo_EmptyCallsign(t *testing.T) {
defer cleanup() defer cleanup()
manager := NewCallsignManager(db.GetConnection()) manager := NewCallsignManager(db.GetConnection())
// Test with empty callsign // Test with empty callsign
info, err := manager.GetCallsignInfo("") info, err := manager.GetCallsignInfo("")
if err == nil { if err == nil {
@ -162,7 +162,7 @@ func TestCallsignManager_GetCacheStats(t *testing.T) {
if err != nil { if err != nil {
t.Error("GetCacheStats should not error:", err) t.Error("GetCacheStats should not error:", err)
} }
if stats == nil { if stats == nil {
t.Error("Expected cache stats, got nil") t.Error("Expected cache stats, got nil")
} }
@ -265,4 +265,4 @@ func TestCallsignParseResult_Struct(t *testing.T) {
if !result.IsValid { if !result.IsValid {
t.Error("IsValid field not preserved") t.Error("IsValid field not preserved")
} }
} }

View file

@ -10,7 +10,7 @@ import (
type HistoryManager struct { type HistoryManager struct {
db *sql.DB db *sql.DB
mutex sync.RWMutex mutex sync.RWMutex
// Configuration // Configuration
maxHistoryDays int maxHistoryDays int
cleanupTicker *time.Ticker cleanupTicker *time.Ticker
@ -23,11 +23,11 @@ func NewHistoryManager(db *sql.DB, maxHistoryDays int) *HistoryManager {
maxHistoryDays: maxHistoryDays, maxHistoryDays: maxHistoryDays,
stopCleanup: make(chan bool), stopCleanup: make(chan bool),
} }
// Start periodic cleanup (every hour) // Start periodic cleanup (every hour)
hm.cleanupTicker = time.NewTicker(1 * time.Hour) hm.cleanupTicker = time.NewTicker(1 * time.Hour)
go hm.periodicCleanup() go hm.periodicCleanup()
return hm return hm
} }
@ -56,14 +56,14 @@ func (hm *HistoryManager) periodicCleanup() {
func (hm *HistoryManager) RecordAircraft(record *AircraftHistoryRecord) error { func (hm *HistoryManager) RecordAircraft(record *AircraftHistoryRecord) error {
hm.mutex.Lock() hm.mutex.Lock()
defer hm.mutex.Unlock() defer hm.mutex.Unlock()
query := ` query := `
INSERT INTO aircraft_history INSERT INTO aircraft_history
(icao, callsign, squawk, latitude, longitude, altitude, (icao, callsign, squawk, latitude, longitude, altitude,
vertical_rate, speed, track, source_id, signal_strength, timestamp) vertical_rate, speed, track, source_id, signal_strength, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
` `
_, err := hm.db.Exec(query, _, err := hm.db.Exec(query,
record.ICAO, record.ICAO,
record.Callsign, record.Callsign,
@ -78,7 +78,7 @@ func (hm *HistoryManager) RecordAircraft(record *AircraftHistoryRecord) error {
record.SignalStrength, record.SignalStrength,
record.Timestamp, record.Timestamp,
) )
return err return err
} }
@ -86,16 +86,16 @@ func (hm *HistoryManager) RecordAircraftBatch(records []AircraftHistoryRecord) e
if len(records) == 0 { if len(records) == 0 {
return nil return nil
} }
hm.mutex.Lock() hm.mutex.Lock()
defer hm.mutex.Unlock() defer hm.mutex.Unlock()
tx, err := hm.db.Begin() tx, err := hm.db.Begin()
if err != nil { if err != nil {
return err return err
} }
defer tx.Rollback() defer tx.Rollback()
stmt, err := tx.Prepare(` stmt, err := tx.Prepare(`
INSERT INTO aircraft_history INSERT INTO aircraft_history
(icao, callsign, squawk, latitude, longitude, altitude, (icao, callsign, squawk, latitude, longitude, altitude,
@ -106,7 +106,7 @@ func (hm *HistoryManager) RecordAircraftBatch(records []AircraftHistoryRecord) e
return err return err
} }
defer stmt.Close() defer stmt.Close()
for _, record := range records { for _, record := range records {
_, err := stmt.Exec( _, err := stmt.Exec(
record.ICAO, record.ICAO,
@ -126,16 +126,16 @@ func (hm *HistoryManager) RecordAircraftBatch(records []AircraftHistoryRecord) e
return fmt.Errorf("failed to insert record for ICAO %s: %w", record.ICAO, err) return fmt.Errorf("failed to insert record for ICAO %s: %w", record.ICAO, err)
} }
} }
return tx.Commit() return tx.Commit()
} }
func (hm *HistoryManager) GetAircraftHistory(icao string, hours int) ([]AircraftHistoryRecord, error) { func (hm *HistoryManager) GetAircraftHistory(icao string, hours int) ([]AircraftHistoryRecord, error) {
hm.mutex.RLock() hm.mutex.RLock()
defer hm.mutex.RUnlock() defer hm.mutex.RUnlock()
since := time.Now().Add(-time.Duration(hours) * time.Hour) since := time.Now().Add(-time.Duration(hours) * time.Hour)
query := ` query := `
SELECT icao, callsign, squawk, latitude, longitude, altitude, SELECT icao, callsign, squawk, latitude, longitude, altitude,
vertical_rate, speed, track, source_id, signal_strength, timestamp vertical_rate, speed, track, source_id, signal_strength, timestamp
@ -144,13 +144,13 @@ func (hm *HistoryManager) GetAircraftHistory(icao string, hours int) ([]Aircraft
ORDER BY timestamp DESC ORDER BY timestamp DESC
LIMIT 1000 LIMIT 1000
` `
rows, err := hm.db.Query(query, icao, since) rows, err := hm.db.Query(query, icao, since)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var records []AircraftHistoryRecord var records []AircraftHistoryRecord
for rows.Next() { for rows.Next() {
var record AircraftHistoryRecord var record AircraftHistoryRecord
@ -173,16 +173,16 @@ func (hm *HistoryManager) GetAircraftHistory(icao string, hours int) ([]Aircraft
} }
records = append(records, record) records = append(records, record)
} }
return records, rows.Err() return records, rows.Err()
} }
func (hm *HistoryManager) GetAircraftTrack(icao string, hours int) ([]TrackPoint, error) { func (hm *HistoryManager) GetAircraftTrack(icao string, hours int) ([]TrackPoint, error) {
hm.mutex.RLock() hm.mutex.RLock()
defer hm.mutex.RUnlock() defer hm.mutex.RUnlock()
since := time.Now().Add(-time.Duration(hours) * time.Hour) since := time.Now().Add(-time.Duration(hours) * time.Hour)
query := ` query := `
SELECT latitude, longitude, altitude, timestamp SELECT latitude, longitude, altitude, timestamp
FROM aircraft_history FROM aircraft_history
@ -191,13 +191,13 @@ func (hm *HistoryManager) GetAircraftTrack(icao string, hours int) ([]TrackPoint
ORDER BY timestamp ASC ORDER BY timestamp ASC
LIMIT 500 LIMIT 500
` `
rows, err := hm.db.Query(query, icao, since) rows, err := hm.db.Query(query, icao, since)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var track []TrackPoint var track []TrackPoint
for rows.Next() { for rows.Next() {
var point TrackPoint var point TrackPoint
@ -212,16 +212,16 @@ func (hm *HistoryManager) GetAircraftTrack(icao string, hours int) ([]TrackPoint
} }
track = append(track, point) track = append(track, point)
} }
return track, rows.Err() return track, rows.Err()
} }
func (hm *HistoryManager) GetRecentAircraft(hours int, limit int) ([]string, error) { func (hm *HistoryManager) GetRecentAircraft(hours int, limit int) ([]string, error) {
hm.mutex.RLock() hm.mutex.RLock()
defer hm.mutex.RUnlock() defer hm.mutex.RUnlock()
since := time.Now().Add(-time.Duration(hours) * time.Hour) since := time.Now().Add(-time.Duration(hours) * time.Hour)
query := ` query := `
SELECT DISTINCT icao SELECT DISTINCT icao
FROM aircraft_history FROM aircraft_history
@ -229,13 +229,13 @@ func (hm *HistoryManager) GetRecentAircraft(hours int, limit int) ([]string, err
ORDER BY MAX(timestamp) DESC ORDER BY MAX(timestamp) DESC
LIMIT ? LIMIT ?
` `
rows, err := hm.db.Query(query, since, limit) rows, err := hm.db.Query(query, since, limit)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var icaos []string var icaos []string
for rows.Next() { for rows.Next() {
var icao string var icao string
@ -245,20 +245,20 @@ func (hm *HistoryManager) GetRecentAircraft(hours int, limit int) ([]string, err
} }
icaos = append(icaos, icao) icaos = append(icaos, icao)
} }
return icaos, rows.Err() return icaos, rows.Err()
} }
func (hm *HistoryManager) GetAircraftLastSeen(icao string) (time.Time, error) { func (hm *HistoryManager) GetAircraftLastSeen(icao string) (time.Time, error) {
hm.mutex.RLock() hm.mutex.RLock()
defer hm.mutex.RUnlock() defer hm.mutex.RUnlock()
query := ` query := `
SELECT MAX(timestamp) SELECT MAX(timestamp)
FROM aircraft_history FROM aircraft_history
WHERE icao = ? WHERE icao = ?
` `
var lastSeen time.Time var lastSeen time.Time
err := hm.db.QueryRow(query, icao).Scan(&lastSeen) err := hm.db.QueryRow(query, icao).Scan(&lastSeen)
return lastSeen, err return lastSeen, err
@ -267,24 +267,24 @@ func (hm *HistoryManager) GetAircraftLastSeen(icao string) (time.Time, error) {
func (hm *HistoryManager) CleanupOldHistory() error { func (hm *HistoryManager) CleanupOldHistory() error {
hm.mutex.Lock() hm.mutex.Lock()
defer hm.mutex.Unlock() defer hm.mutex.Unlock()
if hm.maxHistoryDays <= 0 { if hm.maxHistoryDays <= 0 {
return nil // No cleanup if maxHistoryDays is 0 or negative return nil // No cleanup if maxHistoryDays is 0 or negative
} }
cutoff := time.Now().AddDate(0, 0, -hm.maxHistoryDays) cutoff := time.Now().AddDate(0, 0, -hm.maxHistoryDays)
query := `DELETE FROM aircraft_history WHERE timestamp < ?` query := `DELETE FROM aircraft_history WHERE timestamp < ?`
result, err := hm.db.Exec(query, cutoff) result, err := hm.db.Exec(query, cutoff)
if err != nil { if err != nil {
return err return err
} }
rowsAffected, err := result.RowsAffected() rowsAffected, err := result.RowsAffected()
if err == nil && rowsAffected > 0 { if err == nil && rowsAffected > 0 {
fmt.Printf("Cleaned up %d old aircraft history records\n", rowsAffected) fmt.Printf("Cleaned up %d old aircraft history records\n", rowsAffected)
} }
return nil return nil
} }
@ -295,9 +295,9 @@ func (hm *HistoryManager) GetStatistics() (map[string]interface{}, error) {
func (hm *HistoryManager) GetHistoryStats() (map[string]interface{}, error) { func (hm *HistoryManager) GetHistoryStats() (map[string]interface{}, error) {
hm.mutex.RLock() hm.mutex.RLock()
defer hm.mutex.RUnlock() defer hm.mutex.RUnlock()
stats := make(map[string]interface{}) stats := make(map[string]interface{})
// Total records // Total records
var totalRecords int var totalRecords int
err := hm.db.QueryRow(`SELECT COUNT(*) FROM aircraft_history`).Scan(&totalRecords) err := hm.db.QueryRow(`SELECT COUNT(*) FROM aircraft_history`).Scan(&totalRecords)
@ -305,7 +305,7 @@ func (hm *HistoryManager) GetHistoryStats() (map[string]interface{}, error) {
return nil, err return nil, err
} }
stats["total_records"] = totalRecords stats["total_records"] = totalRecords
// Unique aircraft // Unique aircraft
var uniqueAircraft int var uniqueAircraft int
err = hm.db.QueryRow(`SELECT COUNT(DISTINCT icao) FROM aircraft_history`).Scan(&uniqueAircraft) err = hm.db.QueryRow(`SELECT COUNT(DISTINCT icao) FROM aircraft_history`).Scan(&uniqueAircraft)
@ -313,7 +313,7 @@ func (hm *HistoryManager) GetHistoryStats() (map[string]interface{}, error) {
return nil, err return nil, err
} }
stats["unique_aircraft"] = uniqueAircraft stats["unique_aircraft"] = uniqueAircraft
// Recent records (last 24 hours) // Recent records (last 24 hours)
var recentRecords int var recentRecords int
since := time.Now().Add(-24 * time.Hour) since := time.Now().Add(-24 * time.Hour)
@ -322,7 +322,7 @@ func (hm *HistoryManager) GetHistoryStats() (map[string]interface{}, error) {
return nil, err return nil, err
} }
stats["recent_records_24h"] = recentRecords stats["recent_records_24h"] = recentRecords
// Oldest and newest record timestamps (only if records exist) // Oldest and newest record timestamps (only if records exist)
if totalRecords > 0 { if totalRecords > 0 {
var oldestTimestamp, newestTimestamp time.Time var oldestTimestamp, newestTimestamp time.Time
@ -333,18 +333,18 @@ func (hm *HistoryManager) GetHistoryStats() (map[string]interface{}, error) {
stats["history_days"] = int(time.Since(oldestTimestamp).Hours() / 24) stats["history_days"] = int(time.Since(oldestTimestamp).Hours() / 24)
} }
} }
return stats, nil return stats, nil
} }
func (hm *HistoryManager) GetActivitySummary(hours int) (map[string]interface{}, error) { func (hm *HistoryManager) GetActivitySummary(hours int) (map[string]interface{}, error) {
hm.mutex.RLock() hm.mutex.RLock()
defer hm.mutex.RUnlock() defer hm.mutex.RUnlock()
since := time.Now().Add(-time.Duration(hours) * time.Hour) since := time.Now().Add(-time.Duration(hours) * time.Hour)
summary := make(map[string]interface{}) summary := make(map[string]interface{})
// Aircraft count in time period // Aircraft count in time period
var aircraftCount int var aircraftCount int
err := hm.db.QueryRow(` err := hm.db.QueryRow(`
@ -356,7 +356,7 @@ func (hm *HistoryManager) GetActivitySummary(hours int) (map[string]interface{},
return nil, err return nil, err
} }
summary["aircraft_count"] = aircraftCount summary["aircraft_count"] = aircraftCount
// Message count in time period // Message count in time period
var messageCount int var messageCount int
err = hm.db.QueryRow(` err = hm.db.QueryRow(`
@ -368,7 +368,7 @@ func (hm *HistoryManager) GetActivitySummary(hours int) (map[string]interface{},
return nil, err return nil, err
} }
summary["message_count"] = messageCount summary["message_count"] = messageCount
// Most active sources // Most active sources
query := ` query := `
SELECT source_id, COUNT(*) as count SELECT source_id, COUNT(*) as count
@ -378,13 +378,13 @@ func (hm *HistoryManager) GetActivitySummary(hours int) (map[string]interface{},
ORDER BY count DESC ORDER BY count DESC
LIMIT 5 LIMIT 5
` `
rows, err := hm.db.Query(query, since) rows, err := hm.db.Query(query, since)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
sources := make([]map[string]interface{}, 0) sources := make([]map[string]interface{}, 0)
for rows.Next() { for rows.Next() {
var sourceID string var sourceID string
@ -399,7 +399,7 @@ func (hm *HistoryManager) GetActivitySummary(hours int) (map[string]interface{},
}) })
} }
summary["top_sources"] = sources summary["top_sources"] = sources
return summary, nil return summary, nil
} }
@ -408,4 +408,4 @@ type TrackPoint struct {
Longitude float64 `json:"longitude"` Longitude float64 `json:"longitude"`
Altitude *int `json:"altitude,omitempty"` Altitude *int `json:"altitude,omitempty"`
Timestamp time.Time `json:"timestamp"` Timestamp time.Time `json:"timestamp"`
} }

View file

@ -192,17 +192,17 @@ func GetMigrations() []Migration {
}, },
// Future migrations will be added here // Future migrations will be added here
} }
// Calculate checksums // Calculate checksums
for i := range migrations { for i := range migrations {
migrations[i].Checksum = calculateChecksum(migrations[i].Up) migrations[i].Checksum = calculateChecksum(migrations[i].Up)
} }
// Sort by version // Sort by version
sort.Slice(migrations, func(i, j int) bool { sort.Slice(migrations, func(i, j int) bool {
return migrations[i].Version < migrations[j].Version return migrations[i].Version < migrations[j].Version
}) })
return migrations return migrations
} }
@ -212,19 +212,19 @@ func (m *Migrator) MigrateToLatest() error {
if err != nil { if err != nil {
return fmt.Errorf("failed to get current version: %v", err) return fmt.Errorf("failed to get current version: %v", err)
} }
migrations := GetMigrations() migrations := GetMigrations()
for _, migration := range migrations { for _, migration := range migrations {
if migration.Version <= currentVersion { if migration.Version <= currentVersion {
continue continue
} }
if err := m.applyMigration(migration); err != nil { if err := m.applyMigration(migration); err != nil {
return fmt.Errorf("failed to apply migration %d: %v", migration.Version, err) return fmt.Errorf("failed to apply migration %d: %v", migration.Version, err)
} }
} }
return nil return nil
} }
@ -234,20 +234,20 @@ func (m *Migrator) MigrateTo(targetVersion int) error {
if err != nil { if err != nil {
return fmt.Errorf("failed to get current version: %v", err) return fmt.Errorf("failed to get current version: %v", err)
} }
if targetVersion == currentVersion { if targetVersion == currentVersion {
return nil // Already at target version return nil // Already at target version
} }
migrations := GetMigrations() migrations := GetMigrations()
if targetVersion > currentVersion { if targetVersion > currentVersion {
// Forward migration // Forward migration
for _, migration := range migrations { for _, migration := range migrations {
if migration.Version <= currentVersion || migration.Version > targetVersion { if migration.Version <= currentVersion || migration.Version > targetVersion {
continue continue
} }
if err := m.applyMigration(migration); err != nil { if err := m.applyMigration(migration); err != nil {
return fmt.Errorf("failed to apply migration %d: %v", migration.Version, err) return fmt.Errorf("failed to apply migration %d: %v", migration.Version, err)
} }
@ -258,18 +258,18 @@ func (m *Migrator) MigrateTo(targetVersion int) error {
sort.Slice(migrations, func(i, j int) bool { sort.Slice(migrations, func(i, j int) bool {
return migrations[i].Version > migrations[j].Version return migrations[i].Version > migrations[j].Version
}) })
for _, migration := range migrations { for _, migration := range migrations {
if migration.Version > currentVersion || migration.Version <= targetVersion { if migration.Version > currentVersion || migration.Version <= targetVersion {
continue continue
} }
if err := m.rollbackMigration(migration); err != nil { if err := m.rollbackMigration(migration); err != nil {
return fmt.Errorf("failed to rollback migration %d: %v", migration.Version, err) return fmt.Errorf("failed to rollback migration %d: %v", migration.Version, err)
} }
} }
} }
return nil return nil
} }
@ -279,19 +279,19 @@ func (m *Migrator) GetAppliedMigrations() ([]MigrationRecord, error) {
if err := m.ensureSchemaInfoTable(); err != nil { if err := m.ensureSchemaInfoTable(); err != nil {
return nil, err return nil, err
} }
query := ` query := `
SELECT version, description, applied_at, checksum SELECT version, description, applied_at, checksum
FROM schema_info FROM schema_info
ORDER BY version ORDER BY version
` `
rows, err := m.conn.Query(query) rows, err := m.conn.Query(query)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to query applied migrations: %v", err) return nil, fmt.Errorf("failed to query applied migrations: %v", err)
} }
defer rows.Close() defer rows.Close()
var migrations []MigrationRecord var migrations []MigrationRecord
for rows.Next() { for rows.Next() {
var migration MigrationRecord var migration MigrationRecord
@ -306,7 +306,7 @@ func (m *Migrator) GetAppliedMigrations() ([]MigrationRecord, error) {
} }
migrations = append(migrations, migration) migrations = append(migrations, migration)
} }
return migrations, nil return migrations, nil
} }
@ -315,13 +315,13 @@ func (m *Migrator) getCurrentVersion() (int, error) {
if err := m.ensureSchemaInfoTable(); err != nil { if err := m.ensureSchemaInfoTable(); err != nil {
return 0, err return 0, err
} }
var version int var version int
err := m.conn.QueryRow(`SELECT COALESCE(MAX(version), 0) FROM schema_info`).Scan(&version) err := m.conn.QueryRow(`SELECT COALESCE(MAX(version), 0) FROM schema_info`).Scan(&version)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to get current version: %v", err) return 0, fmt.Errorf("failed to get current version: %v", err)
} }
return version, nil return version, nil
} }
@ -332,13 +332,13 @@ func (m *Migrator) applyMigration(migration Migration) error {
return fmt.Errorf("failed to begin transaction: %v", err) return fmt.Errorf("failed to begin transaction: %v", err)
} }
defer tx.Rollback() defer tx.Rollback()
// Warn about data loss // Warn about data loss
if migration.DataLoss { if migration.DataLoss {
// In a real application, this would show a warning to the user // In a real application, this would show a warning to the user
// For now, we'll just log it // For now, we'll just log it
} }
// Execute migration SQL // Execute migration SQL
statements := strings.Split(migration.Up, ";") statements := strings.Split(migration.Up, ";")
for _, stmt := range statements { for _, stmt := range statements {
@ -346,22 +346,22 @@ func (m *Migrator) applyMigration(migration Migration) error {
if stmt == "" { if stmt == "" {
continue continue
} }
if _, err := tx.Exec(stmt); err != nil { if _, err := tx.Exec(stmt); err != nil {
return fmt.Errorf("failed to execute migration statement: %v", err) return fmt.Errorf("failed to execute migration statement: %v", err)
} }
} }
// Record migration // Record migration
_, err = tx.Exec(` _, err = tx.Exec(`
INSERT INTO schema_info (version, description, checksum) INSERT INTO schema_info (version, description, checksum)
VALUES (?, ?, ?) VALUES (?, ?, ?)
`, migration.Version, migration.Description, migration.Checksum) `, migration.Version, migration.Description, migration.Checksum)
if err != nil { if err != nil {
return fmt.Errorf("failed to record migration: %v", err) return fmt.Errorf("failed to record migration: %v", err)
} }
return tx.Commit() return tx.Commit()
} }
@ -370,13 +370,13 @@ func (m *Migrator) rollbackMigration(migration Migration) error {
if migration.Down == "" { if migration.Down == "" {
return fmt.Errorf("migration %d has no rollback script", migration.Version) return fmt.Errorf("migration %d has no rollback script", migration.Version)
} }
tx, err := m.conn.Begin() tx, err := m.conn.Begin()
if err != nil { if err != nil {
return fmt.Errorf("failed to begin transaction: %v", err) return fmt.Errorf("failed to begin transaction: %v", err)
} }
defer tx.Rollback() defer tx.Rollback()
// Execute rollback SQL // Execute rollback SQL
statements := strings.Split(migration.Down, ";") statements := strings.Split(migration.Down, ";")
for _, stmt := range statements { for _, stmt := range statements {
@ -384,18 +384,18 @@ func (m *Migrator) rollbackMigration(migration Migration) error {
if stmt == "" { if stmt == "" {
continue continue
} }
if _, err := tx.Exec(stmt); err != nil { if _, err := tx.Exec(stmt); err != nil {
return fmt.Errorf("failed to execute rollback statement: %v", err) return fmt.Errorf("failed to execute rollback statement: %v", err)
} }
} }
// Remove migration record // Remove migration record
_, err = tx.Exec(`DELETE FROM schema_info WHERE version = ?`, migration.Version) _, err = tx.Exec(`DELETE FROM schema_info WHERE version = ?`, migration.Version)
if err != nil { if err != nil {
return fmt.Errorf("failed to remove migration record: %v", err) return fmt.Errorf("failed to remove migration record: %v", err)
} }
return tx.Commit() return tx.Commit()
} }
@ -416,4 +416,4 @@ func (m *Migrator) ensureSchemaInfoTable() error {
func calculateChecksum(content string) string { func calculateChecksum(content string) string {
// Simple checksum - in production, use a proper hash function // Simple checksum - in production, use a proper hash function
return fmt.Sprintf("%x", len(content)) return fmt.Sprintf("%x", len(content))
} }

View file

@ -24,7 +24,7 @@ func NewOptimizationManager(db *Database, config *Config) *OptimizationManager {
// PerformMaintenance runs database maintenance tasks including VACUUM // PerformMaintenance runs database maintenance tasks including VACUUM
func (om *OptimizationManager) PerformMaintenance() error { func (om *OptimizationManager) PerformMaintenance() error {
now := time.Now() now := time.Now()
// Check if VACUUM is needed // Check if VACUUM is needed
if om.config.VacuumInterval > 0 && now.Sub(om.lastVacuum) >= om.config.VacuumInterval { if om.config.VacuumInterval > 0 && now.Sub(om.lastVacuum) >= om.config.VacuumInterval {
if err := om.VacuumDatabase(); err != nil { if err := om.VacuumDatabase(); err != nil {
@ -32,7 +32,7 @@ func (om *OptimizationManager) PerformMaintenance() error {
} }
om.lastVacuum = now om.lastVacuum = now
} }
return nil return nil
} }
@ -42,37 +42,37 @@ func (om *OptimizationManager) VacuumDatabase() error {
if conn == nil { if conn == nil {
return fmt.Errorf("database connection not available") return fmt.Errorf("database connection not available")
} }
start := time.Now() start := time.Now()
// Get size before VACUUM // Get size before VACUUM
sizeBefore, err := om.getDatabaseSize() sizeBefore, err := om.getDatabaseSize()
if err != nil { if err != nil {
return fmt.Errorf("failed to get database size: %w", err) return fmt.Errorf("failed to get database size: %w", err)
} }
// Perform VACUUM // Perform VACUUM
if _, err := conn.Exec("VACUUM"); err != nil { if _, err := conn.Exec("VACUUM"); err != nil {
return fmt.Errorf("VACUUM operation failed: %w", err) return fmt.Errorf("VACUUM operation failed: %w", err)
} }
// Get size after VACUUM // Get size after VACUUM
sizeAfter, err := om.getDatabaseSize() sizeAfter, err := om.getDatabaseSize()
if err != nil { if err != nil {
return fmt.Errorf("failed to get database size after VACUUM: %w", err) return fmt.Errorf("failed to get database size after VACUUM: %w", err)
} }
duration := time.Since(start) duration := time.Since(start)
savedBytes := sizeBefore - sizeAfter savedBytes := sizeBefore - sizeAfter
savedPercent := float64(savedBytes) / float64(sizeBefore) * 100 savedPercent := float64(savedBytes) / float64(sizeBefore) * 100
fmt.Printf("VACUUM completed in %v: %.1f MB → %.1f MB (saved %.1f MB, %.1f%%)\n", fmt.Printf("VACUUM completed in %v: %.1f MB → %.1f MB (saved %.1f MB, %.1f%%)\n",
duration, duration,
float64(sizeBefore)/(1024*1024), float64(sizeBefore)/(1024*1024),
float64(sizeAfter)/(1024*1024), float64(sizeAfter)/(1024*1024),
float64(savedBytes)/(1024*1024), float64(savedBytes)/(1024*1024),
savedPercent) savedPercent)
return nil return nil
} }
@ -82,13 +82,13 @@ func (om *OptimizationManager) OptimizeDatabase() error {
if conn == nil { if conn == nil {
return fmt.Errorf("database connection not available") return fmt.Errorf("database connection not available")
} }
fmt.Println("Optimizing database for storage efficiency...") fmt.Println("Optimizing database for storage efficiency...")
// Apply storage-friendly pragmas // Apply storage-friendly pragmas
optimizations := []struct{ optimizations := []struct {
name string name string
query string query string
description string description string
}{ }{
{"Auto VACUUM", "PRAGMA auto_vacuum = INCREMENTAL", "Enable incremental auto-vacuum"}, {"Auto VACUUM", "PRAGMA auto_vacuum = INCREMENTAL", "Enable incremental auto-vacuum"},
@ -96,7 +96,7 @@ func (om *OptimizationManager) OptimizeDatabase() error {
{"Optimize", "PRAGMA optimize", "Update SQLite query planner statistics"}, {"Optimize", "PRAGMA optimize", "Update SQLite query planner statistics"},
{"Analyze", "ANALYZE", "Update table statistics for better query plans"}, {"Analyze", "ANALYZE", "Update table statistics for better query plans"},
} }
for _, opt := range optimizations { for _, opt := range optimizations {
if _, err := conn.Exec(opt.query); err != nil { if _, err := conn.Exec(opt.query); err != nil {
fmt.Printf("Warning: %s failed: %v\n", opt.name, err) fmt.Printf("Warning: %s failed: %v\n", opt.name, err)
@ -104,7 +104,7 @@ func (om *OptimizationManager) OptimizeDatabase() error {
fmt.Printf("✓ %s: %s\n", opt.name, opt.description) fmt.Printf("✓ %s: %s\n", opt.name, opt.description)
} }
} }
return nil return nil
} }
@ -114,83 +114,83 @@ func (om *OptimizationManager) OptimizePageSize(pageSize int) error {
if conn == nil { if conn == nil {
return fmt.Errorf("database connection not available") return fmt.Errorf("database connection not available")
} }
// Check current page size // Check current page size
var currentPageSize int var currentPageSize int
if err := conn.QueryRow("PRAGMA page_size").Scan(&currentPageSize); err != nil { if err := conn.QueryRow("PRAGMA page_size").Scan(&currentPageSize); err != nil {
return fmt.Errorf("failed to get current page size: %w", err) return fmt.Errorf("failed to get current page size: %w", err)
} }
if currentPageSize == pageSize { if currentPageSize == pageSize {
fmt.Printf("Page size already optimal: %d bytes\n", pageSize) fmt.Printf("Page size already optimal: %d bytes\n", pageSize)
return nil return nil
} }
fmt.Printf("Optimizing page size: %d → %d bytes (requires VACUUM)\n", currentPageSize, pageSize) fmt.Printf("Optimizing page size: %d → %d bytes (requires VACUUM)\n", currentPageSize, pageSize)
// Set new page size // Set new page size
query := fmt.Sprintf("PRAGMA page_size = %d", pageSize) query := fmt.Sprintf("PRAGMA page_size = %d", pageSize)
if _, err := conn.Exec(query); err != nil { if _, err := conn.Exec(query); err != nil {
return fmt.Errorf("failed to set page size: %w", err) return fmt.Errorf("failed to set page size: %w", err)
} }
// VACUUM to apply the new page size // VACUUM to apply the new page size
if err := om.VacuumDatabase(); err != nil { if err := om.VacuumDatabase(); err != nil {
return fmt.Errorf("failed to apply page size change: %w", err) return fmt.Errorf("failed to apply page size change: %w", err)
} }
return nil return nil
} }
// GetOptimizationStats returns current database optimization statistics // GetOptimizationStats returns current database optimization statistics
func (om *OptimizationManager) GetOptimizationStats() (*OptimizationStats, error) { func (om *OptimizationManager) GetOptimizationStats() (*OptimizationStats, error) {
stats := &OptimizationStats{} stats := &OptimizationStats{}
// Get database size // Get database size
size, err := om.getDatabaseSize() size, err := om.getDatabaseSize()
if err != nil { if err != nil {
return nil, err return nil, err
} }
stats.DatabaseSize = size stats.DatabaseSize = size
// Get page statistics // Get page statistics
conn := om.db.GetConnection() conn := om.db.GetConnection()
if conn != nil { if conn != nil {
var pageSize, pageCount, freelistCount int var pageSize, pageCount, freelistCount int
conn.QueryRow("PRAGMA page_size").Scan(&pageSize) conn.QueryRow("PRAGMA page_size").Scan(&pageSize)
conn.QueryRow("PRAGMA page_count").Scan(&pageCount) conn.QueryRow("PRAGMA page_count").Scan(&pageCount)
conn.QueryRow("PRAGMA freelist_count").Scan(&freelistCount) conn.QueryRow("PRAGMA freelist_count").Scan(&freelistCount)
stats.PageSize = pageSize stats.PageSize = pageSize
stats.PageCount = pageCount stats.PageCount = pageCount
stats.FreePages = freelistCount stats.FreePages = freelistCount
stats.UsedPages = pageCount - freelistCount stats.UsedPages = pageCount - freelistCount
if pageCount > 0 { if pageCount > 0 {
stats.Efficiency = float64(stats.UsedPages) / float64(pageCount) * 100 stats.Efficiency = float64(stats.UsedPages) / float64(pageCount) * 100
} }
// Check auto vacuum setting // Check auto vacuum setting
var autoVacuum int var autoVacuum int
conn.QueryRow("PRAGMA auto_vacuum").Scan(&autoVacuum) conn.QueryRow("PRAGMA auto_vacuum").Scan(&autoVacuum)
stats.AutoVacuumEnabled = autoVacuum > 0 stats.AutoVacuumEnabled = autoVacuum > 0
} }
stats.LastVacuum = om.lastVacuum stats.LastVacuum = om.lastVacuum
return stats, nil return stats, nil
} }
// OptimizationStats holds database storage optimization statistics // OptimizationStats holds database storage optimization statistics
type OptimizationStats struct { type OptimizationStats struct {
DatabaseSize int64 `json:"database_size"` DatabaseSize int64 `json:"database_size"`
PageSize int `json:"page_size"` PageSize int `json:"page_size"`
PageCount int `json:"page_count"` PageCount int `json:"page_count"`
UsedPages int `json:"used_pages"` UsedPages int `json:"used_pages"`
FreePages int `json:"free_pages"` FreePages int `json:"free_pages"`
Efficiency float64 `json:"efficiency_percent"` Efficiency float64 `json:"efficiency_percent"`
AutoVacuumEnabled bool `json:"auto_vacuum_enabled"` AutoVacuumEnabled bool `json:"auto_vacuum_enabled"`
LastVacuum time.Time `json:"last_vacuum"` LastVacuum time.Time `json:"last_vacuum"`
} }
// getDatabaseSize returns the current database file size in bytes // getDatabaseSize returns the current database file size in bytes
@ -198,11 +198,11 @@ func (om *OptimizationManager) getDatabaseSize() (int64, error) {
if om.config.Path == "" { if om.config.Path == "" {
return 0, fmt.Errorf("database path not configured") return 0, fmt.Errorf("database path not configured")
} }
stat, err := os.Stat(om.config.Path) stat, err := os.Stat(om.config.Path)
if err != nil { if err != nil {
return 0, fmt.Errorf("failed to stat database file: %w", err) return 0, fmt.Errorf("failed to stat database file: %w", err)
} }
return stat.Size(), nil return stat.Size(), nil
} }

View file

@ -221,13 +221,13 @@ func TestOptimizationManager_InvalidPath(t *testing.T) {
func TestOptimizationStats_JSON(t *testing.T) { func TestOptimizationStats_JSON(t *testing.T) {
stats := &OptimizationStats{ stats := &OptimizationStats{
DatabaseSize: 1024000, DatabaseSize: 1024000,
PageSize: 4096, PageSize: 4096,
PageCount: 250, PageCount: 250,
UsedPages: 200, UsedPages: 200,
FreePages: 50, FreePages: 50,
Efficiency: 80.0, Efficiency: 80.0,
AutoVacuumEnabled: true, AutoVacuumEnabled: true,
LastVacuum: time.Now(), LastVacuum: time.Now(),
} }
// Test that all fields are accessible // Test that all fields are accessible
@ -286,9 +286,9 @@ func TestOptimizationManager_WithRealData(t *testing.T) {
} }
// Compare efficiency // Compare efficiency
t.Logf("Optimization results: %.2f%% → %.2f%% efficiency", t.Logf("Optimization results: %.2f%% → %.2f%% efficiency",
statsBefore.Efficiency, statsAfter.Efficiency) statsBefore.Efficiency, statsAfter.Efficiency)
// After optimization, we should have auto-vacuum enabled // After optimization, we should have auto-vacuum enabled
if !statsAfter.AutoVacuumEnabled { if !statsAfter.AutoVacuumEnabled {
t.Error("Auto-vacuum should be enabled after optimization") t.Error("Auto-vacuum should be enabled after optimization")
@ -304,4 +304,4 @@ func TestOptimizationManager_WithRealData(t *testing.T) {
if count == 0 { if count == 0 {
t.Error("Data lost during optimization") t.Error("Data lost during optimization")
} }
} }

View file

@ -17,17 +17,17 @@ func ResolveDatabasePath(configPath string) (string, error) {
} }
return configPath, nil return configPath, nil
} }
// Try system location first (for services) // Try system location first (for services)
if systemPath, err := trySystemPath(); err == nil { if systemPath, err := trySystemPath(); err == nil {
return systemPath, nil return systemPath, nil
} }
// Try user data directory // Try user data directory
if userPath, err := tryUserPath(); err == nil { if userPath, err := tryUserPath(); err == nil {
return userPath, nil return userPath, nil
} }
// Fallback to current directory // Fallback to current directory
return tryCurrentDirPath() return tryCurrentDirPath()
} }
@ -35,7 +35,7 @@ func ResolveDatabasePath(configPath string) (string, error) {
// trySystemPath attempts to use system-wide database location // trySystemPath attempts to use system-wide database location
func trySystemPath() (string, error) { func trySystemPath() (string, error) {
var systemDir string var systemDir string
switch runtime.GOOS { switch runtime.GOOS {
case "linux": case "linux":
systemDir = "/var/lib/skyview" systemDir = "/var/lib/skyview"
@ -46,26 +46,26 @@ func trySystemPath() (string, error) {
default: default:
return "", fmt.Errorf("system path not supported on %s", runtime.GOOS) return "", fmt.Errorf("system path not supported on %s", runtime.GOOS)
} }
// Check if directory exists and is writable // Check if directory exists and is writable
if err := ensureDirExists(systemDir); err != nil { if err := ensureDirExists(systemDir); err != nil {
return "", err return "", err
} }
dbPath := filepath.Join(systemDir, "skyview.db") dbPath := filepath.Join(systemDir, "skyview.db")
// Test write permissions // Test write permissions
if err := testWritePermissions(dbPath); err != nil { if err := testWritePermissions(dbPath); err != nil {
return "", err return "", err
} }
return dbPath, nil return dbPath, nil
} }
// tryUserPath attempts to use user data directory // tryUserPath attempts to use user data directory
func tryUserPath() (string, error) { func tryUserPath() (string, error) {
var userDataDir string var userDataDir string
switch runtime.GOOS { switch runtime.GOOS {
case "linux": case "linux":
if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" { if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" {
@ -91,20 +91,20 @@ func tryUserPath() (string, error) {
default: default:
return "", fmt.Errorf("user path not supported on %s", runtime.GOOS) return "", fmt.Errorf("user path not supported on %s", runtime.GOOS)
} }
skyviewDir := filepath.Join(userDataDir, "skyview") skyviewDir := filepath.Join(userDataDir, "skyview")
if err := ensureDirExists(skyviewDir); err != nil { if err := ensureDirExists(skyviewDir); err != nil {
return "", err return "", err
} }
dbPath := filepath.Join(skyviewDir, "skyview.db") dbPath := filepath.Join(skyviewDir, "skyview.db")
// Test write permissions // Test write permissions
if err := testWritePermissions(dbPath); err != nil { if err := testWritePermissions(dbPath); err != nil {
return "", err return "", err
} }
return dbPath, nil return dbPath, nil
} }
@ -114,14 +114,14 @@ func tryCurrentDirPath() (string, error) {
if err != nil { if err != nil {
return "", fmt.Errorf("cannot get current directory: %v", err) return "", fmt.Errorf("cannot get current directory: %v", err)
} }
dbPath := filepath.Join(currentDir, "skyview.db") dbPath := filepath.Join(currentDir, "skyview.db")
// Test write permissions // Test write permissions
if err := testWritePermissions(dbPath); err != nil { if err := testWritePermissions(dbPath); err != nil {
return "", err return "", err
} }
return dbPath, nil return dbPath, nil
} }
@ -134,23 +134,23 @@ func ensureDirExists(dir string) error {
} else if err != nil { } else if err != nil {
return fmt.Errorf("cannot access directory %s: %v", dir, err) return fmt.Errorf("cannot access directory %s: %v", dir, err)
} }
return nil return nil
} }
// testWritePermissions verifies write access to the database path // testWritePermissions verifies write access to the database path
func testWritePermissions(dbPath string) error { func testWritePermissions(dbPath string) error {
dir := filepath.Dir(dbPath) dir := filepath.Dir(dbPath)
// Check directory write permissions // Check directory write permissions
testFile := filepath.Join(dir, ".skyview_write_test") testFile := filepath.Join(dir, ".skyview_write_test")
if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil { if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil {
return fmt.Errorf("no write permission to directory %s: %v", dir, err) return fmt.Errorf("no write permission to directory %s: %v", dir, err)
} }
// Clean up test file // Clean up test file
os.Remove(testFile) os.Remove(testFile)
return nil return nil
} }
@ -171,4 +171,4 @@ func IsSystemPath(dbPath string) bool {
return programData != "" && filepath.HasPrefix(dbPath, filepath.Join(programData, "skyview")) return programData != "" && filepath.HasPrefix(dbPath, filepath.Join(programData, "skyview"))
} }
return false return false
} }

View file

@ -33,4 +33,4 @@ func setupTestDatabase(t *testing.T) (*Database, func()) {
} }
return db, cleanup return db, cleanup
} }

View file

@ -448,7 +448,7 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa
state.LastUpdate = timestamp state.LastUpdate = timestamp
state.TotalMessages++ state.TotalMessages++
// Persist to database if available and aircraft has position // Persist to database if available and aircraft has position
if m.db != nil && aircraft.Latitude != 0 && aircraft.Longitude != 0 { if m.db != nil && aircraft.Latitude != 0 && aircraft.Longitude != 0 {
m.saveAircraftToDatabase(aircraft, sourceID, signal, timestamp) m.saveAircraftToDatabase(aircraft, sourceID, signal, timestamp)
@ -1077,7 +1077,7 @@ func (m *Merger) validatePosition(aircraft *modes.Aircraft, state *AircraftState
func (m *Merger) saveAircraftToDatabase(aircraft *modes.Aircraft, sourceID string, signal float64, timestamp time.Time) { func (m *Merger) saveAircraftToDatabase(aircraft *modes.Aircraft, sourceID string, signal float64, timestamp time.Time) {
// Convert ICAO24 to hex string // Convert ICAO24 to hex string
icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24) icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24)
// Prepare database record // Prepare database record
record := database.AircraftHistoryRecord{ record := database.AircraftHistoryRecord{
ICAO: icaoHex, ICAO: icaoHex,
@ -1087,7 +1087,7 @@ func (m *Merger) saveAircraftToDatabase(aircraft *modes.Aircraft, sourceID strin
SourceID: sourceID, SourceID: sourceID,
SignalStrength: &signal, SignalStrength: &signal,
} }
// Add optional fields if available // Add optional fields if available
if aircraft.Altitude > 0 { if aircraft.Altitude > 0 {
record.Altitude = &aircraft.Altitude record.Altitude = &aircraft.Altitude
@ -1107,7 +1107,7 @@ func (m *Merger) saveAircraftToDatabase(aircraft *modes.Aircraft, sourceID strin
if aircraft.Callsign != "" { if aircraft.Callsign != "" {
record.Callsign = &aircraft.Callsign record.Callsign = &aircraft.Callsign
} }
// Save to database (non-blocking to avoid slowing down real-time processing) // Save to database (non-blocking to avoid slowing down real-time processing)
go func() { go func() {
if err := m.db.GetHistoryManager().RecordAircraft(&record); err != nil { if err := m.db.GetHistoryManager().RecordAircraft(&record); err != nil {

View file

@ -55,13 +55,13 @@ type OriginConfig struct {
// - Concurrent broadcast system for WebSocket clients // - Concurrent broadcast system for WebSocket clients
// - CORS support for cross-origin web applications // - CORS support for cross-origin web applications
type Server struct { type Server struct {
host string // Bind address for HTTP server host string // Bind address for HTTP server
port int // TCP port for HTTP server port int // TCP port for HTTP server
merger *merger.Merger // Data source for aircraft information merger *merger.Merger // Data source for aircraft information
database *database.Database // Optional database for persistence database *database.Database // Optional database for persistence
staticFiles embed.FS // Embedded static web assets staticFiles embed.FS // Embedded static web assets
server *http.Server // HTTP server instance server *http.Server // HTTP server instance
origin OriginConfig // Geographic reference point origin OriginConfig // Geographic reference point
// WebSocket management // WebSocket management
wsClients map[*websocket.Conn]bool // Active WebSocket client connections wsClients map[*websocket.Conn]bool // Active WebSocket client connections
@ -919,19 +919,19 @@ func (s *Server) handleGetDatabaseStatus(w http.ResponseWriter, r *http.Request)
} }
response := make(map[string]interface{}) response := make(map[string]interface{})
// Get database path and size information // Get database path and size information
dbConfig := s.database.GetConfig() dbConfig := s.database.GetConfig()
dbPath := dbConfig.Path dbPath := dbConfig.Path
response["path"] = dbPath response["path"] = dbPath
// Get file size and modification time // Get file size and modification time
if stat, err := os.Stat(dbPath); err == nil { if stat, err := os.Stat(dbPath); err == nil {
response["size_bytes"] = stat.Size() response["size_bytes"] = stat.Size()
response["size_mb"] = float64(stat.Size()) / (1024 * 1024) response["size_mb"] = float64(stat.Size()) / (1024 * 1024)
response["modified"] = stat.ModTime().Unix() response["modified"] = stat.ModTime().Unix()
} }
// Get optimization statistics // Get optimization statistics
optimizer := database.NewOptimizationManager(s.database, dbConfig) optimizer := database.NewOptimizationManager(s.database, dbConfig)
if optimizationStats, err := optimizer.GetOptimizationStats(); err == nil { if optimizationStats, err := optimizer.GetOptimizationStats(); err == nil {
@ -945,14 +945,14 @@ func (s *Server) handleGetDatabaseStatus(w http.ResponseWriter, r *http.Request)
response["last_vacuum"] = optimizationStats.LastVacuum.Unix() response["last_vacuum"] = optimizationStats.LastVacuum.Unix()
} }
} }
// Get history statistics // Get history statistics
historyStats, err := s.database.GetHistoryManager().GetStatistics() historyStats, err := s.database.GetHistoryManager().GetStatistics()
if err != nil { if err != nil {
log.Printf("Error getting history statistics: %v", err) log.Printf("Error getting history statistics: %v", err)
historyStats = make(map[string]interface{}) historyStats = make(map[string]interface{})
} }
// Get callsign statistics if available // Get callsign statistics if available
callsignStats := make(map[string]interface{}) callsignStats := make(map[string]interface{})
if callsignManager := s.database.GetCallsignManager(); callsignManager != nil { if callsignManager := s.database.GetCallsignManager(); callsignManager != nil {
@ -963,23 +963,23 @@ func (s *Server) handleGetDatabaseStatus(w http.ResponseWriter, r *http.Request)
callsignStats = stats callsignStats = stats
} }
} }
// Get record counts for reference data // Get record counts for reference data
var airportCount, airlineCount int var airportCount, airlineCount int
s.database.GetConnection().QueryRow(`SELECT COUNT(*) FROM airports`).Scan(&airportCount) s.database.GetConnection().QueryRow(`SELECT COUNT(*) FROM airports`).Scan(&airportCount)
s.database.GetConnection().QueryRow(`SELECT COUNT(*) FROM airlines`).Scan(&airlineCount) s.database.GetConnection().QueryRow(`SELECT COUNT(*) FROM airlines`).Scan(&airlineCount)
referenceData := make(map[string]interface{}) referenceData := make(map[string]interface{})
referenceData["airports"] = airportCount referenceData["airports"] = airportCount
referenceData["airlines"] = airlineCount referenceData["airlines"] = airlineCount
response["database_available"] = true response["database_available"] = true
response["path"] = dbPath response["path"] = dbPath
response["reference_data"] = referenceData response["reference_data"] = referenceData
response["history"] = historyStats response["history"] = historyStats
response["callsign"] = callsignStats response["callsign"] = callsignStats
response["timestamp"] = time.Now().Unix() response["timestamp"] = time.Now().Unix()
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
} }
@ -993,20 +993,20 @@ func (s *Server) handleGetDataSources(w http.ResponseWriter, r *http.Request) {
// Create data loader instance // Create data loader instance
loader := database.NewDataLoader(s.database.GetConnection()) loader := database.NewDataLoader(s.database.GetConnection())
availableSources := database.GetAvailableDataSources() availableSources := database.GetAvailableDataSources()
loadedSources, err := loader.GetLoadedDataSources() loadedSources, err := loader.GetLoadedDataSources()
if err != nil { if err != nil {
log.Printf("Error getting loaded data sources: %v", err) log.Printf("Error getting loaded data sources: %v", err)
loadedSources = []database.DataSource{} loadedSources = []database.DataSource{}
} }
response := map[string]interface{}{ response := map[string]interface{}{
"available": availableSources, "available": availableSources,
"loaded": loadedSources, "loaded": loadedSources,
"timestamp": time.Now().Unix(), "timestamp": time.Now().Unix(),
} }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response) json.NewEncoder(w).Encode(response)
} }
@ -1021,7 +1021,7 @@ func (s *Server) handleGetCallsignInfo(w http.ResponseWriter, r *http.Request) {
// Extract callsign from URL parameters // Extract callsign from URL parameters
vars := mux.Vars(r) vars := mux.Vars(r)
callsign := vars["callsign"] callsign := vars["callsign"]
if callsign == "" { if callsign == "" {
http.Error(w, "Callsign parameter required", http.StatusBadRequest) http.Error(w, "Callsign parameter required", http.StatusBadRequest)
return return
@ -1036,7 +1036,7 @@ func (s *Server) handleGetCallsignInfo(w http.ResponseWriter, r *http.Request) {
} }
response := map[string]interface{}{ response := map[string]interface{}{
"callsign": callsignInfo, "callsign": callsignInfo,
"timestamp": time.Now().Unix(), "timestamp": time.Now().Unix(),
} }
@ -1070,12 +1070,12 @@ func (s *Server) debugEmbeddedFiles() {
func (s *Server) handleDatabasePage(w http.ResponseWriter, r *http.Request) { func (s *Server) handleDatabasePage(w http.ResponseWriter, r *http.Request) {
// Debug embedded files first // Debug embedded files first
s.debugEmbeddedFiles() s.debugEmbeddedFiles()
// Try to read the database HTML file from embedded assets // Try to read the database HTML file from embedded assets
data, err := s.staticFiles.ReadFile("static/database.html") data, err := s.staticFiles.ReadFile("static/database.html")
if err != nil { if err != nil {
log.Printf("Error reading database.html: %v", err) log.Printf("Error reading database.html: %v", err)
// Fallback: serve a simple HTML page with API calls // Fallback: serve a simple HTML page with API calls
fallbackHTML := `<!DOCTYPE html> fallbackHTML := `<!DOCTYPE html>
<html><head><title>Database Status - SkyView</title></head> <html><head><title>Database Status - SkyView</title></head>
@ -1091,7 +1091,7 @@ fetch('/api/database/status')
}); });
</script> </script>
</body></html>` </body></html>`
w.Header().Set("Content-Type", "text/html") w.Header().Set("Content-Type", "text/html")
w.Write([]byte(fallbackHTML)) w.Write([]byte(fallbackHTML))
return return

View file

@ -72,10 +72,10 @@ func NewDatabase() *Database {
db := &Database{ db := &Database{
codes: make(map[string]*CodeInfo), codes: make(map[string]*CodeInfo),
} }
// Initialize with standard transponder codes // Initialize with standard transponder codes
db.loadStandardCodes() db.loadStandardCodes()
return db return db
} }
@ -107,7 +107,7 @@ func (db *Database) loadStandardCodes() {
Priority: 90, Priority: 90,
Notes: "General emergency situation requiring immediate attention", Notes: "General emergency situation requiring immediate attention",
}, },
// Standard VFR/IFR Codes // Standard VFR/IFR Codes
{ {
Code: "1200", Code: "1200",
@ -149,7 +149,7 @@ func (db *Database) loadStandardCodes() {
Priority: 5, Priority: 5,
Notes: "Transponder operating but no specific code assigned", Notes: "Transponder operating but no specific code assigned",
}, },
// Special Purpose Codes // Special Purpose Codes
{ {
Code: "1255", Code: "1255",
@ -175,7 +175,7 @@ func (db *Database) loadStandardCodes() {
Priority: 35, Priority: 35,
Notes: "Military interceptor aircraft", Notes: "Military interceptor aircraft",
}, },
// Military Ranges // Military Ranges
{ {
Code: "4000", Code: "4000",
@ -193,7 +193,7 @@ func (db *Database) loadStandardCodes() {
Priority: 12, Priority: 12,
Notes: "Military interceptor operations (0100-0777 range)", Notes: "Military interceptor operations (0100-0777 range)",
}, },
// Additional Common Codes // Additional Common Codes
{ {
Code: "1201", Code: "1201",
@ -219,7 +219,7 @@ func (db *Database) loadStandardCodes() {
Priority: 8, Priority: 8,
Notes: "VFR flight above 12,500 feet requiring transponder", Notes: "VFR flight above 12,500 feet requiring transponder",
}, },
// European Specific // European Specific
{ {
Code: "7001", Code: "7001",
@ -246,7 +246,7 @@ func (db *Database) loadStandardCodes() {
Notes: "General Air Traffic operating in Other Air Traffic area", Notes: "General Air Traffic operating in Other Air Traffic area",
}, },
} }
// Add all codes to the database // Add all codes to the database
for _, code := range codes { for _, code := range codes {
db.codes[code.Code] = code db.codes[code.Code] = code
@ -254,7 +254,7 @@ func (db *Database) loadStandardCodes() {
} }
// Lookup returns information about a given transponder code // Lookup returns information about a given transponder code
// //
// The method accepts both 4-digit strings and integers, automatically // The method accepts both 4-digit strings and integers, automatically
// formatting them as needed. Returns nil if the code is not found in the database. // formatting them as needed. Returns nil if the code is not found in the database.
// //
@ -308,13 +308,13 @@ func (db *Database) LookupHex(hexCode string) *CodeInfo {
// - []*CodeInfo: Slice of all emergency codes, sorted by priority (highest first) // - []*CodeInfo: Slice of all emergency codes, sorted by priority (highest first)
func (db *Database) GetEmergencyCodes() []*CodeInfo { func (db *Database) GetEmergencyCodes() []*CodeInfo {
var emergencyCodes []*CodeInfo var emergencyCodes []*CodeInfo
for _, info := range db.codes { for _, info := range db.codes {
if info.Type == Emergency { if info.Type == Emergency {
emergencyCodes = append(emergencyCodes, info) emergencyCodes = append(emergencyCodes, info)
} }
} }
// Sort by priority (highest first) // Sort by priority (highest first)
for i := 0; i < len(emergencyCodes); i++ { for i := 0; i < len(emergencyCodes); i++ {
for j := i + 1; j < len(emergencyCodes); j++ { for j := i + 1; j < len(emergencyCodes); j++ {
@ -323,7 +323,7 @@ func (db *Database) GetEmergencyCodes() []*CodeInfo {
} }
} }
} }
return emergencyCodes return emergencyCodes
} }
@ -379,7 +379,7 @@ func (db *Database) FormatSquawkWithDescription(code string) string {
if info == nil { if info == nil {
return code // Return just the code if no description available return code // Return just the code if no description available
} }
switch info.Type { switch info.Type {
case Emergency: case Emergency:
return fmt.Sprintf("%s (⚠️ EMERGENCY - %s)", code, info.Description) return fmt.Sprintf("%s (⚠️ EMERGENCY - %s)", code, info.Description)
@ -390,4 +390,4 @@ func (db *Database) FormatSquawkWithDescription(code string) string {
default: default:
return fmt.Sprintf("%s (%s)", code, info.Description) return fmt.Sprintf("%s (%s)", code, info.Description)
} }
} }

View file

@ -9,7 +9,7 @@ func TestNewDatabase(t *testing.T) {
if db == nil { if db == nil {
t.Fatal("NewDatabase() returned nil") t.Fatal("NewDatabase() returned nil")
} }
if len(db.codes) == 0 { if len(db.codes) == 0 {
t.Error("Database should contain pre-loaded codes") t.Error("Database should contain pre-loaded codes")
} }
@ -17,20 +17,20 @@ func TestNewDatabase(t *testing.T) {
func TestEmergencyCodes(t *testing.T) { func TestEmergencyCodes(t *testing.T) {
db := NewDatabase() db := NewDatabase()
emergencyCodes := []string{"7500", "7600", "7700"} emergencyCodes := []string{"7500", "7600", "7700"}
for _, code := range emergencyCodes { for _, code := range emergencyCodes {
info := db.Lookup(code) info := db.Lookup(code)
if info == nil { if info == nil {
t.Errorf("Emergency code %s not found", code) t.Errorf("Emergency code %s not found", code)
continue continue
} }
if info.Type != Emergency { if info.Type != Emergency {
t.Errorf("Code %s should be Emergency type, got %s", code, info.Type) t.Errorf("Code %s should be Emergency type, got %s", code, info.Type)
} }
if !db.IsEmergencyCode(code) { if !db.IsEmergencyCode(code) {
t.Errorf("IsEmergencyCode(%s) should return true", code) t.Errorf("IsEmergencyCode(%s) should return true", code)
} }
@ -39,7 +39,7 @@ func TestEmergencyCodes(t *testing.T) {
func TestStandardCodes(t *testing.T) { func TestStandardCodes(t *testing.T) {
db := NewDatabase() db := NewDatabase()
testCases := []struct { testCases := []struct {
code string code string
description string description string
@ -48,16 +48,16 @@ func TestStandardCodes(t *testing.T) {
{"7000", "VFR - Visual Flight Rules"}, {"7000", "VFR - Visual Flight Rules"},
{"1000", "Mode A/C Not Assigned"}, {"1000", "Mode A/C Not Assigned"},
} }
for _, tc := range testCases { for _, tc := range testCases {
info := db.Lookup(tc.code) info := db.Lookup(tc.code)
if info == nil { if info == nil {
t.Errorf("Standard code %s not found", tc.code) t.Errorf("Standard code %s not found", tc.code)
continue continue
} }
if info.Description != tc.description { if info.Description != tc.description {
t.Errorf("Code %s: expected description %q, got %q", t.Errorf("Code %s: expected description %q, got %q",
tc.code, tc.description, info.Description) tc.code, tc.description, info.Description)
} }
} }
@ -65,17 +65,17 @@ func TestStandardCodes(t *testing.T) {
func TestLookupInt(t *testing.T) { func TestLookupInt(t *testing.T) {
db := NewDatabase() db := NewDatabase()
// Test integer lookup // Test integer lookup
info := db.LookupInt(7700) info := db.LookupInt(7700)
if info == nil { if info == nil {
t.Fatal("LookupInt(7700) returned nil") t.Fatal("LookupInt(7700) returned nil")
} }
if info.Code != "7700" { if info.Code != "7700" {
t.Errorf("Expected code '7700', got '%s'", info.Code) t.Errorf("Expected code '7700', got '%s'", info.Code)
} }
if info.Type != Emergency { if info.Type != Emergency {
t.Errorf("Code 7700 should be Emergency type, got %s", info.Type) t.Errorf("Code 7700 should be Emergency type, got %s", info.Type)
} }
@ -83,11 +83,11 @@ func TestLookupInt(t *testing.T) {
func TestLookupHex(t *testing.T) { func TestLookupHex(t *testing.T) {
db := NewDatabase() db := NewDatabase()
// 7700 in octal is 3840 in decimal, which is F00 in hex // 7700 in octal is 3840 in decimal, which is F00 in hex
// However, squawk codes are transmitted differently in different formats // However, squawk codes are transmitted differently in different formats
// For now, test with a simple hex conversion // For now, test with a simple hex conversion
// Test invalid hex // Test invalid hex
info := db.LookupHex("INVALID") info := db.LookupHex("INVALID")
if info != nil { if info != nil {
@ -97,7 +97,7 @@ func TestLookupHex(t *testing.T) {
func TestFormatSquawkWithDescription(t *testing.T) { func TestFormatSquawkWithDescription(t *testing.T) {
db := NewDatabase() db := NewDatabase()
testCases := []struct { testCases := []struct {
code string code string
expected string expected string
@ -108,7 +108,7 @@ func TestFormatSquawkWithDescription(t *testing.T) {
{"0000", "0000 (🔰 No Transponder/Military)"}, {"0000", "0000 (🔰 No Transponder/Military)"},
{"9999", "9999"}, // Unknown code should return just the code {"9999", "9999"}, // Unknown code should return just the code
} }
for _, tc := range testCases { for _, tc := range testCases {
result := db.FormatSquawkWithDescription(tc.code) result := db.FormatSquawkWithDescription(tc.code)
if result != tc.expected { if result != tc.expected {
@ -120,12 +120,12 @@ func TestFormatSquawkWithDescription(t *testing.T) {
func TestGetEmergencyCodes(t *testing.T) { func TestGetEmergencyCodes(t *testing.T) {
db := NewDatabase() db := NewDatabase()
emergencyCodes := db.GetEmergencyCodes() emergencyCodes := db.GetEmergencyCodes()
if len(emergencyCodes) != 3 { if len(emergencyCodes) != 3 {
t.Errorf("Expected 3 emergency codes, got %d", len(emergencyCodes)) t.Errorf("Expected 3 emergency codes, got %d", len(emergencyCodes))
} }
// Check that they're sorted by priority (highest first) // Check that they're sorted by priority (highest first)
for i := 1; i < len(emergencyCodes); i++ { for i := 1; i < len(emergencyCodes); i++ {
if emergencyCodes[i-1].Priority < emergencyCodes[i].Priority { if emergencyCodes[i-1].Priority < emergencyCodes[i].Priority {
@ -136,7 +136,7 @@ func TestGetEmergencyCodes(t *testing.T) {
func TestAddCustomCode(t *testing.T) { func TestAddCustomCode(t *testing.T) {
db := NewDatabase() db := NewDatabase()
customCode := &CodeInfo{ customCode := &CodeInfo{
Code: "1234", Code: "1234",
Description: "Test Custom Code", Description: "Test Custom Code",
@ -145,14 +145,14 @@ func TestAddCustomCode(t *testing.T) {
Priority: 50, Priority: 50,
Notes: "This is a test custom code", Notes: "This is a test custom code",
} }
db.AddCustomCode(customCode) db.AddCustomCode(customCode)
info := db.Lookup("1234") info := db.Lookup("1234")
if info == nil { if info == nil {
t.Fatal("Custom code not found after adding") t.Fatal("Custom code not found after adding")
} }
if info.Description != "Test Custom Code" { if info.Description != "Test Custom Code" {
t.Errorf("Custom code description mismatch: expected %q, got %q", t.Errorf("Custom code description mismatch: expected %q, got %q",
"Test Custom Code", info.Description) "Test Custom Code", info.Description)
@ -170,7 +170,7 @@ func TestCodeTypeString(t *testing.T) {
{Military, "Military"}, {Military, "Military"},
{Special, "Special"}, {Special, "Special"},
} }
for _, tc := range testCases { for _, tc := range testCases {
result := tc.codeType.String() result := tc.codeType.String()
if result != tc.expected { if result != tc.expected {
@ -181,16 +181,16 @@ func TestCodeTypeString(t *testing.T) {
func TestGetAllCodes(t *testing.T) { func TestGetAllCodes(t *testing.T) {
db := NewDatabase() db := NewDatabase()
allCodes := db.GetAllCodes() allCodes := db.GetAllCodes()
if len(allCodes) == 0 { if len(allCodes) == 0 {
t.Error("GetAllCodes() should return non-empty slice") t.Error("GetAllCodes() should return non-empty slice")
} }
// Verify we can find known codes in the result // Verify we can find known codes in the result
found7700 := false found7700 := false
found1200 := false found1200 := false
for _, code := range allCodes { for _, code := range allCodes {
if code.Code == "7700" { if code.Code == "7700" {
found7700 = true found7700 = true
@ -199,11 +199,11 @@ func TestGetAllCodes(t *testing.T) {
found1200 = true found1200 = true
} }
} }
if !found7700 { if !found7700 {
t.Error("Emergency code 7700 not found in GetAllCodes() result") t.Error("Emergency code 7700 not found in GetAllCodes() result")
} }
if !found1200 { if !found1200 {
t.Error("Standard code 1200 not found in GetAllCodes() result") t.Error("Standard code 1200 not found in GetAllCodes() result")
} }
} }