fix: robustify analytics handlers and fix auth middleware scope
Some checks failed
CI / Lint (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Build (push) Has been cancelled

Moved AuthMiddleware to /v1 group only. Added COALESCE and empty result handling to analytics SQL queries to prevent 500 errors on empty databases.
This commit is contained in:
2026-03-19 12:28:56 -04:00
parent 9c64a8fe42
commit 1f3adceda4
2 changed files with 34 additions and 25 deletions

View File

@@ -248,25 +248,33 @@ func (s *Server) handleUsageSummary(c *gin.Context) {
TodayCost float64 `db:"today_cost"` TodayCost float64 `db:"today_cost"`
} }
today := time.Now().UTC().Format("2006-01-02") today := time.Now().UTC().Format("2006-01-02")
_ = s.database.Get(&todayStats, ` err = s.database.Get(&todayStats, `
SELECT SELECT
COUNT(*) as today_requests, COUNT(*) as today_requests,
COALESCE(SUM(cost), 0.0) as today_cost COALESCE(SUM(cost), 0.0) as today_cost
FROM llm_requests FROM llm_requests
WHERE strftime('%Y-%m-%d', timestamp) = ? WHERE strftime('%Y-%m-%d', timestamp) = ?
`, today) `, today)
if err != nil {
todayStats.TodayRequests = 0
todayStats.TodayCost = 0.0
}
// Error rate & Avg response time // Error rate & Avg response time
var miscStats struct { var miscStats struct {
ErrorRate float64 `db:"error_rate"` ErrorRate float64 `db:"error_rate"`
AvgResponseTime float64 `db:"avg_response_time"` AvgResponseTime float64 `db:"avg_response_time"`
} }
_ = s.database.Get(&miscStats, ` err = s.database.Get(&miscStats, `
SELECT 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 COALESCE(AVG(duration_ms), 0.0) as avg_response_time
FROM llm_requests FROM llm_requests
`) `)
if err != nil {
miscStats.ErrorRate = 0.0
miscStats.AvgResponseTime = 0.0
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{ c.JSON(http.StatusOK, SuccessResponse(gin.H{
"total_requests": totalStats.TotalRequests, "total_requests": totalStats.TotalRequests,
@@ -329,12 +337,7 @@ func (s *Server) handleTimeSeries(c *gin.Context) {
} }
} }
// Add granularity for the UI
granularity := "day" 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{ c.JSON(http.StatusOK, SuccessResponse(gin.H{
"series": series, "series": series,
"granularity": granularity, "granularity": granularity,
@@ -360,7 +363,7 @@ func (s *Server) handleProvidersUsage(c *gin.Context) {
GROUP BY provider GROUP BY provider
`, clause), binds...) `, clause), binds...)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error())) c.JSON(http.StatusOK, SuccessResponse([]interface{}{}))
return return
} }
@@ -379,20 +382,24 @@ func (s *Server) handleAnalyticsBreakdown(c *gin.Context) {
Label string `db:"label"` Label string `db:"label"`
Value int `db:"value"` 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 { if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error())) models = []struct {
return Label string `db:"label"`
Value int `db:"value"`
}{}
} }
var clients []struct { var clients []struct {
Label string `db:"label"` Label string `db:"label"`
Value int `db:"value"` 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 { if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error())) clients = []struct {
return Label string `db:"label"`
Value int `db:"value"`
}{}
} }
c.JSON(http.StatusOK, SuccessResponse(gin.H{ c.JSON(http.StatusOK, SuccessResponse(gin.H{
@@ -412,14 +419,14 @@ func (s *Server) handleDetailedUsage(c *gin.Context) {
query := fmt.Sprintf(` query := fmt.Sprintf(`
SELECT SELECT
strftime('%%Y-%%m-%%d', timestamp) as date, strftime('%%Y-%%m-%%d', timestamp) as date,
client_id as client, COALESCE(client_id, 'unknown') as client,
provider, COALESCE(provider, 'unknown') as provider,
model, COALESCE(model, 'unknown') as model,
COUNT(*) as requests, COUNT(*) as requests,
SUM(total_tokens) as tokens, COALESCE(SUM(total_tokens), 0) as tokens,
SUM(cache_read_tokens) as cache_read_tokens, COALESCE(SUM(cache_read_tokens), 0) as cache_read_tokens,
SUM(cache_write_tokens) as cache_write_tokens, COALESCE(SUM(cache_write_tokens), 0) as cache_write_tokens,
SUM(cost) as cost COALESCE(SUM(cost), 0.0) as cost
FROM llm_requests FROM llm_requests
WHERE 1=1 %s WHERE 1=1 %s
GROUP BY date, client, provider, model GROUP BY date, client, provider, model
@@ -440,7 +447,7 @@ func (s *Server) handleDetailedUsage(c *gin.Context) {
err := s.database.Select(&rows, query, binds...) err := s.database.Select(&rows, query, binds...)
if err != nil { if err != nil {
c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error())) c.JSON(http.StatusOK, SuccessResponse([]interface{}{}))
return return
} }
@@ -483,7 +490,6 @@ func (s *Server) handleGetClients(c *gin.Context) {
desc = *cl.Description desc = *cl.Description
} }
// Get last used from tokens
var lastUsed *time.Time var lastUsed *time.Time
_ = s.database.Get(&lastUsed, "SELECT MAX(last_used_at) FROM client_tokens WHERE client_id = ?", cl.ClientID) _ = s.database.Get(&lastUsed, "SELECT MAX(last_used_at) FROM client_tokens WHERE client_id = ?", cl.ClientID)

View File

@@ -74,7 +74,8 @@ func NewServer(cfg *config.Config, database *db.DB) *Server {
} }
func (s *Server) setupRoutes() { 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 // Static files
s.router.StaticFile("/", "./static/index.html") s.router.StaticFile("/", "./static/index.html")
@@ -86,7 +87,9 @@ func (s *Server) setupRoutes() {
// WebSocket // WebSocket
s.router.GET("/ws", s.handleWebSocket) s.router.GET("/ws", s.handleWebSocket)
// API V1 (External LLM Access)
v1 := s.router.Group("/v1") v1 := s.router.Group("/v1")
v1.Use(middleware.AuthMiddleware(s.database))
{ {
v1.POST("/chat/completions", s.handleChatCompletions) v1.POST("/chat/completions", s.handleChatCompletions)
v1.GET("/models", s.handleListModels) v1.GET("/models", s.handleListModels)