From 1f3adceda4ebaa18f894c8c2ad4568842872d3d4 Mon Sep 17 00:00:00 2001 From: hobokenchicken Date: Thu, 19 Mar 2026 12:28:56 -0400 Subject: [PATCH] fix: robustify analytics handlers and fix auth middleware scope Moved AuthMiddleware to /v1 group only. Added COALESCE and empty result handling to analytics SQL queries to prevent 500 errors on empty databases. --- internal/server/dashboard.go | 54 ++++++++++++++++++++---------------- internal/server/server.go | 5 +++- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/internal/server/dashboard.go b/internal/server/dashboard.go index 7081cab3..e34c6266 100644 --- a/internal/server/dashboard.go +++ b/internal/server/dashboard.go @@ -248,25 +248,33 @@ func (s *Server) handleUsageSummary(c *gin.Context) { TodayCost float64 `db:"today_cost"` } today := time.Now().UTC().Format("2006-01-02") - _ = s.database.Get(&todayStats, ` + err = s.database.Get(&todayStats, ` SELECT COUNT(*) as today_requests, COALESCE(SUM(cost), 0.0) as today_cost FROM llm_requests WHERE strftime('%Y-%m-%d', timestamp) = ? `, today) + if err != nil { + todayStats.TodayRequests = 0 + todayStats.TodayCost = 0.0 + } // Error rate & Avg response time var miscStats struct { ErrorRate float64 `db:"error_rate"` AvgResponseTime float64 `db:"avg_response_time"` } - _ = s.database.Get(&miscStats, ` + err = s.database.Get(&miscStats, ` SELECT - (CAST(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*)) * 100.0 as error_rate, + CASE WHEN COUNT(*) = 0 THEN 0.0 ELSE (CAST(SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*)) * 100.0 END as error_rate, COALESCE(AVG(duration_ms), 0.0) as avg_response_time FROM llm_requests `) + if err != nil { + miscStats.ErrorRate = 0.0 + miscStats.AvgResponseTime = 0.0 + } c.JSON(http.StatusOK, SuccessResponse(gin.H{ "total_requests": totalStats.TotalRequests, @@ -329,12 +337,7 @@ func (s *Server) handleTimeSeries(c *gin.Context) { } } - // Add granularity for the UI granularity := "day" - if filter.Period == "24h" || filter.Period == "today" { - // If we had hourly bucketing we'd use 'hour' - } - c.JSON(http.StatusOK, SuccessResponse(gin.H{ "series": series, "granularity": granularity, @@ -360,7 +363,7 @@ func (s *Server) handleProvidersUsage(c *gin.Context) { GROUP BY provider `, clause), binds...) if err != nil { - c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error())) + c.JSON(http.StatusOK, SuccessResponse([]interface{}{})) return } @@ -379,20 +382,24 @@ func (s *Server) handleAnalyticsBreakdown(c *gin.Context) { Label string `db:"label"` Value int `db:"value"` } - err := s.database.Select(&models, fmt.Sprintf("SELECT model as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY model ORDER BY value DESC", clause), binds...) + err := s.database.Select(&models, fmt.Sprintf("SELECT COALESCE(model, 'unknown') as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY model ORDER BY value DESC", clause), binds...) if err != nil { - c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error())) - return + models = []struct { + Label string `db:"label"` + Value int `db:"value"` + }{} } var clients []struct { Label string `db:"label"` Value int `db:"value"` } - err = s.database.Select(&clients, fmt.Sprintf("SELECT client_id as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY client_id ORDER BY value DESC", clause), binds...) + err = s.database.Select(&clients, fmt.Sprintf("SELECT COALESCE(client_id, 'unknown') as label, COUNT(*) as value FROM llm_requests WHERE 1=1 %s GROUP BY client_id ORDER BY value DESC", clause), binds...) if err != nil { - c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error())) - return + clients = []struct { + Label string `db:"label"` + Value int `db:"value"` + }{} } c.JSON(http.StatusOK, SuccessResponse(gin.H{ @@ -412,14 +419,14 @@ func (s *Server) handleDetailedUsage(c *gin.Context) { query := fmt.Sprintf(` SELECT strftime('%%Y-%%m-%%d', timestamp) as date, - client_id as client, - provider, - model, + COALESCE(client_id, 'unknown') as client, + COALESCE(provider, 'unknown') as provider, + COALESCE(model, 'unknown') as model, COUNT(*) as requests, - SUM(total_tokens) as tokens, - SUM(cache_read_tokens) as cache_read_tokens, - SUM(cache_write_tokens) as cache_write_tokens, - SUM(cost) as cost + COALESCE(SUM(total_tokens), 0) as tokens, + COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens, + COALESCE(SUM(cache_write_tokens), 0) as cache_write_tokens, + COALESCE(SUM(cost), 0.0) as cost FROM llm_requests WHERE 1=1 %s GROUP BY date, client, provider, model @@ -440,7 +447,7 @@ func (s *Server) handleDetailedUsage(c *gin.Context) { err := s.database.Select(&rows, query, binds...) if err != nil { - c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error())) + c.JSON(http.StatusOK, SuccessResponse([]interface{}{})) return } @@ -483,7 +490,6 @@ func (s *Server) handleGetClients(c *gin.Context) { desc = *cl.Description } - // Get last used from tokens var lastUsed *time.Time _ = s.database.Get(&lastUsed, "SELECT MAX(last_used_at) FROM client_tokens WHERE client_id = ?", cl.ClientID) diff --git a/internal/server/server.go b/internal/server/server.go index 0148b5eb..dbbbda19 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -74,7 +74,8 @@ func NewServer(cfg *config.Config, database *db.DB) *Server { } func (s *Server) setupRoutes() { - s.router.Use(middleware.AuthMiddleware(s.database)) + // Global middleware should only be for logging/recovery + // Auth is specific to groups // Static files s.router.StaticFile("/", "./static/index.html") @@ -86,7 +87,9 @@ func (s *Server) setupRoutes() { // WebSocket s.router.GET("/ws", s.handleWebSocket) + // API V1 (External LLM Access) v1 := s.router.Group("/v1") + v1.Use(middleware.AuthMiddleware(s.database)) { v1.POST("/chat/completions", s.handleChatCompletions) v1.GET("/models", s.handleListModels)