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:
parent
d0dfd3ed46
commit
96f90b1543
1 changed files with 209 additions and 7 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue