Add comprehensive Claude Code monitoring and realtime streaming to the K8s dashboard. Includes API endpoints for health, stats, summary, inventory, and live event streaming. Frontend provides overview, usage, inventory, debug, and live feed views.
163 lines
4.0 KiB
Go
163 lines
4.0 KiB
Go
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,
|
|
})
|
|
}
|
|
}
|