diff --git a/internal/server/dashboard.go b/internal/server/dashboard.go index a6a37f44..cf56b926 100644 --- a/internal/server/dashboard.go +++ b/internal/server/dashboard.go @@ -1,6 +1,7 @@ package server import ( + "database/sql" "fmt" "net/http" "strings" @@ -314,27 +315,28 @@ func (s *Server) handleTimeSeries(c *gin.Context) { ORDER BY bucket `, clause) - var rows []struct { - Bucket string `db:"bucket"` - Requests int `db:"requests"` - Tokens int `db:"tokens"` - Cost float64 `db:"cost"` - } - - err := s.database.Select(&rows, query, binds...) + rows, err := s.database.Queryx(query, binds...) if err != nil { c.JSON(http.StatusInternalServerError, ErrorResponse(err.Error())) return } + defer rows.Close() - series := make([]gin.H, len(rows)) - for i, r := range rows { - series[i] = gin.H{ - "time": r.Bucket, - "requests": r.Requests, - "tokens": r.Tokens, - "cost": r.Cost, + var series []gin.H + for rows.Next() { + var bucket string + var requests int + var tokens int + var cost float64 + if err := rows.Scan(&bucket, &requests, &tokens, &cost); err != nil { + continue } + series = append(series, gin.H{ + "time": bucket, + "requests": requests, + "tokens": tokens, + "cost": cost, + }) } granularity := "day" @@ -352,12 +354,8 @@ func (s *Server) handleProvidersUsage(c *gin.Context) { clause, binds := filter.ToSQL() - var rows []struct { - Provider string `db:"provider"` - Requests int `db:"requests"` - } - err := s.database.Select(&rows, fmt.Sprintf(` - SELECT provider, COUNT(*) as requests + rows, err := s.database.Queryx(fmt.Sprintf(` + SELECT COALESCE(provider, 'unknown') as provider, COUNT(*) as requests FROM llm_requests WHERE 1=1 %s GROUP BY provider @@ -366,8 +364,18 @@ func (s *Server) handleProvidersUsage(c *gin.Context) { c.JSON(http.StatusOK, SuccessResponse([]interface{}{})) return } + defer rows.Close() - c.JSON(http.StatusOK, SuccessResponse(rows)) + var results []gin.H + for rows.Next() { + var provider string + var requests int + if err := rows.Scan(&provider, &requests); err == nil { + results = append(results, gin.H{"provider": provider, "requests": requests}) + } + } + + c.JSON(http.StatusOK, SuccessResponse(results)) } func (s *Server) handleClientsUsage(c *gin.Context) { @@ -378,11 +386,7 @@ func (s *Server) handleClientsUsage(c *gin.Context) { clause, binds := filter.ToSQL() - var rows []struct { - ClientID string `db:"client_id" json:"client_id"` - Requests int `db:"requests" json:"requests"` - } - err := s.database.Select(&rows, fmt.Sprintf(` + rows, err := s.database.Queryx(fmt.Sprintf(` SELECT COALESCE(client_id, 'unknown') as client_id, COUNT(*) as requests FROM llm_requests WHERE 1=1 %s @@ -392,8 +396,18 @@ func (s *Server) handleClientsUsage(c *gin.Context) { c.JSON(http.StatusOK, SuccessResponse([]interface{}{})) return } + defer rows.Close() - c.JSON(http.StatusOK, SuccessResponse(rows)) + var results []gin.H + for rows.Next() { + var clientID string + var requests int + if err := rows.Scan(&clientID, &requests); err == nil { + results = append(results, gin.H{"client_id": clientID, "requests": requests}) + } + } + + c.JSON(http.StatusOK, SuccessResponse(results)) } func (s *Server) handleAnalyticsBreakdown(c *gin.Context) { @@ -404,28 +418,38 @@ func (s *Server) handleAnalyticsBreakdown(c *gin.Context) { clause, binds := filter.ToSQL() + // Models breakdown var models []struct { - Label string `db:"label"` - Value int `db:"value"` + Label string `json:"label"` + Value int `json:"value"` } - 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 { - models = []struct { - Label string `db:"label"` - Value int `db:"value"` - }{} + mRows, err := s.database.Queryx(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 { + for mRows.Next() { + var label string + var value int + if err := mRows.Scan(&label, &value); err == nil { + models = append(models, struct{Label string `json:"label"`; Value int `json:"value"`}{label, value}) + } + } + mRows.Close() } + // Clients breakdown var clients []struct { - Label string `db:"label"` - Value int `db:"value"` + Label string `json:"label"` + Value int `json:"value"` } - 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 { - clients = []struct { - Label string `db:"label"` - Value int `db:"value"` - }{} + cRows, err := s.database.Queryx(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 { + for cRows.Next() { + var label string + var value int + if err := cRows.Scan(&label, &value); err == nil { + clients = append(clients, struct{Label string `json:"label"`; Value int `json:"value"`}{label, value}) + } + } + cRows.Close() } c.JSON(http.StatusOK, SuccessResponse(gin.H{ @@ -459,25 +483,34 @@ func (s *Server) handleDetailedUsage(c *gin.Context) { ORDER BY date DESC, cost DESC `, clause) - var rows []struct { - Date string `db:"date" json:"date"` - Client string `db:"client" json:"client"` - Provider string `db:"provider" json:"provider"` - Model string `db:"model" json:"model"` - Requests int `db:"requests" json:"requests"` - Tokens int `db:"tokens" json:"tokens"` - CacheReadTokens int `db:"cache_read_tokens" json:"cache_read_tokens"` - CacheWriteTokens int `db:"cache_write_tokens" json:"cache_write_tokens"` - Cost float64 `db:"cost" json:"cost"` - } - - err := s.database.Select(&rows, query, binds...) + rows, err := s.database.Queryx(query, binds...) if err != nil { c.JSON(http.StatusOK, SuccessResponse([]interface{}{})) return } + defer rows.Close() - c.JSON(http.StatusOK, SuccessResponse(rows)) + var results []gin.H + for rows.Next() { + var date, client, provider, model string + var requests, tokens, cacheRead, cacheWrite int + var cost float64 + if err := rows.Scan(&date, &client, &provider, &model, &requests, &tokens, &cacheRead, &cacheWrite, &cost); err == nil { + results = append(results, gin.H{ + "date": date, + "client": client, + "provider": provider, + "model": model, + "requests": requests, + "tokens": tokens, + "cache_read_tokens": cacheRead, + "cache_write_tokens": cacheWrite, + "cost": cost, + }) + } + } + + c.JSON(http.StatusOK, SuccessResponse(results)) } func (s *Server) handleGetClients(c *gin.Context) { @@ -517,7 +550,10 @@ func (s *Server) handleGetClients(c *gin.Context) { } var lastUsed *time.Time - _ = s.database.Get(&lastUsed, "SELECT MAX(last_used_at) FROM client_tokens WHERE client_id = ?", cl.ClientID) + err := s.database.Get(&lastUsed, "SELECT MAX(last_used_at) FROM client_tokens WHERE client_id = ?", cl.ClientID) + if err != nil && err != sql.ErrNoRows { + // ignore + } uiClients[i] = UIClient{ ID: cl.ClientID,