diff --git a/assets/assets.go b/assets/assets.go
index a4a923b..e80edaa 100644
--- a/assets/assets.go
+++ b/assets/assets.go
@@ -29,6 +29,5 @@ 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/assets/static/css/style.css b/assets/static/css/style.css
index 77623e8..3879d96 100644
--- a/assets/static/css/style.css
+++ b/assets/static/css/style.css
@@ -262,28 +262,21 @@ body {
}
.legend-icon {
- width: 24px;
- height: 24px;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- margin-right: 8px;
+ width: 16px;
+ height: 16px;
+ border-radius: 2px;
+ border: 1px solid #ffffff;
}
-.legend-icon svg {
- width: 100%;
- height: 100%;
-}
-
-.legend-icon.light svg { color: #00bfff; } /* Sky blue for light aircraft */
-.legend-icon.medium svg { color: #00ff88; } /* Green for medium aircraft */
-.legend-icon.large svg { color: #ff8c00; } /* Orange for large aircraft */
-.legend-icon.high-vortex svg { color: #ff4500; } /* Red-orange for high vortex large */
-.legend-icon.heavy svg { color: #ff0000; } /* Red for heavy aircraft */
-.legend-icon.helicopter svg { color: #ff00ff; } /* Magenta for helicopters */
-.legend-icon.military svg { color: #ff4444; } /* Red-orange for military */
-.legend-icon.ga svg { color: #ffff00; } /* Yellow for general aviation */
-.legend-icon.ground svg { color: #888888; } /* Gray for ground vehicles */
+.legend-icon.light { background: #00bfff; } /* Sky blue for light aircraft */
+.legend-icon.medium { background: #00ff88; } /* Green for medium aircraft */
+.legend-icon.large { background: #ff8c00; } /* Orange for large aircraft */
+.legend-icon.high-vortex { background: #ff4500; } /* Red-orange for high vortex large */
+.legend-icon.heavy { background: #ff0000; } /* Red for heavy aircraft */
+.legend-icon.helicopter { background: #ff00ff; } /* Magenta for helicopters */
+.legend-icon.military { background: #ff4444; } /* Red-orange for military */
+.legend-icon.ga { background: #ffff00; } /* Yellow for general aviation */
+.legend-icon.ground { background: #888888; } /* Gray for ground vehicles */
.table-controls {
display: flex;
diff --git a/assets/static/icons/heavy.svg b/assets/static/icons/heavy.svg
deleted file mode 100644
index 060d255..0000000
--- a/assets/static/icons/heavy.svg
+++ /dev/null
@@ -1,26 +0,0 @@
-
-
\ No newline at end of file
diff --git a/assets/static/icons/large.svg b/assets/static/icons/large.svg
deleted file mode 100644
index f0c916a..0000000
--- a/assets/static/icons/large.svg
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
\ No newline at end of file
diff --git a/assets/static/icons/light.svg b/assets/static/icons/light.svg
deleted file mode 100644
index b134afd..0000000
--- a/assets/static/icons/light.svg
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
\ No newline at end of file
diff --git a/assets/static/icons/medium.svg b/assets/static/icons/medium.svg
deleted file mode 100644
index ba07323..0000000
--- a/assets/static/icons/medium.svg
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
\ No newline at end of file
diff --git a/assets/static/index.html b/assets/static/index.html
index 7eb70e1..2994330 100644
--- a/assets/static/index.html
+++ b/assets/static/index.html
@@ -112,175 +112,37 @@
ADS-B Categories
-
-
-
+
Light < 7000kg
-
-
-
+
Medium 7000-34000kg
-
-
-
+
Large 34000-136000kg
-
-
-
+
High Vortex Large
-
-
-
+
Heavy > 136000kg
-
-
-
+
Rotorcraft
-
-
-
+
Glider/Ultralight
-
-
-
+
Surface Vehicle
-
-
-
-
-
Military
-
Sources
diff --git a/assets/static/js/modules/aircraft-manager.js b/assets/static/js/modules/aircraft-manager.js
index d2bbfdd..78f5ada 100644
--- a/assets/static/js/modules/aircraft-manager.js
+++ b/assets/static/js/modules/aircraft-manager.js
@@ -331,48 +331,60 @@ export class AircraftManager {
}
getAircraftIconType(aircraft) {
- // For icon selection, determine which SVG shape to use based on category
+ // For icon selection, we still need basic categories
+ // This determines which SVG shape to use
if (aircraft.OnGround) return 'ground';
if (aircraft.Category) {
const cat = aircraft.Category.toLowerCase();
- // Specialized aircraft types
+ // Map to basic icon types for visual representation
if (cat.includes('helicopter') || cat.includes('rotorcraft')) return 'helicopter';
if (cat.includes('military') || cat.includes('fighter') || cat.includes('bomber')) return 'military';
- if (cat.includes('glider') || cat.includes('ultralight')) return 'ga';
-
- // Weight-based categories with specific icons
- if (cat.includes('light') && cat.includes('7000')) return 'light';
- if (cat.includes('medium') && cat.includes('7000-34000')) return 'medium';
- if (cat.includes('large') && cat.includes('34000-136000')) return 'large';
- if (cat.includes('heavy') && cat.includes('136000')) return 'heavy';
- if (cat.includes('high vortex')) return 'large'; // Use large icon for high vortex
-
- // Fallback category matching
- if (cat.includes('heavy') || cat.includes('super')) return 'heavy';
- if (cat.includes('large')) return 'large';
- if (cat.includes('medium')) return 'medium';
- if (cat.includes('light')) return 'light';
+ if (cat.includes('cargo') || cat.includes('heavy') || cat.includes('super')) return 'cargo';
+ if (cat.includes('light') || cat.includes('glider') || cat.includes('ultralight')) return 'ga';
}
- // Default to medium icon for unknown aircraft
- return 'medium';
+ // Default commercial icon for everything else
+ return 'commercial';
}
getAircraftColor(type, aircraft) {
- // Color mapping based on aircraft type/size
- switch (type) {
- case 'military': return '#ff4444'; // Red-orange for military
- case 'helicopter': return '#ff00ff'; // Magenta for helicopters
- case 'ground': return '#888888'; // Gray for ground vehicles
- case 'ga': return '#ffff00'; // Yellow for general aviation
- case 'light': return '#00bfff'; // Sky blue for light aircraft
- case 'medium': return '#00ff88'; // Green for medium aircraft
- case 'large': return '#ff8c00'; // Orange for large aircraft
- case 'heavy': return '#ff0000'; // Red for heavy aircraft
- default: return '#00ff88'; // Default green for unknown
+ // Special colors for specific types
+ if (type === 'military') return '#ff4444';
+ if (type === 'helicopter') return '#ff00ff';
+ if (type === 'ground') return '#888888';
+ if (type === 'ga') return '#ffff00';
+
+ // For commercial and cargo types, use weight-based colors
+ if (aircraft && aircraft.Category) {
+ const cat = aircraft.Category.toLowerCase();
+
+ // Check for specific weight ranges in the category string
+ // Light aircraft (< 7000kg) - Sky blue
+ if (cat.includes('light')) {
+ return '#00bfff';
+ }
+ // Medium aircraft (7000-34000kg) - Green
+ if (cat.includes('medium')) {
+ return '#00ff88';
+ }
+ // High Vortex Large - Red-orange (special wake turbulence category)
+ if (cat.includes('high vortex')) {
+ return '#ff4500';
+ }
+ // Large aircraft (34000-136000kg) - Orange
+ if (cat.includes('large')) {
+ return '#ff8c00';
+ }
+ // Heavy aircraft (> 136000kg) - Red
+ if (cat.includes('heavy') || cat.includes('super')) {
+ return '#ff0000';
+ }
}
+
+ // Default to green for unknown commercial aircraft
+ return '#00ff88';
}
diff --git a/cmd/skyview-data/main.go b/cmd/skyview-data/main.go
index 99496fa..e7bb5f9 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,13 +454,11 @@ 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)
}
}
@@ -473,7 +471,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) {
@@ -481,7 +479,7 @@ func cmdImport(db *database.Database, sourceName string, force bool) error {
break
}
}
-
+
if matchedSource == nil {
return fmt.Errorf("unknown data source: %s", sourceName)
}
@@ -493,17 +491,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 {
@@ -552,10 +550,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"
}
@@ -566,30 +564,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)
@@ -598,18 +596,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
}
@@ -617,23 +615,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)
@@ -641,47 +639,48 @@ 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 50c77b1..f7b7614 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 bcbb312..108a0d9 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,6 +240,7 @@ func (db *Database) Health() error {
return db.conn.Ping()
}
+
// DefaultConfig returns the default database configuration
func DefaultConfig() *Config {
return &Config{
@@ -257,4 +258,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 3357ed3..4e6d21b 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 66b3803..6d31dfd 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,37 +206,29 @@ 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()
}
@@ -247,21 +239,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)
@@ -271,7 +263,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 {
@@ -282,15 +274,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], `"`)
@@ -304,37 +296,29 @@ 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()
}
@@ -346,9 +330,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 {
@@ -356,13 +340,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 (
@@ -378,7 +362,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()
@@ -390,13 +374,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")
@@ -410,7 +394,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 != "" {
@@ -423,7 +407,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 != "" {
@@ -431,10 +415,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,
@@ -448,7 +432,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)
@@ -457,7 +441,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()
}
@@ -476,13 +460,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
@@ -498,7 +482,7 @@ func (dl *DataLoader) GetLoadedDataSources() ([]DataSource, error) {
}
sources = append(sources, source)
}
-
+
return sources, rows.Err()
}
@@ -509,10 +493,11 @@ 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()
@@ -520,22 +505,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 8fbb0f5..9bfebe8 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 67b511f..2f563c7 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 06246a7..731f61a 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 ea25f36..1121fa3 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 20ea20b..e58fcbf 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 e55929c..abe322e 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 1a0d0aa..c1aeb74 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 515054c..64d98fc 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 46b4e0f..9d4bcee 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 dd049f1..6288a37 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 0208c50..b2f54e4 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')
});