From 2bffa2c4183a99b33dfa685a2ff5dfd74a991a3b Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Mon, 1 Sep 2025 10:05:29 +0200 Subject: [PATCH] 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 --- assets/assets.go | 1 + cmd/skyview-data/main.go | 139 +++++++++--------- internal/database/api_client.go | 126 ++++++++-------- internal/database/database.go | 71 +++++---- internal/database/database_test.go | 2 +- internal/database/loader.go | 159 +++++++++++---------- internal/database/loader_test.go | 18 +-- internal/database/manager_callsign.go | 88 ++++++------ internal/database/manager_callsign_test.go | 24 ++-- internal/database/manager_history.go | 96 ++++++------- internal/database/migrations.go | 64 ++++----- internal/database/optimization.go | 80 +++++------ internal/database/optimization_test.go | 18 +-- internal/database/path.go | 44 +++--- internal/database/test_helpers.go | 2 +- internal/merger/merger.go | 8 +- internal/server/server.go | 46 +++--- internal/squawk/squawk.go | 28 ++-- internal/squawk/squawk_test.go | 56 ++++---- 19 files changed, 543 insertions(+), 527 deletions(-) diff --git a/assets/assets.go b/assets/assets.go index e80edaa..a4a923b 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -29,5 +29,6 @@ import "embed" // external file deployment or complicated asset management. // // Updated to include database.html for database status page +// //go:embed static var Static embed.FS diff --git a/cmd/skyview-data/main.go b/cmd/skyview-data/main.go index e7bb5f9..99496fa 100644 --- a/cmd/skyview-data/main.go +++ b/cmd/skyview-data/main.go @@ -19,28 +19,28 @@ import ( // Shared configuration structures (should match main skyview) type Config struct { - Server ServerConfig `json:"server"` - Sources []SourceConfig `json:"sources"` - Settings Settings `json:"settings"` - Database *database.Config `json:"database,omitempty"` - Callsign *CallsignConfig `json:"callsign,omitempty"` - Origin OriginConfig `json:"origin"` + Server ServerConfig `json:"server"` + Sources []SourceConfig `json:"sources"` + Settings Settings `json:"settings"` + Database *database.Config `json:"database,omitempty"` + Callsign *CallsignConfig `json:"callsign,omitempty"` + Origin OriginConfig `json:"origin"` } type CallsignConfig struct { - Enabled bool `json:"enabled"` - CacheHours int `json:"cache_hours"` - PrivacyMode bool `json:"privacy_mode"` - Sources map[string]CallsignSourceConfig `json:"sources"` - ExternalAPIs map[string]ExternalAPIConfig `json:"external_apis,omitempty"` + Enabled bool `json:"enabled"` + CacheHours int `json:"cache_hours"` + PrivacyMode bool `json:"privacy_mode"` + Sources map[string]CallsignSourceConfig `json:"sources"` + ExternalAPIs map[string]ExternalAPIConfig `json:"external_apis,omitempty"` } type CallsignSourceConfig struct { - Enabled bool `json:"enabled"` - Priority int `json:"priority"` - License string `json:"license"` - RequiresConsent bool `json:"requires_consent,omitempty"` - UserAcceptsTerms bool `json:"user_accepts_terms,omitempty"` + Enabled bool `json:"enabled"` + Priority int `json:"priority"` + License string `json:"license"` + RequiresConsent bool `json:"requires_consent,omitempty"` + UserAcceptsTerms bool `json:"user_accepts_terms,omitempty"` } type ExternalAPIConfig struct { @@ -51,14 +51,14 @@ type ExternalAPIConfig struct { } type OriginConfig struct { - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - Name string `json:"name,omitempty"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Name string `json:"name,omitempty"` } type ServerConfig struct { - Host string `json:"host"` - Port int `json:"port"` + Host string `json:"host"` + Port int `json:"port"` } type SourceConfig struct { @@ -152,7 +152,7 @@ OPTIONS: if err != nil { log.Fatalf("Configuration loading failed: %v", err) } - + // Initialize database connection using shared config db, err := initDatabaseFromConfig(config, *dbPath) if err != nil { @@ -215,7 +215,7 @@ func initDatabase(dbPath string) (*database.Database, error) { // cmdInit initializes an empty database func cmdInit(db *database.Database, force bool) error { dbPath := db.GetConfig().Path - + // Check if database already exists and has data if !force { if stats, err := db.GetHistoryManager().GetStatistics(); err == nil { @@ -271,19 +271,19 @@ func cmdList(db *database.Database) error { func cmdStatus(db *database.Database) error { fmt.Println("SkyView Database Status") fmt.Println("======================") - + dbPath := db.GetConfig().Path fmt.Printf("Database: %s\n", dbPath) - + // Check if file exists and get size if stat, err := os.Stat(dbPath); err == nil { fmt.Printf("Size: %.2f MB\n", float64(stat.Size())/(1024*1024)) fmt.Printf("Modified: %s\n", stat.ModTime().Format(time.RFC3339)) - + // Add database optimization stats optimizer := database.NewOptimizationManager(db, db.GetConfig()) 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) if stats.AutoVacuumEnabled { fmt.Printf("Auto-VACUUM: Enabled\n") @@ -320,7 +320,7 @@ func cmdStatus(db *database.Database) error { var airportCount, airlineCount int db.GetConnection().QueryRow(`SELECT COUNT(*) FROM airports`).Scan(&airportCount) db.GetConnection().QueryRow(`SELECT COUNT(*) FROM airlines`).Scan(&airlineCount) - + // Get data source update information var lastUpdate time.Time var updateCount int @@ -329,7 +329,7 @@ func cmdStatus(db *database.Database) error { FROM data_sources WHERE imported_at IS NOT NULL `).Scan(&updateCount, &lastUpdate) - + fmt.Printf("šŸ“Š Database Statistics:\n") fmt.Printf(" Reference Data:\n") 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(" Flight History:\n") if totalRecords, ok := stats["total_records"].(int); ok { 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 oldest, ok := oldestRecord.(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"), newest.Format("2006-01-02")) } } } - + // Show airport data sample if available if airportCount > 0 { var sampleAirports []string @@ -398,7 +398,7 @@ func cmdStatus(db *database.Database) error { // cmdUpdate updates data from specified sources (or safe sources by default) func cmdUpdate(db *database.Database, sources []string, force bool) error { availableSources := database.GetAvailableDataSources() - + // If no sources specified, use safe (non-consent-required) sources if len(sources) == 0 { 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, " ", ""))) } } - + if len(sources) == 0 { log.Println("No safe data sources available for automatic update") return nil @@ -416,7 +416,7 @@ func cmdUpdate(db *database.Database, sources []string, force bool) error { } loader := database.NewDataLoader(db.GetConnection()) - + for _, sourceName := range sources { // Find matching source var matchedSource *database.DataSource @@ -426,13 +426,13 @@ func cmdUpdate(db *database.Database, sources []string, force bool) error { break } } - + if matchedSource == nil { log.Printf("āš ļø Unknown source: %s", sourceName) continue } - // Check for consent requirement + // Check for consent requirement if matchedSource.RequiresConsent && !force { 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) @@ -446,7 +446,7 @@ func cmdUpdate(db *database.Database, sources []string, force bool) error { } log.Printf("Loading %s...", matchedSource.Name) - + result, err := loader.LoadDataSource(*matchedSource) if err != nil { 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) - + if len(result.Errors) > 0 { log.Printf(" %d errors occurred during import (first few):", len(result.Errors)) for i, errMsg := range result.Errors { - if i >= 3 { break } + if i >= 3 { + break + } 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 func cmdImport(db *database.Database, sourceName string, force bool) error { availableSources := database.GetAvailableDataSources() - + var matchedSource *database.DataSource for _, available := range availableSources { if strings.EqualFold(strings.ReplaceAll(available.Name, " ", ""), sourceName) { @@ -479,7 +481,7 @@ func cmdImport(db *database.Database, sourceName string, force bool) error { break } } - + if matchedSource == nil { 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.Println() 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?") { fmt.Println("Import cancelled.") return nil } - + matchedSource.UserAcceptedLicense = true } fmt.Printf("šŸ“„ Importing %s...\n", matchedSource.Name) - + loader := database.NewDataLoader(db.GetConnection()) result, err := loader.LoadDataSource(*matchedSource) if err != nil { @@ -550,10 +552,10 @@ func cmdReset(db *database.Database, force bool) error { // askForConfirmation asks the user for yes/no confirmation func askForConfirmation(question string) bool { fmt.Printf("%s (y/N): ", question) - + var response string fmt.Scanln(&response) - + response = strings.ToLower(strings.TrimSpace(response)) return response == "y" || response == "yes" } @@ -564,30 +566,30 @@ func loadConfig(configPath string) (*Config, error) { if err != nil { return nil, fmt.Errorf("failed to read config file %s: %w", configPath, err) } - + var config Config if err := json.Unmarshal(data, &config); err != nil { return nil, fmt.Errorf("failed to parse config file %s: %w", configPath, err) } - + return &config, nil } // initDatabaseFromConfig initializes database using shared configuration func initDatabaseFromConfig(config *Config, dbPathOverride string) (*database.Database, error) { var dbConfig *database.Config - + if config.Database != nil { dbConfig = config.Database } else { dbConfig = database.DefaultConfig() } - + // Allow command-line override of database path if dbPathOverride != "" { dbConfig.Path = dbPathOverride } - + // Resolve database path if empty if dbConfig.Path == "" { resolvedPath, err := database.ResolveDatabasePath(dbConfig.Path) @@ -596,18 +598,18 @@ func initDatabaseFromConfig(config *Config, dbPathOverride string) (*database.Da } dbConfig.Path = resolvedPath } - + // Create and initialize database db, err := database.NewDatabase(dbConfig) if err != nil { return nil, fmt.Errorf("failed to create database: %w", err) } - + if err := db.Initialize(); err != nil { db.Close() return nil, fmt.Errorf("failed to initialize database: %w", err) } - + return db, nil } @@ -615,23 +617,23 @@ func initDatabaseFromConfig(config *Config, dbPathOverride string) (*database.Da func cmdOptimize(db *database.Database, force bool) error { fmt.Println("Database Storage Optimization") fmt.Println("============================") - + // We need to get the database path from the config // For now, let's create a simple optimization manager config := &database.Config{ Path: "./dev-skyview.db", // Default path - this should be configurable } - + // Create optimization manager optimizer := database.NewOptimizationManager(db, config) - + // Get current stats fmt.Println("šŸ“Š Current Database Statistics:") stats, err := optimizer.GetOptimizationStats() if err != nil { return fmt.Errorf("failed to get database stats: %w", err) } - + fmt.Printf(" • Size: %.1f MB\n", float64(stats.DatabaseSize)/(1024*1024)) fmt.Printf(" • Page Size: %d bytes\n", stats.PageSize) 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(" • Efficiency: %.1f%%\n", stats.Efficiency) fmt.Printf(" • Auto VACUUM: %v\n", stats.AutoVacuumEnabled) - + // Check if optimization is needed needsOptimization := stats.FreePages > 0 || stats.Efficiency < 95.0 - + if !needsOptimization && !force { fmt.Println("āœ… Database is already well optimized!") fmt.Println(" Use --force to run optimization anyway") return nil } - + // Perform optimizations if force && !needsOptimization { fmt.Println("\nšŸ”§ Force optimization requested:") } else { fmt.Println("\nšŸ”§ Applying Optimizations:") } - + if err := optimizer.VacuumDatabase(); err != nil { return fmt.Errorf("VACUUM failed: %w", err) } - + if err := optimizer.OptimizeDatabase(); err != nil { return fmt.Errorf("optimization failed: %w", err) } - + // Show final stats fmt.Println("\nšŸ“ˆ Final Statistics:") finalStats, err := optimizer.GetOptimizationStats() if err != nil { return fmt.Errorf("failed to get final stats: %w", err) } - + fmt.Printf(" • Size: %.1f MB\n", float64(finalStats.DatabaseSize)/(1024*1024)) fmt.Printf(" • Efficiency: %.1f%%\n", finalStats.Efficiency) fmt.Printf(" • Free Pages: %d\n", finalStats.FreePages) - + if stats.DatabaseSize > finalStats.DatabaseSize { saved := stats.DatabaseSize - finalStats.DatabaseSize fmt.Printf(" • Space Saved: %.1f MB\n", float64(saved)/(1024*1024)) } - + fmt.Println("\nāœ… Database optimization completed!") return nil } - diff --git a/internal/database/api_client.go b/internal/database/api_client.go index f7b7614..50c77b1 100644 --- a/internal/database/api_client.go +++ b/internal/database/api_client.go @@ -13,12 +13,12 @@ import ( type ExternalAPIClient struct { httpClient *http.Client mutex sync.RWMutex - + // Configuration timeout time.Duration maxRetries int userAgent string - + // Rate limiting lastRequest time.Time minInterval time.Duration @@ -32,28 +32,28 @@ type APIClientConfig struct { } type OpenSkyFlightInfo struct { - ICAO string `json:"icao"` - Callsign string `json:"callsign"` - Origin string `json:"origin"` - Destination string `json:"destination"` - FirstSeen time.Time `json:"first_seen"` - LastSeen time.Time `json:"last_seen"` - AircraftType string `json:"aircraft_type"` - Registration string `json:"registration"` - FlightNumber string `json:"flight_number"` - Airline string `json:"airline"` + ICAO string `json:"icao"` + Callsign string `json:"callsign"` + Origin string `json:"origin"` + Destination string `json:"destination"` + FirstSeen time.Time `json:"first_seen"` + LastSeen time.Time `json:"last_seen"` + AircraftType string `json:"aircraft_type"` + Registration string `json:"registration"` + FlightNumber string `json:"flight_number"` + Airline string `json:"airline"` } type APIError struct { - Operation string - StatusCode int - Message string - Retryable bool - RetryAfter time.Duration + Operation string + StatusCode int + Message string + Retryable bool + RetryAfter time.Duration } 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) } @@ -70,7 +70,7 @@ func NewExternalAPIClient(config APIClientConfig) *ExternalAPIClient { if config.MinInterval == 0 { config.MinInterval = 1 * time.Second // Default rate limit } - + return &ExternalAPIClient{ httpClient: &http.Client{ Timeout: config.Timeout, @@ -85,7 +85,7 @@ func NewExternalAPIClient(config APIClientConfig) *ExternalAPIClient { func (c *ExternalAPIClient) enforceRateLimit() { c.mutex.Lock() defer c.mutex.Unlock() - + elapsed := time.Since(c.lastRequest) if elapsed < c.minInterval { 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) { c.enforceRateLimit() - + req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } - + req.Header.Set("User-Agent", c.userAgent) req.Header.Set("Accept", "application/json") - + var resp *http.Response var lastErr error - + for attempt := 0; attempt <= c.maxRetries; attempt++ { if attempt > 0 { // Exponential backoff @@ -117,16 +117,16 @@ func (c *ExternalAPIClient) makeRequest(ctx context.Context, url string) (*http. case <-time.After(backoff): } } - + resp, lastErr = c.httpClient.Do(req) if lastErr != nil { continue } - + // Check for retryable status codes if resp.StatusCode >= 500 || resp.StatusCode == 429 { resp.Body.Close() - + // Handle rate limiting if resp.StatusCode == 429 { retryAfter := parseRetryAfter(resp.Header.Get("Retry-After")) @@ -140,15 +140,15 @@ func (c *ExternalAPIClient) makeRequest(ctx context.Context, url string) (*http. } continue } - + // Success or non-retryable error break } - + if lastErr != nil { return nil, lastErr } - + return resp, nil } @@ -156,14 +156,14 @@ func (c *ExternalAPIClient) GetFlightInfoFromOpenSky(ctx context.Context, icao s if icao == "" { return nil, fmt.Errorf("empty ICAO code") } - + // OpenSky Network API endpoint for flight information apiURL := fmt.Sprintf("https://opensky-network.org/api/flights/aircraft?icao24=%s&begin=%d&end=%d", icao, time.Now().Add(-24*time.Hour).Unix(), time.Now().Unix(), ) - + resp, err := c.makeRequest(ctx, apiURL) if err != nil { return nil, &APIError{ @@ -173,7 +173,7 @@ func (c *ExternalAPIClient) GetFlightInfoFromOpenSky(ctx context.Context, icao s } } defer resp.Body.Close() - + if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, &APIError{ @@ -183,7 +183,7 @@ func (c *ExternalAPIClient) GetFlightInfoFromOpenSky(ctx context.Context, icao s Retryable: resp.StatusCode >= 500 || resp.StatusCode == 429, } } - + var flights [][]interface{} decoder := json.NewDecoder(resp.Body) if err := decoder.Decode(&flights); err != nil { @@ -193,11 +193,11 @@ func (c *ExternalAPIClient) GetFlightInfoFromOpenSky(ctx context.Context, icao s Retryable: false, } } - + if len(flights) == 0 { return nil, nil // No flight information available } - + // Parse the most recent flight flight := flights[0] if len(flight) < 10 { @@ -207,11 +207,11 @@ func (c *ExternalAPIClient) GetFlightInfoFromOpenSky(ctx context.Context, icao s Retryable: false, } } - + info := &OpenSkyFlightInfo{ ICAO: icao, } - + // Parse fields based on OpenSky API documentation if callsign, ok := flight[1].(string); ok { info.Callsign = callsign @@ -228,7 +228,7 @@ func (c *ExternalAPIClient) GetFlightInfoFromOpenSky(ctx context.Context, icao s if destination, ok := flight[5].(string); ok { info.Destination = destination } - + return info, nil } @@ -236,10 +236,10 @@ func (c *ExternalAPIClient) GetAircraftInfoFromOpenSky(ctx context.Context, icao if icao == "" { return nil, fmt.Errorf("empty ICAO code") } - + // OpenSky Network metadata API apiURL := fmt.Sprintf("https://opensky-network.org/api/metadata/aircraft/icao/%s", icao) - + resp, err := c.makeRequest(ctx, apiURL) if err != nil { return nil, &APIError{ @@ -249,11 +249,11 @@ func (c *ExternalAPIClient) GetAircraftInfoFromOpenSky(ctx context.Context, icao } } defer resp.Body.Close() - + if resp.StatusCode == http.StatusNotFound { return nil, nil // Aircraft not found } - + if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) return nil, &APIError{ @@ -263,7 +263,7 @@ func (c *ExternalAPIClient) GetAircraftInfoFromOpenSky(ctx context.Context, icao Retryable: resp.StatusCode >= 500 || resp.StatusCode == 429, } } - + var aircraft map[string]interface{} decoder := json.NewDecoder(resp.Body) if err := decoder.Decode(&aircraft); err != nil { @@ -273,7 +273,7 @@ func (c *ExternalAPIClient) GetAircraftInfoFromOpenSky(ctx context.Context, icao Retryable: false, } } - + return aircraft, nil } @@ -282,7 +282,7 @@ func (c *ExternalAPIClient) EnhanceCallsignWithExternalData(ctx context.Context, enhancement["callsign"] = callsign enhancement["icao"] = icao enhancement["enhanced"] = false - + // Try to get flight information from OpenSky if flightInfo, err := c.GetFlightInfoFromOpenSky(ctx, icao); err == nil && flightInfo != nil { enhancement["flight_info"] = map[string]interface{}{ @@ -295,53 +295,53 @@ func (c *ExternalAPIClient) EnhanceCallsignWithExternalData(ctx context.Context, } enhancement["enhanced"] = true } - + // Try to get aircraft metadata if aircraftInfo, err := c.GetAircraftInfoFromOpenSky(ctx, icao); err == nil && aircraftInfo != nil { enhancement["aircraft_info"] = aircraftInfo enhancement["enhanced"] = true } - + return enhancement, nil } func (c *ExternalAPIClient) BatchEnhanceCallsigns(ctx context.Context, callsigns map[string]string) (map[string]map[string]interface{}, error) { results := make(map[string]map[string]interface{}) - + for callsign, icao := range callsigns { select { case <-ctx.Done(): return results, ctx.Err() default: } - + enhanced, err := c.EnhanceCallsignWithExternalData(ctx, callsign, icao) if err != nil { // Log error but continue with other callsigns fmt.Printf("Warning: failed to enhance callsign %s (ICAO: %s): %v\n", callsign, icao, err) continue } - + results[callsign] = enhanced } - + return results, nil } func (c *ExternalAPIClient) TestConnection(ctx context.Context) error { // Test with a simple API call testURL := "https://opensky-network.org/api/states?time=0&lamin=0&lomin=0&lamax=1&lomax=1" - + resp, err := c.makeRequest(ctx, testURL) if err != nil { return fmt.Errorf("connection test failed: %w", err) } defer resp.Body.Close() - + if resp.StatusCode != http.StatusOK { return fmt.Errorf("connection test returned status %d", resp.StatusCode) } - + return nil } @@ -349,24 +349,24 @@ func parseRetryAfter(header string) time.Duration { if header == "" { return 0 } - + // Try parsing as seconds if seconds, err := time.ParseDuration(header + "s"); err == nil { return seconds } - + // Try parsing as HTTP date if t, err := http.ParseTime(header); err == nil { return time.Until(t) } - + return 0 } // HealthCheck provides information about the client's health func (c *ExternalAPIClient) HealthCheck(ctx context.Context) map[string]interface{} { health := make(map[string]interface{}) - + // Test connection if err := c.TestConnection(ctx); err != nil { health["status"] = "unhealthy" @@ -374,16 +374,16 @@ func (c *ExternalAPIClient) HealthCheck(ctx context.Context) map[string]interfac } else { health["status"] = "healthy" } - + // Add configuration info health["timeout"] = c.timeout.String() health["max_retries"] = c.maxRetries health["min_interval"] = c.minInterval.String() health["user_agent"] = c.userAgent - + c.mutex.RLock() health["last_request"] = c.lastRequest c.mutex.RUnlock() - + return health -} \ No newline at end of file +} diff --git a/internal/database/database.go b/internal/database/database.go index 108a0d9..bcbb312 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -19,35 +19,35 @@ import ( // Database represents the main database connection and operations type Database struct { - conn *sql.DB - config *Config - migrator *Migrator - callsign *CallsignManager - history *HistoryManager + conn *sql.DB + config *Config + migrator *Migrator + callsign *CallsignManager + history *HistoryManager } // Config holds database configuration options type Config struct { // Database file path (auto-resolved if empty) Path string `json:"path"` - + // 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"` - + // Connection settings MaxOpenConns int `json:"max_open_conns"` // Default: 10 MaxIdleConns int `json:"max_idle_conns"` // Default: 5 ConnMaxLifetime time.Duration `json:"conn_max_lifetime"` // Default: 1 hour - + // 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 - + // Compression settings - EnableCompression bool `json:"enable_compression"` // Enable automatic compression - CompressionLevel int `json:"compression_level"` // Compression level (1-9, default: 6) - PageSize int `json:"page_size"` // SQLite page size (default: 4096) + EnableCompression bool `json:"enable_compression"` // Enable automatic compression + CompressionLevel int `json:"compression_level"` // Compression level (1-9, default: 6) + PageSize int `json:"page_size"` // SQLite page size (default: 4096) } // AircraftHistoryRecord represents a stored aircraft position update @@ -93,18 +93,18 @@ type AirlineRecord struct { // AirportRecord represents embedded airport data from OpenFlights type AirportRecord struct { - ID int `json:"id"` - Name string `json:"name"` - City string `json:"city"` - Country string `json:"country"` - IATA string `json:"iata"` - ICAO string `json:"icao"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - Altitude int `json:"altitude"` + ID int `json:"id"` + Name string `json:"name"` + City string `json:"city"` + Country string `json:"country"` + IATA string `json:"iata"` + ICAO string `json:"icao"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Altitude int `json:"altitude"` TimezoneOffset float64 `json:"timezone_offset"` - DST string `json:"dst"` - Timezone string `json:"timezone"` + DST string `json:"dst"` + Timezone string `json:"timezone"` } // DatabaseError represents database operation errors @@ -131,7 +131,7 @@ func NewDatabase(config *Config) (*Database, error) { if config == nil { config = DefaultConfig() } - + // Resolve database path dbPath, err := ResolveDatabasePath(config.Path) if err != nil { @@ -142,7 +142,7 @@ func NewDatabase(config *Config) (*Database, error) { } } config.Path = dbPath - + // Open database connection conn, err := sql.Open("sqlite3", buildConnectionString(dbPath)) if err != nil { @@ -152,12 +152,12 @@ func NewDatabase(config *Config) (*Database, error) { Retryable: true, } } - + // Configure connection pool conn.SetMaxOpenConns(config.MaxOpenConns) conn.SetMaxIdleConns(config.MaxIdleConns) conn.SetConnMaxLifetime(config.ConnMaxLifetime) - + // Test connection if err := conn.Ping(); err != nil { conn.Close() @@ -167,17 +167,17 @@ func NewDatabase(config *Config) (*Database, error) { Retryable: true, } } - + db := &Database{ conn: conn, config: config, } - + // Initialize components db.migrator = NewMigrator(conn) db.callsign = NewCallsignManager(conn) db.history = NewHistoryManager(conn, config.MaxHistoryDays) - + return db, nil } @@ -191,7 +191,7 @@ func (db *Database) Initialize() error { Retryable: false, } } - + // Load embedded OpenFlights data if not already loaded if err := db.callsign.LoadEmbeddedData(); err != nil { return &DatabaseError{ @@ -200,7 +200,7 @@ func (db *Database) Initialize() error { Retryable: false, } } - + return nil } @@ -240,7 +240,6 @@ func (db *Database) Health() error { return db.conn.Ping() } - // DefaultConfig returns the default database configuration func DefaultConfig() *Config { return &Config{ @@ -258,4 +257,4 @@ func DefaultConfig() *Config { // buildConnectionString creates SQLite connection string with optimizations func buildConnectionString(path string) string { return fmt.Sprintf("%s?_journal_mode=WAL&_synchronous=NORMAL&_cache_size=-64000&_temp_store=MEMORY&_foreign_keys=ON", path) -} \ No newline at end of file +} diff --git a/internal/database/database_test.go b/internal/database/database_test.go index 4e6d21b..3357ed3 100644 --- a/internal/database/database_test.go +++ b/internal/database/database_test.go @@ -164,4 +164,4 @@ func TestDatabasePragmas(t *testing.T) { if journalMode != "wal" { t.Errorf("Expected WAL journal mode, got: %s", journalMode) } -} \ No newline at end of file +} diff --git a/internal/database/loader.go b/internal/database/loader.go index 6d31dfd..66b3803 100644 --- a/internal/database/loader.go +++ b/internal/database/loader.go @@ -37,32 +37,32 @@ type DataSource struct { // LoadResult contains the results of a data loading operation type LoadResult struct { - Source string `json:"source"` - RecordsTotal int `json:"records_total"` - RecordsNew int `json:"records_new"` - RecordsError int `json:"records_error"` + Source string `json:"source"` + RecordsTotal int `json:"records_total"` + RecordsNew int `json:"records_new"` + RecordsError int `json:"records_error"` Duration time.Duration `json:"duration"` - Errors []string `json:"errors,omitempty"` + Errors []string `json:"errors,omitempty"` } // NewDataLoader creates a new data loader with HTTP client func NewDataLoader(conn *sql.DB) *DataLoader { // Check for insecure TLS environment variable insecureTLS := os.Getenv("SKYVIEW_INSECURE_TLS") == "1" - + transport := &http.Transport{ MaxIdleConns: 10, IdleConnTimeout: 90 * time.Second, DisableCompression: false, } - + // Allow insecure certificates if requested if insecureTLS { transport.TLSClientConfig = &tls.Config{ InsecureSkipVerify: true, } } - + return &DataLoader{ conn: conn, client: &http.Client{ @@ -85,7 +85,7 @@ func GetAvailableDataSources() []DataSource { }, { Name: "OpenFlights Airports", - License: "AGPL-3.0", + License: "AGPL-3.0", URL: "https://raw.githubusercontent.com/jpatokal/openflights/master/data/airports.dat", RequiresConsent: false, // Runtime data consumption doesn't require explicit consent Format: "openflights", @@ -111,23 +111,23 @@ func (dl *DataLoader) LoadDataSource(source DataSource) (*LoadResult, error) { defer func() { result.Duration = time.Since(startTime) }() - + // Check license acceptance if required if source.RequiresConsent && !source.UserAcceptedLicense { return nil, fmt.Errorf("user has not accepted license for source: %s (%s)", source.Name, source.License) } - + // Download data resp, err := dl.client.Get(source.URL) if err != nil { return nil, fmt.Errorf("failed to download data from %s: %v", source.URL, err) } defer resp.Body.Close() - + if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("HTTP error downloading data: %s", resp.Status) } - + // Parse and load data based on format switch source.Format { case "openflights": @@ -137,10 +137,10 @@ func (dl *DataLoader) LoadDataSource(source DataSource) (*LoadResult, error) { return dl.loadOpenFlightsAirports(resp.Body, source, result) } return nil, fmt.Errorf("unknown OpenFlights data type: %s", source.Name) - + case "ourairports": return dl.loadOurAirports(resp.Body, source, result) - + default: 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) } defer tx.Rollback() - + // Record data source if err := dl.recordDataSource(tx, source); err != nil { return nil, err } - + // Clear existing data from this source _, err = tx.Exec(`DELETE FROM airlines WHERE data_source = ?`, source.Name) if err != nil { return nil, fmt.Errorf("failed to clear existing airline data: %v", err) } - + csvReader := csv.NewReader(reader) csvReader.FieldsPerRecord = -1 // Variable number of fields - + insertStmt, err := tx.Prepare(` INSERT OR REPLACE INTO airlines (id, name, alias, iata_code, icao_code, callsign, country, active, data_source) 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) } defer insertStmt.Close() - + for { record, err := csvReader.Read() 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)) continue } - + if len(record) < 7 { result.RecordsError++ result.Errors = append(result.Errors, "insufficient fields in record") continue } - + result.RecordsTotal++ - + // Parse OpenFlights airline format: // ID, Name, Alias, IATA, ICAO, Callsign, Country, Active id, _ := strconv.Atoi(record[0]) @@ -206,29 +206,37 @@ func (dl *DataLoader) loadOpenFlightsAirlines(reader io.Reader, source DataSourc callsign := strings.Trim(record[5], `"`) country := strings.Trim(record[6], `"`) active := len(record) > 7 && strings.Trim(record[7], `"`) == "Y" - + // Convert \N to empty strings - if alias == "\\N" { alias = "" } - if iata == "\\N" { iata = "" } - if icao == "\\N" { icao = "" } - if callsign == "\\N" { callsign = "" } - + if alias == "\\N" { + alias = "" + } + 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) if err != nil { result.RecordsError++ result.Errors = append(result.Errors, fmt.Sprintf("insert error for airline %s: %v", name, err)) continue } - + result.RecordsNew++ } - + // Update record count _, err = tx.Exec(`UPDATE data_sources SET record_count = ? WHERE name = ?`, result.RecordsNew, source.Name) if err != nil { return nil, fmt.Errorf("failed to update record count: %v", err) } - + 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) } defer tx.Rollback() - + // Record data source if err := dl.recordDataSource(tx, source); err != nil { return nil, err } - + // Clear existing data from this source _, err = tx.Exec(`DELETE FROM airports WHERE data_source = ?`, source.Name) if err != nil { return nil, fmt.Errorf("failed to clear existing airport data: %v", err) } - + csvReader := csv.NewReader(reader) csvReader.FieldsPerRecord = -1 - + insertStmt, err := tx.Prepare(` INSERT OR REPLACE INTO airports (id, name, city, country, iata_code, icao_code, latitude, longitude, 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) } defer insertStmt.Close() - + for { record, err := csvReader.Read() 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)) continue } - + if len(record) < 12 { result.RecordsError++ result.Errors = append(result.Errors, "insufficient fields in airport record") continue } - + result.RecordsTotal++ - + // Parse OpenFlights airport format id, _ := strconv.Atoi(record[0]) name := strings.Trim(record[1], `"`) @@ -296,29 +304,37 @@ func (dl *DataLoader) loadOpenFlightsAirports(reader io.Reader, source DataSourc tzOffset, _ := strconv.ParseFloat(record[9], 64) dst := strings.Trim(record[10], `"`) timezone := strings.Trim(record[11], `"`) - + // Convert \N to empty strings - if iata == "\\N" { iata = "" } - if icao == "\\N" { icao = "" } - if dst == "\\N" { dst = "" } - if timezone == "\\N" { timezone = "" } - + if iata == "\\N" { + iata = "" + } + 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) if err != nil { result.RecordsError++ result.Errors = append(result.Errors, fmt.Sprintf("insert error for airport %s: %v", name, err)) continue } - + result.RecordsNew++ } - + // Update record count _, err = tx.Exec(`UPDATE data_sources SET record_count = ? WHERE name = ?`, result.RecordsNew, source.Name) if err != nil { return nil, fmt.Errorf("failed to update record count: %v", err) } - + 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) } defer tx.Rollback() - + csvReader := csv.NewReader(reader) - + // Read header row headers, err := csvReader.Read() 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)} return result, err } - + // Create header index map for easier field access headerIndex := make(map[string]int) for i, header := range headers { headerIndex[strings.TrimSpace(header)] = i } - + // Prepare statement for airports stmt, err := tx.Prepare(` INSERT OR REPLACE INTO airports ( @@ -362,7 +378,7 @@ func (dl *DataLoader) loadOurAirports(reader io.Reader, source DataSource, resul return result, err } defer stmt.Close() - + // Process each row for { 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)) continue } - + // Skip rows with insufficient fields if len(record) < len(headerIndex) { result.RecordsError++ continue } - + // Extract fields using header index sourceID := getFieldByHeader(record, headerIndex, "id") 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") wikipediaLink := getFieldByHeader(record, headerIndex, "wikipedia_link") keywords := getFieldByHeader(record, headerIndex, "keywords") - + // Parse coordinates var latitude, longitude float64 if latStr := getFieldByHeader(record, headerIndex, "latitude_deg"); latStr != "" { @@ -407,7 +423,7 @@ func (dl *DataLoader) loadOurAirports(reader io.Reader, source DataSource, resul longitude = lng } } - + // Parse elevation var elevation int if elevStr := getFieldByHeader(record, headerIndex, "elevation_ft"); elevStr != "" { @@ -415,10 +431,10 @@ func (dl *DataLoader) loadOurAirports(reader io.Reader, source DataSource, resul elevation = elev } } - + // Parse scheduled service scheduledService := getFieldByHeader(record, headerIndex, "scheduled_service") == "yes" - + // Insert airport record _, err = stmt.Exec( sourceID, name, ident, airportType, icaoCode, iataCode, @@ -432,7 +448,7 @@ func (dl *DataLoader) loadOurAirports(reader io.Reader, source DataSource, resul result.RecordsNew++ } } - + // Update data source tracking _, err = tx.Exec(` 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 { return result, fmt.Errorf("failed to update data source tracking: %v", err) } - + return result, tx.Commit() } @@ -460,13 +476,13 @@ func (dl *DataLoader) GetLoadedDataSources() ([]DataSource, error) { FROM data_sources ORDER BY name ` - + rows, err := dl.conn.Query(query) if err != nil { return nil, err } defer rows.Close() - + var sources []DataSource for rows.Next() { var source DataSource @@ -482,7 +498,7 @@ func (dl *DataLoader) GetLoadedDataSources() ([]DataSource, error) { } sources = append(sources, source) } - + 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) VALUES (?, ?, ?, ?, ?) `, source.Name, source.License, source.URL, source.Version, source.UserAcceptedLicense) - + return err } - // ClearDataSource removes all data from a specific source func (dl *DataLoader) ClearDataSource(sourceName string) error { 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) } defer tx.Rollback() - + // Clear from all tables _, err = tx.Exec(`DELETE FROM airlines WHERE data_source = ?`, sourceName) if err != nil { return fmt.Errorf("failed to clear airlines: %v", err) } - + _, err = tx.Exec(`DELETE FROM airports WHERE data_source = ?`, sourceName) if err != nil { return fmt.Errorf("failed to clear airports: %v", err) } - + _, err = tx.Exec(`DELETE FROM data_sources WHERE name = ?`, sourceName) if err != nil { return fmt.Errorf("failed to clear data source record: %v", err) } - + return tx.Commit() -} \ No newline at end of file +} diff --git a/internal/database/loader_test.go b/internal/database/loader_test.go index 9bfebe8..8fbb0f5 100644 --- a/internal/database/loader_test.go +++ b/internal/database/loader_test.go @@ -33,9 +33,9 @@ func TestDataLoader_LoadOpenFlightsAirlines(t *testing.T) { result, err := loader.LoadDataSource(source) if err != nil { // Network issues in tests are acceptable - if strings.Contains(err.Error(), "connection") || - strings.Contains(err.Error(), "timeout") || - strings.Contains(err.Error(), "no such host") { + if strings.Contains(err.Error(), "connection") || + strings.Contains(err.Error(), "timeout") || + strings.Contains(err.Error(), "no such host") { t.Skipf("Skipping network test due to connectivity issue: %v", err) } t.Fatal("LoadDataSource failed:", err) @@ -45,7 +45,7 @@ func TestDataLoader_LoadOpenFlightsAirlines(t *testing.T) { 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) // Verify some data was processed @@ -72,16 +72,16 @@ func TestDataLoader_LoadOurAirports(t *testing.T) { result, err := loader.LoadDataSource(source) if err != nil { // Network issues in tests are acceptable - if strings.Contains(err.Error(), "connection") || - strings.Contains(err.Error(), "timeout") || - strings.Contains(err.Error(), "no such host") { + if strings.Contains(err.Error(), "connection") || + strings.Contains(err.Error(), "timeout") || + strings.Contains(err.Error(), "no such host") { t.Skipf("Skipping network test due to connectivity issue: %v", err) } t.Fatal("LoadDataSource failed:", err) } 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) } } @@ -174,4 +174,4 @@ func TestLoadResult_Struct(t *testing.T) { if len(result.Errors) != 2 { t.Error("Errors field not preserved") } -} \ No newline at end of file +} diff --git a/internal/database/manager_callsign.go b/internal/database/manager_callsign.go index 2f563c7..67b511f 100644 --- a/internal/database/manager_callsign.go +++ b/internal/database/manager_callsign.go @@ -12,7 +12,7 @@ import ( type CallsignManager struct { db *sql.DB mutex sync.RWMutex - + // Compiled regex patterns for callsign parsing airlinePattern *regexp.Regexp flightPattern *regexp.Regexp @@ -42,14 +42,14 @@ func (cm *CallsignManager) ParseCallsign(callsign string) *CallsignParseResult { ParsedTime: time.Now(), IsValid: false, } - + if callsign == "" { return result } - + // Clean and normalize the callsign normalized := strings.TrimSpace(strings.ToUpper(callsign)) - + // Try airline pattern first (most common for commercial flights) if matches := cm.airlinePattern.FindStringSubmatch(normalized); len(matches) == 3 { result.AirlineCode = matches[1] @@ -57,7 +57,7 @@ func (cm *CallsignManager) ParseCallsign(callsign string) *CallsignParseResult { result.IsValid = true return result } - + // Fall back to general flight pattern if matches := cm.flightPattern.FindStringSubmatch(normalized); len(matches) == 3 { result.AirlineCode = matches[1] @@ -65,24 +65,24 @@ func (cm *CallsignManager) ParseCallsign(callsign string) *CallsignParseResult { result.IsValid = true return result } - + return result } func (cm *CallsignManager) GetCallsignInfo(callsign string) (*CallsignInfo, error) { cm.mutex.RLock() defer cm.mutex.RUnlock() - + if callsign == "" { return nil, fmt.Errorf("empty callsign") } - + // First check the cache cached, err := cm.getCallsignFromCache(callsign) if err == nil && cached != nil { return cached, nil } - + // Parse the callsign parsed := cm.ParseCallsign(callsign) if !parsed.IsValid { @@ -91,13 +91,13 @@ func (cm *CallsignManager) GetCallsignInfo(callsign string) (*CallsignInfo, erro IsValid: false, }, nil } - + // Look up airline information airline, err := cm.getAirlineByCode(parsed.AirlineCode) if err != nil && err != sql.ErrNoRows { return nil, fmt.Errorf("failed to lookup airline %s: %w", parsed.AirlineCode, err) } - + // Build the result info := &CallsignInfo{ OriginalCallsign: callsign, @@ -106,7 +106,7 @@ func (cm *CallsignManager) GetCallsignInfo(callsign string) (*CallsignInfo, erro IsValid: true, LastUpdated: time.Now(), } - + if airline != nil { info.AirlineName = airline.Name info.AirlineCountry = airline.Country @@ -114,7 +114,7 @@ func (cm *CallsignManager) GetCallsignInfo(callsign string) (*CallsignInfo, erro } else { info.DisplayName = fmt.Sprintf("%s %s", parsed.AirlineCode, parsed.FlightNumber) } - + // Cache the result (fire and forget) go func() { 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) } }() - + return info, nil } @@ -133,10 +133,10 @@ func (cm *CallsignManager) getCallsignFromCache(callsign string) (*CallsignInfo, FROM callsign_cache WHERE callsign = ? AND expires_at > datetime('now') ` - + var info CallsignInfo var cacheExpires time.Time - + err := cm.db.QueryRow(query, callsign).Scan( &info.OriginalCallsign, &info.AirlineCode, @@ -148,25 +148,25 @@ func (cm *CallsignManager) getCallsignFromCache(callsign string) (*CallsignInfo, &info.LastUpdated, &cacheExpires, ) - + if err != nil { return nil, err } - + return &info, nil } func (cm *CallsignManager) cacheCallsignInfo(info *CallsignInfo) error { // Cache for 24 hours by default cacheExpires := time.Now().Add(24 * time.Hour) - + query := ` INSERT OR REPLACE INTO callsign_cache (callsign, airline_icao, flight_number, airline_name, airline_country, cached_at, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?) ` - + _, err := cm.db.Exec(query, info.OriginalCallsign, info.AirlineCode, @@ -176,7 +176,7 @@ func (cm *CallsignManager) cacheCallsignInfo(info *CallsignInfo) error { info.LastUpdated, cacheExpires, ) - + return err } @@ -190,7 +190,7 @@ func (cm *CallsignManager) getAirlineByCode(code string) (*AirlineRecord, error) name LIMIT 1 ` - + var airline AirlineRecord err := cm.db.QueryRow(query, code, code, code).Scan( &airline.ICAOCode, @@ -199,31 +199,31 @@ func (cm *CallsignManager) getAirlineByCode(code string) (*AirlineRecord, error) &airline.Country, &airline.Active, ) - + if err != nil { return nil, err } - + return &airline, nil } func (cm *CallsignManager) GetAirlinesByCountry(country string) ([]AirlineRecord, error) { cm.mutex.RLock() defer cm.mutex.RUnlock() - + query := ` SELECT icao_code, iata_code, name, country, active FROM airlines WHERE country = ? AND active = 1 ORDER BY name ` - + rows, err := cm.db.Query(query, country) if err != nil { return nil, err } defer rows.Close() - + var airlines []AirlineRecord for rows.Next() { var airline AirlineRecord @@ -239,14 +239,14 @@ func (cm *CallsignManager) GetAirlinesByCountry(country string) ([]AirlineRecord } airlines = append(airlines, airline) } - + return airlines, rows.Err() } func (cm *CallsignManager) SearchAirlines(query string) ([]AirlineRecord, error) { cm.mutex.RLock() defer cm.mutex.RUnlock() - + searchQuery := ` SELECT icao_code, iata_code, name, country, active FROM airlines @@ -265,11 +265,11 @@ func (cm *CallsignManager) SearchAirlines(query string) ([]AirlineRecord, error) name LIMIT 50 ` - + searchTerm := "%" + strings.ToUpper(query) + "%" exactTerm := strings.ToUpper(query) - - rows, err := cm.db.Query(searchQuery, + + rows, err := cm.db.Query(searchQuery, searchTerm, searchTerm, searchTerm, searchTerm, exactTerm, exactTerm, exactTerm, ) @@ -277,7 +277,7 @@ func (cm *CallsignManager) SearchAirlines(query string) ([]AirlineRecord, error) return nil, err } defer rows.Close() - + var airlines []AirlineRecord for rows.Next() { var airline AirlineRecord @@ -293,14 +293,14 @@ func (cm *CallsignManager) SearchAirlines(query string) ([]AirlineRecord, error) } airlines = append(airlines, airline) } - + return airlines, rows.Err() } func (cm *CallsignManager) ClearExpiredCache() error { cm.mutex.Lock() defer cm.mutex.Unlock() - + query := `DELETE FROM callsign_cache WHERE expires_at <= datetime('now')` _, err := cm.db.Exec(query) return err @@ -309,9 +309,9 @@ func (cm *CallsignManager) ClearExpiredCache() error { func (cm *CallsignManager) GetCacheStats() (map[string]interface{}, error) { cm.mutex.RLock() defer cm.mutex.RUnlock() - + stats := make(map[string]interface{}) - + // Total cached entries var totalCached int 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 } stats["total_cached"] = totalCached - + // Valid (non-expired) entries var validCached int 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 } stats["valid_cached"] = validCached - + // Expired entries stats["expired_cached"] = totalCached - validCached - + // Total airlines in database var totalAirlines int 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 } stats["total_airlines"] = totalAirlines - + return stats, nil } @@ -349,14 +349,14 @@ func (cm *CallsignManager) LoadEmbeddedData() error { if err != nil { return err } - + if count > 0 { // Data already loaded return nil } - + // For now, we'll implement this as a placeholder // In a full implementation, this would load embedded airline data // from embedded files or resources return nil -} \ No newline at end of file +} diff --git a/internal/database/manager_callsign_test.go b/internal/database/manager_callsign_test.go index 731f61a..06246a7 100644 --- a/internal/database/manager_callsign_test.go +++ b/internal/database/manager_callsign_test.go @@ -21,15 +21,15 @@ func TestCallsignManager_ParseCallsign(t *testing.T) { manager := NewCallsignManager(db.GetConnection()) testCases := []struct { - callsign string - expectedValid bool - expectedAirline string - expectedFlight string + callsign string + expectedValid bool + expectedAirline string + expectedFlight string }{ {"UAL123", true, "UAL", "123"}, {"BA4567", true, "BA", "4567"}, {"AFR89", true, "AFR", "89"}, - {"N123AB", false, "", ""}, // Aircraft registration, not callsign + {"N123AB", false, "", ""}, // Aircraft registration, not callsign {"INVALID", false, "", ""}, // No numbers {"123", false, "", ""}, // Only numbers {"A", false, "", ""}, // Too short @@ -39,15 +39,15 @@ func TestCallsignManager_ParseCallsign(t *testing.T) { for _, tc := range testCases { result := manager.ParseCallsign(tc.callsign) 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) } 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) } 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) } } @@ -101,7 +101,7 @@ func TestCallsignManager_GetCallsignInfo_InvalidCallsign(t *testing.T) { defer cleanup() manager := NewCallsignManager(db.GetConnection()) - + // Test with invalid callsign format info, err := manager.GetCallsignInfo("INVALID") if err != nil { @@ -129,7 +129,7 @@ func TestCallsignManager_GetCallsignInfo_EmptyCallsign(t *testing.T) { defer cleanup() manager := NewCallsignManager(db.GetConnection()) - + // Test with empty callsign info, err := manager.GetCallsignInfo("") if err == nil { @@ -162,7 +162,7 @@ func TestCallsignManager_GetCacheStats(t *testing.T) { if err != nil { t.Error("GetCacheStats should not error:", err) } - + if stats == nil { t.Error("Expected cache stats, got nil") } @@ -265,4 +265,4 @@ func TestCallsignParseResult_Struct(t *testing.T) { if !result.IsValid { t.Error("IsValid field not preserved") } -} \ No newline at end of file +} diff --git a/internal/database/manager_history.go b/internal/database/manager_history.go index 1121fa3..ea25f36 100644 --- a/internal/database/manager_history.go +++ b/internal/database/manager_history.go @@ -10,7 +10,7 @@ import ( type HistoryManager struct { db *sql.DB mutex sync.RWMutex - + // Configuration maxHistoryDays int cleanupTicker *time.Ticker @@ -23,11 +23,11 @@ func NewHistoryManager(db *sql.DB, maxHistoryDays int) *HistoryManager { maxHistoryDays: maxHistoryDays, stopCleanup: make(chan bool), } - + // Start periodic cleanup (every hour) hm.cleanupTicker = time.NewTicker(1 * time.Hour) go hm.periodicCleanup() - + return hm } @@ -56,14 +56,14 @@ func (hm *HistoryManager) periodicCleanup() { func (hm *HistoryManager) RecordAircraft(record *AircraftHistoryRecord) error { hm.mutex.Lock() defer hm.mutex.Unlock() - + query := ` INSERT INTO aircraft_history (icao, callsign, squawk, latitude, longitude, altitude, vertical_rate, speed, track, source_id, signal_strength, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` - + _, err := hm.db.Exec(query, record.ICAO, record.Callsign, @@ -78,7 +78,7 @@ func (hm *HistoryManager) RecordAircraft(record *AircraftHistoryRecord) error { record.SignalStrength, record.Timestamp, ) - + return err } @@ -86,16 +86,16 @@ func (hm *HistoryManager) RecordAircraftBatch(records []AircraftHistoryRecord) e if len(records) == 0 { return nil } - + hm.mutex.Lock() defer hm.mutex.Unlock() - + tx, err := hm.db.Begin() if err != nil { return err } defer tx.Rollback() - + stmt, err := tx.Prepare(` INSERT INTO aircraft_history (icao, callsign, squawk, latitude, longitude, altitude, @@ -106,7 +106,7 @@ func (hm *HistoryManager) RecordAircraftBatch(records []AircraftHistoryRecord) e return err } defer stmt.Close() - + for _, record := range records { _, err := stmt.Exec( 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 tx.Commit() } func (hm *HistoryManager) GetAircraftHistory(icao string, hours int) ([]AircraftHistoryRecord, error) { hm.mutex.RLock() defer hm.mutex.RUnlock() - + since := time.Now().Add(-time.Duration(hours) * time.Hour) - + query := ` SELECT icao, callsign, squawk, latitude, longitude, altitude, 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 LIMIT 1000 ` - + rows, err := hm.db.Query(query, icao, since) if err != nil { return nil, err } defer rows.Close() - + var records []AircraftHistoryRecord for rows.Next() { var record AircraftHistoryRecord @@ -173,16 +173,16 @@ func (hm *HistoryManager) GetAircraftHistory(icao string, hours int) ([]Aircraft } records = append(records, record) } - + return records, rows.Err() } func (hm *HistoryManager) GetAircraftTrack(icao string, hours int) ([]TrackPoint, error) { hm.mutex.RLock() defer hm.mutex.RUnlock() - + since := time.Now().Add(-time.Duration(hours) * time.Hour) - + query := ` SELECT latitude, longitude, altitude, timestamp FROM aircraft_history @@ -191,13 +191,13 @@ func (hm *HistoryManager) GetAircraftTrack(icao string, hours int) ([]TrackPoint ORDER BY timestamp ASC LIMIT 500 ` - + rows, err := hm.db.Query(query, icao, since) if err != nil { return nil, err } defer rows.Close() - + var track []TrackPoint for rows.Next() { var point TrackPoint @@ -212,16 +212,16 @@ func (hm *HistoryManager) GetAircraftTrack(icao string, hours int) ([]TrackPoint } track = append(track, point) } - + return track, rows.Err() } func (hm *HistoryManager) GetRecentAircraft(hours int, limit int) ([]string, error) { hm.mutex.RLock() defer hm.mutex.RUnlock() - + since := time.Now().Add(-time.Duration(hours) * time.Hour) - + query := ` SELECT DISTINCT icao FROM aircraft_history @@ -229,13 +229,13 @@ func (hm *HistoryManager) GetRecentAircraft(hours int, limit int) ([]string, err ORDER BY MAX(timestamp) DESC LIMIT ? ` - + rows, err := hm.db.Query(query, since, limit) if err != nil { return nil, err } defer rows.Close() - + var icaos []string for rows.Next() { var icao string @@ -245,20 +245,20 @@ func (hm *HistoryManager) GetRecentAircraft(hours int, limit int) ([]string, err } icaos = append(icaos, icao) } - + return icaos, rows.Err() } func (hm *HistoryManager) GetAircraftLastSeen(icao string) (time.Time, error) { hm.mutex.RLock() defer hm.mutex.RUnlock() - + query := ` SELECT MAX(timestamp) FROM aircraft_history WHERE icao = ? ` - + var lastSeen time.Time err := hm.db.QueryRow(query, icao).Scan(&lastSeen) return lastSeen, err @@ -267,24 +267,24 @@ func (hm *HistoryManager) GetAircraftLastSeen(icao string) (time.Time, error) { func (hm *HistoryManager) CleanupOldHistory() error { hm.mutex.Lock() defer hm.mutex.Unlock() - + if hm.maxHistoryDays <= 0 { return nil // No cleanup if maxHistoryDays is 0 or negative } - + cutoff := time.Now().AddDate(0, 0, -hm.maxHistoryDays) - + query := `DELETE FROM aircraft_history WHERE timestamp < ?` result, err := hm.db.Exec(query, cutoff) if err != nil { return err } - + rowsAffected, err := result.RowsAffected() if err == nil && rowsAffected > 0 { fmt.Printf("Cleaned up %d old aircraft history records\n", rowsAffected) } - + return nil } @@ -295,9 +295,9 @@ func (hm *HistoryManager) GetStatistics() (map[string]interface{}, error) { func (hm *HistoryManager) GetHistoryStats() (map[string]interface{}, error) { hm.mutex.RLock() defer hm.mutex.RUnlock() - + stats := make(map[string]interface{}) - + // Total records var totalRecords int 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 } stats["total_records"] = totalRecords - + // Unique aircraft var uniqueAircraft int 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 } stats["unique_aircraft"] = uniqueAircraft - + // Recent records (last 24 hours) var recentRecords int since := time.Now().Add(-24 * time.Hour) @@ -322,7 +322,7 @@ func (hm *HistoryManager) GetHistoryStats() (map[string]interface{}, error) { return nil, err } stats["recent_records_24h"] = recentRecords - + // Oldest and newest record timestamps (only if records exist) if totalRecords > 0 { 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) } } - + return stats, nil } func (hm *HistoryManager) GetActivitySummary(hours int) (map[string]interface{}, error) { hm.mutex.RLock() defer hm.mutex.RUnlock() - + since := time.Now().Add(-time.Duration(hours) * time.Hour) - + summary := make(map[string]interface{}) - + // Aircraft count in time period var aircraftCount int err := hm.db.QueryRow(` @@ -356,7 +356,7 @@ func (hm *HistoryManager) GetActivitySummary(hours int) (map[string]interface{}, return nil, err } summary["aircraft_count"] = aircraftCount - + // Message count in time period var messageCount int err = hm.db.QueryRow(` @@ -368,7 +368,7 @@ func (hm *HistoryManager) GetActivitySummary(hours int) (map[string]interface{}, return nil, err } summary["message_count"] = messageCount - + // Most active sources query := ` SELECT source_id, COUNT(*) as count @@ -378,13 +378,13 @@ func (hm *HistoryManager) GetActivitySummary(hours int) (map[string]interface{}, ORDER BY count DESC LIMIT 5 ` - + rows, err := hm.db.Query(query, since) if err != nil { return nil, err } defer rows.Close() - + sources := make([]map[string]interface{}, 0) for rows.Next() { var sourceID string @@ -399,7 +399,7 @@ func (hm *HistoryManager) GetActivitySummary(hours int) (map[string]interface{}, }) } summary["top_sources"] = sources - + return summary, nil } @@ -408,4 +408,4 @@ type TrackPoint struct { Longitude float64 `json:"longitude"` Altitude *int `json:"altitude,omitempty"` Timestamp time.Time `json:"timestamp"` -} \ No newline at end of file +} diff --git a/internal/database/migrations.go b/internal/database/migrations.go index e58fcbf..20ea20b 100644 --- a/internal/database/migrations.go +++ b/internal/database/migrations.go @@ -192,17 +192,17 @@ func GetMigrations() []Migration { }, // Future migrations will be added here } - + // Calculate checksums for i := range migrations { migrations[i].Checksum = calculateChecksum(migrations[i].Up) } - + // Sort by version sort.Slice(migrations, func(i, j int) bool { return migrations[i].Version < migrations[j].Version }) - + return migrations } @@ -212,19 +212,19 @@ func (m *Migrator) MigrateToLatest() error { if err != nil { return fmt.Errorf("failed to get current version: %v", err) } - + migrations := GetMigrations() - + for _, migration := range migrations { if migration.Version <= currentVersion { continue } - + if err := m.applyMigration(migration); err != nil { return fmt.Errorf("failed to apply migration %d: %v", migration.Version, err) } } - + return nil } @@ -234,20 +234,20 @@ func (m *Migrator) MigrateTo(targetVersion int) error { if err != nil { return fmt.Errorf("failed to get current version: %v", err) } - + if targetVersion == currentVersion { return nil // Already at target version } - + migrations := GetMigrations() - + if targetVersion > currentVersion { // Forward migration for _, migration := range migrations { if migration.Version <= currentVersion || migration.Version > targetVersion { continue } - + if err := m.applyMigration(migration); err != nil { 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 { return migrations[i].Version > migrations[j].Version }) - + for _, migration := range migrations { if migration.Version > currentVersion || migration.Version <= targetVersion { continue } - + if err := m.rollbackMigration(migration); err != nil { return fmt.Errorf("failed to rollback migration %d: %v", migration.Version, err) } } } - + return nil } @@ -279,19 +279,19 @@ func (m *Migrator) GetAppliedMigrations() ([]MigrationRecord, error) { if err := m.ensureSchemaInfoTable(); err != nil { return nil, err } - + query := ` SELECT version, description, applied_at, checksum FROM schema_info ORDER BY version ` - + rows, err := m.conn.Query(query) if err != nil { return nil, fmt.Errorf("failed to query applied migrations: %v", err) } defer rows.Close() - + var migrations []MigrationRecord for rows.Next() { var migration MigrationRecord @@ -306,7 +306,7 @@ func (m *Migrator) GetAppliedMigrations() ([]MigrationRecord, error) { } migrations = append(migrations, migration) } - + return migrations, nil } @@ -315,13 +315,13 @@ func (m *Migrator) getCurrentVersion() (int, error) { if err := m.ensureSchemaInfoTable(); err != nil { return 0, err } - + var version int err := m.conn.QueryRow(`SELECT COALESCE(MAX(version), 0) FROM schema_info`).Scan(&version) if err != nil { return 0, fmt.Errorf("failed to get current version: %v", err) } - + return version, nil } @@ -332,13 +332,13 @@ func (m *Migrator) applyMigration(migration Migration) error { return fmt.Errorf("failed to begin transaction: %v", err) } defer tx.Rollback() - + // Warn about data loss if migration.DataLoss { // In a real application, this would show a warning to the user // For now, we'll just log it } - + // Execute migration SQL statements := strings.Split(migration.Up, ";") for _, stmt := range statements { @@ -346,22 +346,22 @@ func (m *Migrator) applyMigration(migration Migration) error { if stmt == "" { continue } - + if _, err := tx.Exec(stmt); err != nil { return fmt.Errorf("failed to execute migration statement: %v", err) } } - + // Record migration _, err = tx.Exec(` INSERT INTO schema_info (version, description, checksum) VALUES (?, ?, ?) `, migration.Version, migration.Description, migration.Checksum) - + if err != nil { return fmt.Errorf("failed to record migration: %v", err) } - + return tx.Commit() } @@ -370,13 +370,13 @@ func (m *Migrator) rollbackMigration(migration Migration) error { if migration.Down == "" { return fmt.Errorf("migration %d has no rollback script", migration.Version) } - + tx, err := m.conn.Begin() if err != nil { return fmt.Errorf("failed to begin transaction: %v", err) } defer tx.Rollback() - + // Execute rollback SQL statements := strings.Split(migration.Down, ";") for _, stmt := range statements { @@ -384,18 +384,18 @@ func (m *Migrator) rollbackMigration(migration Migration) error { if stmt == "" { continue } - + if _, err := tx.Exec(stmt); err != nil { return fmt.Errorf("failed to execute rollback statement: %v", err) } } - + // Remove migration record _, err = tx.Exec(`DELETE FROM schema_info WHERE version = ?`, migration.Version) if err != nil { return fmt.Errorf("failed to remove migration record: %v", err) } - + return tx.Commit() } @@ -416,4 +416,4 @@ func (m *Migrator) ensureSchemaInfoTable() error { func calculateChecksum(content string) string { // Simple checksum - in production, use a proper hash function return fmt.Sprintf("%x", len(content)) -} \ No newline at end of file +} diff --git a/internal/database/optimization.go b/internal/database/optimization.go index abe322e..e55929c 100644 --- a/internal/database/optimization.go +++ b/internal/database/optimization.go @@ -24,7 +24,7 @@ func NewOptimizationManager(db *Database, config *Config) *OptimizationManager { // PerformMaintenance runs database maintenance tasks including VACUUM func (om *OptimizationManager) PerformMaintenance() error { now := time.Now() - + // Check if VACUUM is needed if om.config.VacuumInterval > 0 && now.Sub(om.lastVacuum) >= om.config.VacuumInterval { if err := om.VacuumDatabase(); err != nil { @@ -32,7 +32,7 @@ func (om *OptimizationManager) PerformMaintenance() error { } om.lastVacuum = now } - + return nil } @@ -42,37 +42,37 @@ func (om *OptimizationManager) VacuumDatabase() error { if conn == nil { return fmt.Errorf("database connection not available") } - + start := time.Now() - + // Get size before VACUUM sizeBefore, err := om.getDatabaseSize() if err != nil { return fmt.Errorf("failed to get database size: %w", err) } - + // Perform VACUUM if _, err := conn.Exec("VACUUM"); err != nil { return fmt.Errorf("VACUUM operation failed: %w", err) } - + // Get size after VACUUM sizeAfter, err := om.getDatabaseSize() if err != nil { return fmt.Errorf("failed to get database size after VACUUM: %w", err) } - + duration := time.Since(start) savedBytes := sizeBefore - sizeAfter savedPercent := float64(savedBytes) / float64(sizeBefore) * 100 - + fmt.Printf("VACUUM completed in %v: %.1f MB → %.1f MB (saved %.1f MB, %.1f%%)\n", duration, float64(sizeBefore)/(1024*1024), float64(sizeAfter)/(1024*1024), float64(savedBytes)/(1024*1024), savedPercent) - + return nil } @@ -82,13 +82,13 @@ func (om *OptimizationManager) OptimizeDatabase() error { if conn == nil { return fmt.Errorf("database connection not available") } - + fmt.Println("Optimizing database for storage efficiency...") - + // Apply storage-friendly pragmas - optimizations := []struct{ - name string - query string + optimizations := []struct { + name string + query string description string }{ {"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"}, {"Analyze", "ANALYZE", "Update table statistics for better query plans"}, } - + for _, opt := range optimizations { if _, err := conn.Exec(opt.query); err != nil { 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) } } - + return nil } @@ -114,83 +114,83 @@ func (om *OptimizationManager) OptimizePageSize(pageSize int) error { if conn == nil { return fmt.Errorf("database connection not available") } - + // Check current page size var currentPageSize int if err := conn.QueryRow("PRAGMA page_size").Scan(¤tPageSize); err != nil { return fmt.Errorf("failed to get current page size: %w", err) } - + if currentPageSize == pageSize { fmt.Printf("Page size already optimal: %d bytes\n", pageSize) return nil } - + fmt.Printf("Optimizing page size: %d → %d bytes (requires VACUUM)\n", currentPageSize, pageSize) - + // Set new page size query := fmt.Sprintf("PRAGMA page_size = %d", pageSize) if _, err := conn.Exec(query); err != nil { return fmt.Errorf("failed to set page size: %w", err) } - + // VACUUM to apply the new page size if err := om.VacuumDatabase(); err != nil { return fmt.Errorf("failed to apply page size change: %w", err) } - + return nil } // GetOptimizationStats returns current database optimization statistics func (om *OptimizationManager) GetOptimizationStats() (*OptimizationStats, error) { stats := &OptimizationStats{} - + // Get database size size, err := om.getDatabaseSize() if err != nil { return nil, err } stats.DatabaseSize = size - + // Get page statistics conn := om.db.GetConnection() if conn != nil { var pageSize, pageCount, freelistCount int 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) - + stats.PageSize = pageSize stats.PageCount = pageCount stats.FreePages = freelistCount stats.UsedPages = pageCount - freelistCount - + if pageCount > 0 { stats.Efficiency = float64(stats.UsedPages) / float64(pageCount) * 100 } - + // Check auto vacuum setting var autoVacuum int conn.QueryRow("PRAGMA auto_vacuum").Scan(&autoVacuum) stats.AutoVacuumEnabled = autoVacuum > 0 } - + stats.LastVacuum = om.lastVacuum - + return stats, nil } // OptimizationStats holds database storage optimization statistics type OptimizationStats struct { DatabaseSize int64 `json:"database_size"` - PageSize int `json:"page_size"` - PageCount int `json:"page_count"` - UsedPages int `json:"used_pages"` - FreePages int `json:"free_pages"` - Efficiency float64 `json:"efficiency_percent"` - AutoVacuumEnabled bool `json:"auto_vacuum_enabled"` - LastVacuum time.Time `json:"last_vacuum"` + PageSize int `json:"page_size"` + PageCount int `json:"page_count"` + UsedPages int `json:"used_pages"` + FreePages int `json:"free_pages"` + Efficiency float64 `json:"efficiency_percent"` + AutoVacuumEnabled bool `json:"auto_vacuum_enabled"` + LastVacuum time.Time `json:"last_vacuum"` } // getDatabaseSize returns the current database file size in bytes @@ -198,11 +198,11 @@ func (om *OptimizationManager) getDatabaseSize() (int64, error) { if om.config.Path == "" { return 0, fmt.Errorf("database path not configured") } - + stat, err := os.Stat(om.config.Path) if err != nil { return 0, fmt.Errorf("failed to stat database file: %w", err) } - + return stat.Size(), nil -} \ No newline at end of file +} diff --git a/internal/database/optimization_test.go b/internal/database/optimization_test.go index c1aeb74..1a0d0aa 100644 --- a/internal/database/optimization_test.go +++ b/internal/database/optimization_test.go @@ -221,13 +221,13 @@ func TestOptimizationManager_InvalidPath(t *testing.T) { func TestOptimizationStats_JSON(t *testing.T) { stats := &OptimizationStats{ DatabaseSize: 1024000, - PageSize: 4096, - PageCount: 250, - UsedPages: 200, - FreePages: 50, - Efficiency: 80.0, + PageSize: 4096, + PageCount: 250, + UsedPages: 200, + FreePages: 50, + Efficiency: 80.0, AutoVacuumEnabled: true, - LastVacuum: time.Now(), + LastVacuum: time.Now(), } // Test that all fields are accessible @@ -286,9 +286,9 @@ func TestOptimizationManager_WithRealData(t *testing.T) { } // Compare efficiency - t.Logf("Optimization results: %.2f%% → %.2f%% efficiency", + t.Logf("Optimization results: %.2f%% → %.2f%% efficiency", statsBefore.Efficiency, statsAfter.Efficiency) - + // After optimization, we should have auto-vacuum enabled if !statsAfter.AutoVacuumEnabled { t.Error("Auto-vacuum should be enabled after optimization") @@ -304,4 +304,4 @@ func TestOptimizationManager_WithRealData(t *testing.T) { if count == 0 { t.Error("Data lost during optimization") } -} \ No newline at end of file +} diff --git a/internal/database/path.go b/internal/database/path.go index 64d98fc..515054c 100644 --- a/internal/database/path.go +++ b/internal/database/path.go @@ -17,17 +17,17 @@ func ResolveDatabasePath(configPath string) (string, error) { } return configPath, nil } - + // Try system location first (for services) if systemPath, err := trySystemPath(); err == nil { return systemPath, nil } - + // Try user data directory if userPath, err := tryUserPath(); err == nil { return userPath, nil } - + // Fallback to current directory return tryCurrentDirPath() } @@ -35,7 +35,7 @@ func ResolveDatabasePath(configPath string) (string, error) { // trySystemPath attempts to use system-wide database location func trySystemPath() (string, error) { var systemDir string - + switch runtime.GOOS { case "linux": systemDir = "/var/lib/skyview" @@ -46,26 +46,26 @@ func trySystemPath() (string, error) { default: return "", fmt.Errorf("system path not supported on %s", runtime.GOOS) } - + // Check if directory exists and is writable if err := ensureDirExists(systemDir); err != nil { return "", err } - + dbPath := filepath.Join(systemDir, "skyview.db") - + // Test write permissions if err := testWritePermissions(dbPath); err != nil { return "", err } - + return dbPath, nil } // tryUserPath attempts to use user data directory func tryUserPath() (string, error) { var userDataDir string - + switch runtime.GOOS { case "linux": if xdgData := os.Getenv("XDG_DATA_HOME"); xdgData != "" { @@ -91,20 +91,20 @@ func tryUserPath() (string, error) { default: return "", fmt.Errorf("user path not supported on %s", runtime.GOOS) } - + skyviewDir := filepath.Join(userDataDir, "skyview") - + if err := ensureDirExists(skyviewDir); err != nil { return "", err } - + dbPath := filepath.Join(skyviewDir, "skyview.db") - + // Test write permissions if err := testWritePermissions(dbPath); err != nil { return "", err } - + return dbPath, nil } @@ -114,14 +114,14 @@ func tryCurrentDirPath() (string, error) { if err != nil { return "", fmt.Errorf("cannot get current directory: %v", err) } - + dbPath := filepath.Join(currentDir, "skyview.db") - + // Test write permissions if err := testWritePermissions(dbPath); err != nil { return "", err } - + return dbPath, nil } @@ -134,23 +134,23 @@ func ensureDirExists(dir string) error { } else if err != nil { return fmt.Errorf("cannot access directory %s: %v", dir, err) } - + return nil } // testWritePermissions verifies write access to the database path func testWritePermissions(dbPath string) error { dir := filepath.Dir(dbPath) - + // Check directory write permissions testFile := filepath.Join(dir, ".skyview_write_test") if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil { return fmt.Errorf("no write permission to directory %s: %v", dir, err) } - + // Clean up test file os.Remove(testFile) - + return nil } @@ -171,4 +171,4 @@ func IsSystemPath(dbPath string) bool { return programData != "" && filepath.HasPrefix(dbPath, filepath.Join(programData, "skyview")) } return false -} \ No newline at end of file +} diff --git a/internal/database/test_helpers.go b/internal/database/test_helpers.go index 9d4bcee..46b4e0f 100644 --- a/internal/database/test_helpers.go +++ b/internal/database/test_helpers.go @@ -33,4 +33,4 @@ func setupTestDatabase(t *testing.T) (*Database, func()) { } return db, cleanup -} \ No newline at end of file +} diff --git a/internal/merger/merger.go b/internal/merger/merger.go index 6288a37..dd049f1 100644 --- a/internal/merger/merger.go +++ b/internal/merger/merger.go @@ -448,7 +448,7 @@ func (m *Merger) UpdateAircraft(sourceID string, aircraft *modes.Aircraft, signa state.LastUpdate = timestamp state.TotalMessages++ - + // Persist to database if available and aircraft has position if m.db != nil && aircraft.Latitude != 0 && aircraft.Longitude != 0 { 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) { // Convert ICAO24 to hex string icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24) - + // Prepare database record record := database.AircraftHistoryRecord{ ICAO: icaoHex, @@ -1087,7 +1087,7 @@ func (m *Merger) saveAircraftToDatabase(aircraft *modes.Aircraft, sourceID strin SourceID: sourceID, SignalStrength: &signal, } - + // Add optional fields if available if aircraft.Altitude > 0 { record.Altitude = &aircraft.Altitude @@ -1107,7 +1107,7 @@ func (m *Merger) saveAircraftToDatabase(aircraft *modes.Aircraft, sourceID strin if aircraft.Callsign != "" { record.Callsign = &aircraft.Callsign } - + // Save to database (non-blocking to avoid slowing down real-time processing) go func() { if err := m.db.GetHistoryManager().RecordAircraft(&record); err != nil { diff --git a/internal/server/server.go b/internal/server/server.go index b2f54e4..0208c50 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -55,13 +55,13 @@ type OriginConfig struct { // - Concurrent broadcast system for WebSocket clients // - CORS support for cross-origin web applications type Server struct { - host string // Bind address for HTTP server - port int // TCP port for HTTP server - merger *merger.Merger // Data source for aircraft information + host string // Bind address for HTTP server + port int // TCP port for HTTP server + merger *merger.Merger // Data source for aircraft information database *database.Database // Optional database for persistence - staticFiles embed.FS // Embedded static web assets - server *http.Server // HTTP server instance - origin OriginConfig // Geographic reference point + staticFiles embed.FS // Embedded static web assets + server *http.Server // HTTP server instance + origin OriginConfig // Geographic reference point // WebSocket management 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{}) - + // Get database path and size information dbConfig := s.database.GetConfig() dbPath := dbConfig.Path response["path"] = dbPath - + // Get file size and modification time if stat, err := os.Stat(dbPath); err == nil { response["size_bytes"] = stat.Size() response["size_mb"] = float64(stat.Size()) / (1024 * 1024) response["modified"] = stat.ModTime().Unix() } - + // Get optimization statistics optimizer := database.NewOptimizationManager(s.database, dbConfig) 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() } } - + // Get history statistics historyStats, err := s.database.GetHistoryManager().GetStatistics() if err != nil { log.Printf("Error getting history statistics: %v", err) historyStats = make(map[string]interface{}) } - + // Get callsign statistics if available callsignStats := make(map[string]interface{}) if callsignManager := s.database.GetCallsignManager(); callsignManager != nil { @@ -963,23 +963,23 @@ func (s *Server) handleGetDatabaseStatus(w http.ResponseWriter, r *http.Request) callsignStats = stats } } - + // Get record counts for reference data var airportCount, airlineCount int s.database.GetConnection().QueryRow(`SELECT COUNT(*) FROM airports`).Scan(&airportCount) s.database.GetConnection().QueryRow(`SELECT COUNT(*) FROM airlines`).Scan(&airlineCount) - + referenceData := make(map[string]interface{}) referenceData["airports"] = airportCount referenceData["airlines"] = airlineCount - + response["database_available"] = true response["path"] = dbPath response["reference_data"] = referenceData response["history"] = historyStats response["callsign"] = callsignStats response["timestamp"] = time.Now().Unix() - + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } @@ -993,20 +993,20 @@ func (s *Server) handleGetDataSources(w http.ResponseWriter, r *http.Request) { // Create data loader instance loader := database.NewDataLoader(s.database.GetConnection()) - + availableSources := database.GetAvailableDataSources() loadedSources, err := loader.GetLoadedDataSources() if err != nil { log.Printf("Error getting loaded data sources: %v", err) loadedSources = []database.DataSource{} } - + response := map[string]interface{}{ "available": availableSources, "loaded": loadedSources, "timestamp": time.Now().Unix(), } - + w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } @@ -1021,7 +1021,7 @@ func (s *Server) handleGetCallsignInfo(w http.ResponseWriter, r *http.Request) { // Extract callsign from URL parameters vars := mux.Vars(r) callsign := vars["callsign"] - + if callsign == "" { http.Error(w, "Callsign parameter required", http.StatusBadRequest) return @@ -1036,7 +1036,7 @@ func (s *Server) handleGetCallsignInfo(w http.ResponseWriter, r *http.Request) { } response := map[string]interface{}{ - "callsign": callsignInfo, + "callsign": callsignInfo, "timestamp": time.Now().Unix(), } @@ -1070,12 +1070,12 @@ func (s *Server) debugEmbeddedFiles() { func (s *Server) handleDatabasePage(w http.ResponseWriter, r *http.Request) { // Debug embedded files first s.debugEmbeddedFiles() - + // Try to read the database HTML file from embedded assets data, err := s.staticFiles.ReadFile("static/database.html") if err != nil { log.Printf("Error reading database.html: %v", err) - + // Fallback: serve a simple HTML page with API calls fallbackHTML := ` Database Status - SkyView @@ -1091,7 +1091,7 @@ fetch('/api/database/status') }); ` - + w.Header().Set("Content-Type", "text/html") w.Write([]byte(fallbackHTML)) return diff --git a/internal/squawk/squawk.go b/internal/squawk/squawk.go index 2647bd1..8786ee9 100644 --- a/internal/squawk/squawk.go +++ b/internal/squawk/squawk.go @@ -72,10 +72,10 @@ func NewDatabase() *Database { db := &Database{ codes: make(map[string]*CodeInfo), } - + // Initialize with standard transponder codes db.loadStandardCodes() - + return db } @@ -107,7 +107,7 @@ func (db *Database) loadStandardCodes() { Priority: 90, Notes: "General emergency situation requiring immediate attention", }, - + // Standard VFR/IFR Codes { Code: "1200", @@ -149,7 +149,7 @@ func (db *Database) loadStandardCodes() { Priority: 5, Notes: "Transponder operating but no specific code assigned", }, - + // Special Purpose Codes { Code: "1255", @@ -175,7 +175,7 @@ func (db *Database) loadStandardCodes() { Priority: 35, Notes: "Military interceptor aircraft", }, - + // Military Ranges { Code: "4000", @@ -193,7 +193,7 @@ func (db *Database) loadStandardCodes() { Priority: 12, Notes: "Military interceptor operations (0100-0777 range)", }, - + // Additional Common Codes { Code: "1201", @@ -219,7 +219,7 @@ func (db *Database) loadStandardCodes() { Priority: 8, Notes: "VFR flight above 12,500 feet requiring transponder", }, - + // European Specific { Code: "7001", @@ -246,7 +246,7 @@ func (db *Database) loadStandardCodes() { Notes: "General Air Traffic operating in Other Air Traffic area", }, } - + // Add all codes to the database for _, code := range codes { db.codes[code.Code] = code @@ -254,7 +254,7 @@ func (db *Database) loadStandardCodes() { } // Lookup returns information about a given transponder code -// +// // 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. // @@ -308,13 +308,13 @@ func (db *Database) LookupHex(hexCode string) *CodeInfo { // - []*CodeInfo: Slice of all emergency codes, sorted by priority (highest first) func (db *Database) GetEmergencyCodes() []*CodeInfo { var emergencyCodes []*CodeInfo - + for _, info := range db.codes { if info.Type == Emergency { emergencyCodes = append(emergencyCodes, info) } } - + // Sort by priority (highest first) for i := 0; i < len(emergencyCodes); i++ { for j := i + 1; j < len(emergencyCodes); j++ { @@ -323,7 +323,7 @@ func (db *Database) GetEmergencyCodes() []*CodeInfo { } } } - + return emergencyCodes } @@ -379,7 +379,7 @@ func (db *Database) FormatSquawkWithDescription(code string) string { if info == nil { return code // Return just the code if no description available } - + switch info.Type { case Emergency: return fmt.Sprintf("%s (āš ļø EMERGENCY - %s)", code, info.Description) @@ -390,4 +390,4 @@ func (db *Database) FormatSquawkWithDescription(code string) string { default: return fmt.Sprintf("%s (%s)", code, info.Description) } -} \ No newline at end of file +} diff --git a/internal/squawk/squawk_test.go b/internal/squawk/squawk_test.go index 6a008db..fec85bc 100644 --- a/internal/squawk/squawk_test.go +++ b/internal/squawk/squawk_test.go @@ -9,7 +9,7 @@ func TestNewDatabase(t *testing.T) { if db == nil { t.Fatal("NewDatabase() returned nil") } - + if len(db.codes) == 0 { t.Error("Database should contain pre-loaded codes") } @@ -17,20 +17,20 @@ func TestNewDatabase(t *testing.T) { func TestEmergencyCodes(t *testing.T) { db := NewDatabase() - + emergencyCodes := []string{"7500", "7600", "7700"} - + for _, code := range emergencyCodes { info := db.Lookup(code) if info == nil { t.Errorf("Emergency code %s not found", code) continue } - + if info.Type != Emergency { t.Errorf("Code %s should be Emergency type, got %s", code, info.Type) } - + if !db.IsEmergencyCode(code) { t.Errorf("IsEmergencyCode(%s) should return true", code) } @@ -39,7 +39,7 @@ func TestEmergencyCodes(t *testing.T) { func TestStandardCodes(t *testing.T) { db := NewDatabase() - + testCases := []struct { code string description string @@ -48,16 +48,16 @@ func TestStandardCodes(t *testing.T) { {"7000", "VFR - Visual Flight Rules"}, {"1000", "Mode A/C Not Assigned"}, } - + for _, tc := range testCases { info := db.Lookup(tc.code) if info == nil { t.Errorf("Standard code %s not found", tc.code) continue } - + 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) } } @@ -65,17 +65,17 @@ func TestStandardCodes(t *testing.T) { func TestLookupInt(t *testing.T) { db := NewDatabase() - + // Test integer lookup info := db.LookupInt(7700) if info == nil { t.Fatal("LookupInt(7700) returned nil") } - + if info.Code != "7700" { t.Errorf("Expected code '7700', got '%s'", info.Code) } - + if info.Type != Emergency { 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) { db := NewDatabase() - + // 7700 in octal is 3840 in decimal, which is F00 in hex // However, squawk codes are transmitted differently in different formats // For now, test with a simple hex conversion - + // Test invalid hex info := db.LookupHex("INVALID") if info != nil { @@ -97,7 +97,7 @@ func TestLookupHex(t *testing.T) { func TestFormatSquawkWithDescription(t *testing.T) { db := NewDatabase() - + testCases := []struct { code string expected string @@ -108,7 +108,7 @@ func TestFormatSquawkWithDescription(t *testing.T) { {"0000", "0000 (šŸ”° No Transponder/Military)"}, {"9999", "9999"}, // Unknown code should return just the code } - + for _, tc := range testCases { result := db.FormatSquawkWithDescription(tc.code) if result != tc.expected { @@ -120,12 +120,12 @@ func TestFormatSquawkWithDescription(t *testing.T) { func TestGetEmergencyCodes(t *testing.T) { db := NewDatabase() - + emergencyCodes := db.GetEmergencyCodes() if len(emergencyCodes) != 3 { t.Errorf("Expected 3 emergency codes, got %d", len(emergencyCodes)) } - + // Check that they're sorted by priority (highest first) for i := 1; i < len(emergencyCodes); i++ { if emergencyCodes[i-1].Priority < emergencyCodes[i].Priority { @@ -136,7 +136,7 @@ func TestGetEmergencyCodes(t *testing.T) { func TestAddCustomCode(t *testing.T) { db := NewDatabase() - + customCode := &CodeInfo{ Code: "1234", Description: "Test Custom Code", @@ -145,14 +145,14 @@ func TestAddCustomCode(t *testing.T) { Priority: 50, Notes: "This is a test custom code", } - + db.AddCustomCode(customCode) - + info := db.Lookup("1234") if info == nil { t.Fatal("Custom code not found after adding") } - + if info.Description != "Test Custom Code" { t.Errorf("Custom code description mismatch: expected %q, got %q", "Test Custom Code", info.Description) @@ -170,7 +170,7 @@ func TestCodeTypeString(t *testing.T) { {Military, "Military"}, {Special, "Special"}, } - + for _, tc := range testCases { result := tc.codeType.String() if result != tc.expected { @@ -181,16 +181,16 @@ func TestCodeTypeString(t *testing.T) { func TestGetAllCodes(t *testing.T) { db := NewDatabase() - + allCodes := db.GetAllCodes() if len(allCodes) == 0 { t.Error("GetAllCodes() should return non-empty slice") } - + // Verify we can find known codes in the result found7700 := false found1200 := false - + for _, code := range allCodes { if code.Code == "7700" { found7700 = true @@ -199,11 +199,11 @@ func TestGetAllCodes(t *testing.T) { found1200 = true } } - + if !found7700 { t.Error("Emergency code 7700 not found in GetAllCodes() result") } if !found1200 { t.Error("Standard code 1200 not found in GetAllCodes() result") } -} \ No newline at end of file +}