feat: Enhance database status API with comprehensive information

- Add database file size, path, and modification timestamp to /api/database/status
- Include storage efficiency metrics and page statistics
- Add optimization statistics using database manager
- Import "os" package for file system operations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-31 19:39:56 +02:00
commit 96f90b1543

View file

@ -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 := `<!DOCTYPE html>
<html><head><title>Database Status - SkyView</title></head>
<body>
<h1>Database Status</h1>
<div id="status">Loading...</div>
<script>
fetch('/api/database/status')
.then(r => r.json())
.then(data => {
document.getElementById('status').innerHTML =
'<pre>' + JSON.stringify(data, null, 2) + '</pre>';
});
</script>
</body></html>`
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(fallbackHTML))
return
}
w.Header().Set("Content-Type", "text/html")
w.Write(data)
}