package api import ( "net/http" "path/filepath" "github.com/will/k8s-agent-dashboard/internal/claude" ) // ClaudeLoader is a minimal interface for Claude Ops endpoints. // // Keep it small so handlers are easy to test with fakes. type ClaudeLoader interface { ClaudeDir() string LoadStatsCache() (*claude.StatsCache, error) ListDir(name string) ([]claude.DirEntry, error) FileMeta(relPath string) (claude.FileMeta, error) PathExists(relPath string) bool } func GetClaudeStats(loader ClaudeLoader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { stats, err := loader.LoadStatsCache() if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) return } respondJSON(w, http.StatusOK, stats) } } type ClaudeSummaryResponse struct { Totals ClaudeSummaryTotals `json:"totals"` PerModel map[string]claude.ModelUsage `json:"perModel"` Derived ClaudeSummaryDerived `json:"derived"` } type ClaudeSummaryTotals struct { TotalSessions int `json:"totalSessions"` TotalMessages int `json:"totalMessages"` } type ClaudeSummaryDerived struct { CacheHitRatioEstimate float64 `json:"cacheHitRatioEstimate"` TopModelByOutputTokens string `json:"topModelByOutputTokens"` } func GetClaudeSummary(loader ClaudeLoader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { stats, err := loader.LoadStatsCache() if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) return } resp := ClaudeSummaryResponse{ Totals: ClaudeSummaryTotals{ TotalSessions: stats.TotalSessions, TotalMessages: stats.TotalMessages, }, PerModel: stats.ModelUsage, } var inputTokens, cacheRead, cacheCreate int maxOut := -1 topModel := "" for model, usage := range stats.ModelUsage { inputTokens += usage.InputTokens cacheRead += usage.CacheReadInputTokens cacheCreate += usage.CacheCreationInputTokens if usage.OutputTokens > maxOut { maxOut = usage.OutputTokens topModel = model } } den := float64(inputTokens + cacheRead + cacheCreate) ratio := 0.0 if den > 0 { ratio = float64(cacheRead) / den } resp.Derived = ClaudeSummaryDerived{ CacheHitRatioEstimate: ratio, TopModelByOutputTokens: topModel, } respondJSON(w, http.StatusOK, resp) } } func GetClaudeHealth(loader ClaudeLoader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { checks := map[string]bool{} missing := false for _, rel := range []string{"stats-cache.json", "history.jsonl", filepath.Join("state", "component-registry.json")} { exists := loader.PathExists(rel) checks[rel] = exists if !exists { missing = true } } status := "ok" if missing { status = "degraded" } respondJSON(w, http.StatusOK, map[string]any{ "status": status, "claudeDir": loader.ClaudeDir(), "fileChecks": checks, }) } } func GetClaudeInventory(loader ClaudeLoader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { agents, err := loader.ListDir("agents") if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) return } skills, err := loader.ListDir("skills") if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) return } commands, err := loader.ListDir("commands") if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) return } respondJSON(w, http.StatusOK, map[string]any{ "agents": agents, "skills": skills, "commands": commands, }) } } func GetClaudeDebugFiles(loader ClaudeLoader) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var metas []claude.FileMeta for _, rel := range []string{ "stats-cache.json", "history.jsonl", filepath.Join("state", "component-registry.json"), } { meta, err := loader.FileMeta(rel) if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) return } metas = append(metas, meta) } respondJSON(w, http.StatusOK, map[string]any{ "files": metas, }) } }