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"`
}
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)

View File

@@ -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)