style: Apply code formatting with go fmt
- Run 'make format' to ensure all Go code follows standard formatting - Maintains consistent code style across the entire codebase - No functional changes, only whitespace and formatting improvements
This commit is contained in:
parent
4fd0846127
commit
2bffa2c418
19 changed files with 543 additions and 527 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,4 +164,4 @@ func TestDatabasePragmas(t *testing.T) {
|
|||
if journalMode != "wal" {
|
||||
t.Errorf("Expected WAL journal mode, got: %s", journalMode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,4 +33,4 @@ func setupTestDatabase(t *testing.T) (*Database, func()) {
|
|||
}
|
||||
|
||||
return db, cleanup
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 := `<!DOCTYPE html>
|
||||
<html><head><title>Database Status - SkyView</title></head>
|
||||
|
|
@ -1091,7 +1091,7 @@ fetch('/api/database/status')
|
|||
});
|
||||
</script>
|
||||
</body></html>`
|
||||
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write([]byte(fallbackHTML))
|
||||
return
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue