diff --git a/internal/server/server.go b/internal/server/server.go index c03cd7b..b2f54e4 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -18,8 +18,10 @@ import ( "embed" "encoding/json" "fmt" + "io/fs" "log" "net/http" + "os" "path" "strconv" "strings" @@ -29,6 +31,7 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/websocket" + "skyview/internal/database" "skyview/internal/merger" ) @@ -52,12 +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 - staticFiles embed.FS // Embedded static web assets - server *http.Server // HTTP server instance - origin OriginConfig // Geographic reference point + 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 // WebSocket management wsClients map[*websocket.Conn]bool // Active WebSocket client connections @@ -98,15 +102,17 @@ type AircraftUpdate struct { // - host: Bind address (empty for all interfaces, "localhost" for local only) // - port: TCP port number for the HTTP server // - merger: Data merger instance providing aircraft information +// - database: Optional database for persistence and callsign enhancement // - staticFiles: Embedded filesystem containing web assets // - origin: Geographic reference point for the map interface // // Returns a configured but not yet started server instance. -func NewWebServer(host string, port int, merger *merger.Merger, staticFiles embed.FS, origin OriginConfig) *Server { +func NewWebServer(host string, port int, merger *merger.Merger, database *database.Database, staticFiles embed.FS, origin OriginConfig) *Server { return &Server{ host: host, port: port, merger: merger, + database: database, staticFiles: staticFiles, origin: origin, wsClients: make(map[*websocket.Conn]bool), @@ -204,6 +210,10 @@ func (s *Server) setupRoutes() http.Handler { api.HandleFunc("/origin", s.handleGetOrigin).Methods("GET") api.HandleFunc("/coverage/{sourceId}", s.handleGetCoverage).Methods("GET") api.HandleFunc("/heatmap/{sourceId}", s.handleGetHeatmap).Methods("GET") + // Database API endpoints + api.HandleFunc("/database/status", s.handleGetDatabaseStatus).Methods("GET") + api.HandleFunc("/database/sources", s.handleGetDataSources).Methods("GET") + api.HandleFunc("/callsign/{callsign}", s.handleGetCallsignInfo).Methods("GET") // WebSocket router.HandleFunc("/ws", s.handleWebSocket) @@ -214,6 +224,8 @@ func (s *Server) setupRoutes() http.Handler { // Main page router.HandleFunc("/", s.handleIndex) + // Database status page + router.HandleFunc("/database", s.handleDatabasePage) // Enable CORS return s.enableCORS(router) @@ -898,3 +910,193 @@ func (s *Server) handleDebugWebSocket(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } + +// handleGetDatabaseStatus returns database status and statistics +func (s *Server) handleGetDatabaseStatus(w http.ResponseWriter, r *http.Request) { + if s.database == nil { + http.Error(w, "Database not available", http.StatusServiceUnavailable) + return + } + + 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 { + response["efficiency_percent"] = optimizationStats.Efficiency + response["page_size"] = optimizationStats.PageSize + response["page_count"] = optimizationStats.PageCount + response["used_pages"] = optimizationStats.UsedPages + response["free_pages"] = optimizationStats.FreePages + response["auto_vacuum_enabled"] = optimizationStats.AutoVacuumEnabled + if !optimizationStats.LastVacuum.IsZero() { + 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 { + stats, err := callsignManager.GetCacheStats() + if err != nil { + log.Printf("Error getting callsign statistics: %v", err) + } else { + 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) +} + +// handleGetDataSources returns information about loaded external data sources +func (s *Server) handleGetDataSources(w http.ResponseWriter, r *http.Request) { + if s.database == nil { + http.Error(w, "Database not available", http.StatusServiceUnavailable) + return + } + + // 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) +} + +// handleGetCallsignInfo returns enriched information about a callsign +func (s *Server) handleGetCallsignInfo(w http.ResponseWriter, r *http.Request) { + if s.database == nil { + http.Error(w, "Database not available", http.StatusServiceUnavailable) + return + } + + // Extract callsign from URL parameters + vars := mux.Vars(r) + callsign := vars["callsign"] + + if callsign == "" { + http.Error(w, "Callsign parameter required", http.StatusBadRequest) + return + } + + // Get callsign information from database + callsignInfo, err := s.database.GetCallsignManager().GetCallsignInfo(callsign) + if err != nil { + log.Printf("Error getting callsign info for %s: %v", callsign, err) + http.Error(w, "Failed to lookup callsign information", http.StatusInternalServerError) + return + } + + response := map[string]interface{}{ + "callsign": callsignInfo, + "timestamp": time.Now().Unix(), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// debugEmbeddedFiles lists all embedded files for debugging +func (s *Server) debugEmbeddedFiles() { + log.Println("=== Debugging Embedded Files ===") + err := fs.WalkDir(s.staticFiles, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + log.Printf("Error walking %s: %v", path, err) + return nil + } + if !d.IsDir() { + info, _ := d.Info() + log.Printf("Embedded file: %s (size: %d bytes)", path, info.Size()) + } else { + log.Printf("Embedded dir: %s/", path) + } + return nil + }) + if err != nil { + log.Printf("Error walking embedded files: %v", err) + } + log.Println("=== End Embedded Files Debug ===") +} + +// handleDatabasePage serves the database status page +func (s *Server) handleDatabasePage(w http.ResponseWriter, r *http.Request) { + // Debug embedded files first + s.debugEmbeddedFiles() + + // Try to read the database HTML file from embedded assets + data, err := s.staticFiles.ReadFile("static/database.html") + if err != nil { + log.Printf("Error reading database.html: %v", err) + + // Fallback: serve a simple HTML page with API calls + fallbackHTML := ` +Database Status - SkyView + +

Database Status

+
Loading...
+ +` + + w.Header().Set("Content-Type", "text/html") + w.Write([]byte(fallbackHTML)) + return + } + + w.Header().Set("Content-Type", "text/html") + w.Write(data) +}