Files
claude-code/docs/plans/2026-01-01-claude-ops-dashboard-implementation.md
OpenCode Test e43e052a32 Add design plans for dashboard integration
Add implementation plans for morning report, Claude ops dashboard, and realtime monitoring features.
2026-01-03 10:55:22 -08:00

443 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Claude Ops Dashboard Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Extend the existing Go web dashboard to monitor Claude Code agent activity, context usage, skills/commands usage signals, and cost-related token stats from your local `~/.claude/` directory.
**Architecture:** Keep the current lightweight Go HTTP server + static HTML/JS frontend. Add a new “Claude Ops” section with API endpoints that parse local Claude Code state files (primarily `~/.claude/stats-cache.json`, `~/.claude/history.jsonl`, `~/.claude/state/component-registry.json`, and directory listings under `~/.claude/agents`, `~/.claude/skills`, `~/.claude/commands`). Frontend gains new navigation tabs + tables/charts using vanilla JS.
**Tech Stack:** Go 1.21+, chi router, vanilla HTML/CSS/JS (no React), file-based data sources under `~/.claude/`.
---
## Scope (YAGNI)
In this first iteration, focus on read-only analytics:
- Daily activity + token usage (from `~/.claude/stats-cache.json`)
- Recent sessions list (from `~/.claude/history.jsonl` without parsing full message bodies)
- Installed agents/skills/commands inventory (from `~/.claude/agents/`, `~/.claude/skills/`, `~/.claude/commands/`)
- “Debug” view: show which data files are missing/unreadable and last-modified timestamps
Explicitly out of scope:
- Real-time websocket streaming
- Multi-user auth
- Editing/triggering slash commands
- Deep semantic parsing of every history event (well add iteratively)
---
## Task 1: Add Claude directory config to server
**Files:**
- Modify: `~/.claude/dashboard/cmd/server/main.go`
- Modify: `~/.claude/dashboard/README.md`
**Step 1: Write the failing test**
Create a minimal unit test that ensures server config defaults to `~/.claude` when not specified.
- Create: `~/.claude/dashboard/cmd/server/config_test.go`
```go
package main
import (
"os"
"path/filepath"
"testing"
)
func TestDefaultClaudeDir(t *testing.T) {
home, err := os.UserHomeDir()
if err != nil {
t.Fatalf("UserHomeDir: %v", err)
}
want := filepath.Join(home, ".claude")
got := defaultClaudeDir()
if got != want {
t.Fatalf("defaultClaudeDir() = %q, want %q", got, want)
}
}
```
**Step 2: Run test to verify it fails**
Run: `go test ./...`
Expected: FAIL with `undefined: defaultClaudeDir`
**Step 3: Write minimal implementation**
- Modify: `~/.claude/dashboard/cmd/server/main.go`
Add helper:
```go
func defaultClaudeDir() string {
home, err := os.UserHomeDir()
if err != nil {
return "/home/will/.claude" // fallback; best-effort
}
return filepath.Join(home, ".claude")
}
```
Add CLI flag:
- `--claude` (default `~/.claude`)
**Step 4: Run test to verify it passes**
Run: `go test ./...`
Expected: PASS
**Step 5: Commit**
```bash
git add cmd/server/main.go cmd/server/config_test.go README.md
git commit -m "feat: add default claude dir config"
```
---
## Task 2: Add Claude models for API responses
**Files:**
- Create: `~/.claude/dashboard/internal/claude/models.go`
**Step 1: Write the failing test**
- Create: `~/.claude/dashboard/internal/claude/models_test.go`
```go
package claude
import "testing"
func TestModelTypesCompile(t *testing.T) {
_ = StatsCache{}
_ = DailyActivity{}
_ = ModelUsage{}
}
```
**Step 2: Run test to verify it fails**
Run: `go test ./...`
Expected: FAIL with `undefined: StatsCache`
**Step 3: Write minimal implementation**
- Create: `~/.claude/dashboard/internal/claude/models.go`
Implement structs matching the subset we use:
```go
package claude
type DailyActivity struct {
Date string `json:"date"`
MessageCount int `json:"messageCount"`
SessionCount int `json:"sessionCount"`
ToolCallCount int `json:"toolCallCount"`
}
type DailyModelTokens struct {
Date string `json:"date"`
TokensByModel map[string]int `json:"tokensByModel"`
}
type ModelUsage struct {
InputTokens int `json:"inputTokens"`
OutputTokens int `json:"outputTokens"`
CacheReadInputTokens int `json:"cacheReadInputTokens"`
CacheCreationInputTokens int `json:"cacheCreationInputTokens"`
WebSearchRequests int `json:"webSearchRequests"`
CostUSD float64 `json:"costUSD"`
ContextWindow int `json:"contextWindow"`
}
type StatsCache struct {
Version int `json:"version"`
LastComputedDate string `json:"lastComputedDate"`
DailyActivity []DailyActivity `json:"dailyActivity"`
DailyModelTokens []DailyModelTokens `json:"dailyModelTokens"`
ModelUsage map[string]ModelUsage `json:"modelUsage"`
TotalSessions int `json:"totalSessions"`
TotalMessages int `json:"totalMessages"`
}
```
**Step 4: Run test to verify it passes**
Run: `go test ./...`
Expected: PASS
**Step 5: Commit**
```bash
git add internal/claude/models.go internal/claude/models_test.go
git commit -m "feat: add claude stats response models"
```
---
## Task 3: Implement Claude file loader
**Files:**
- Create: `~/.claude/dashboard/internal/claude/loader.go`
- Test: `~/.claude/dashboard/internal/claude/loader_test.go`
**Step 1: Write the failing test**
```go
package claude
import (
"os"
"path/filepath"
"testing"
)
func TestLoadStatsCache(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "stats-cache.json")
err := os.WriteFile(p, []byte(`{"version":1,"lastComputedDate":"2025-12-31","totalSessions":1,"totalMessages":2}`), 0644)
if err != nil {
t.Fatalf("WriteFile: %v", err)
}
loader := NewLoader(dir)
stats, err := loader.LoadStatsCache()
if err != nil {
t.Fatalf("LoadStatsCache: %v", err)
}
if stats.TotalSessions != 1 {
t.Fatalf("TotalSessions=%d", stats.TotalSessions)
}
}
```
**Step 2: Run test to verify it fails**
Run: `go test ./...`
Expected: FAIL with `undefined: NewLoader`
**Step 3: Write minimal implementation**
Implement `Loader` with:
- `NewLoader(claudeDir string)`
- `LoadStatsCache() (*StatsCache, error)` reads `<claudeDir>/stats-cache.json`
- `ListDir(name string) ([]DirEntry, error)` for `agents/`, `skills/`, `commands/`
- `FileInfo(path string) (FileMeta, error)` for debug view
**Step 4: Run test to verify it passes**
Run: `go test ./...`
Expected: PASS
**Step 5: Commit**
```bash
git add internal/claude/loader.go internal/claude/loader_test.go
git commit -m "feat: load claude stats-cache.json"
```
---
## Task 4: Add new Claude Ops API routes
**Files:**
- Modify: `~/.claude/dashboard/cmd/server/main.go`
- Create: `~/.claude/dashboard/internal/api/claude_handlers.go`
- Modify: `~/.claude/dashboard/internal/api/handlers.go` (only if you want shared helpers)
**Step 1: Write the failing test**
- Create: `~/.claude/dashboard/internal/api/claude_handlers_test.go`
```go
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/will/k8s-agent-dashboard/internal/claude"
)
type fakeLoader struct{}
func (f fakeLoader) LoadStatsCache() (*claude.StatsCache, error) {
return &claude.StatsCache{TotalSessions: 3}, nil
}
func TestGetClaudeStats(t *testing.T) {
r := chi.NewRouter()
r.Get("/api/claude/stats", GetClaudeStats(fakeLoader{}))
req := httptest.NewRequest(http.MethodGet, "/api/claude/stats", nil)
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
if w.Code != 200 {
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
}
}
```
**Step 2: Run test to verify it fails**
Run: `go test ./...`
Expected: FAIL with `undefined: GetClaudeStats`
**Step 3: Write minimal implementation**
Create endpoints:
- `GET /api/claude/health` → returns `{status:"ok", claudeDir:"..."}` and file presence checks
- `GET /api/claude/stats` → returns parsed `StatsCache`
- `GET /api/claude/inventory` → lists agents/skills/commands entries
- `GET /api/claude/debug/files` → returns file metas for key files and last-modified
Wire in `cmd/server/main.go`:
- Build loader from `--claude` flag
- Register routes under `/api/claude`
**Step 4: Run test to verify it passes**
Run: `go test ./...`
Expected: PASS
**Step 5: Commit**
```bash
git add cmd/server/main.go internal/api/claude_handlers.go internal/api/claude_handlers_test.go
git commit -m "feat: add claude ops api endpoints"
```
---
## Task 5: Add new UI navigation tabs
**Files:**
- Modify: `~/.claude/dashboard/cmd/server/web/index.html`
- Modify: `~/.claude/dashboard/cmd/server/web/static/css/style.css`
**Step 1: Make a minimal UI change (no tests)**
Add nav buttons:
- Overview
- Usage
- Inventory
- Debug
Add new `<section>` elements mirroring existing “views” pattern.
**Step 2: Manual verification**
Run: `go run ./cmd/server --port 8080 --data /tmp/k8s --claude ~/.claude`
Expected: New tabs switch views (even if empty).
**Step 3: Commit**
```bash
git add cmd/server/web/index.html cmd/server/web/static/css/style.css
git commit -m "feat: add claude ops dashboard views"
```
---
## Task 6: Implement frontend data fetching + rendering
**Files:**
- Modify: `~/.claude/dashboard/cmd/server/web/static/js/app.js`
**Step 1: Add API calls**
Add functions:
- `loadClaudeStats()``GET /api/claude/stats`
- `loadClaudeInventory()``GET /api/claude/inventory`
- `loadClaudeDebugFiles()``GET /api/claude/debug/files`
Integrate into `loadAllData()`.
**Step 2: Add render functions**
- Overview: show totals + lastComputedDate
- Usage: simple table for `dailyActivity` (date, messages, sessions, tool calls)
- Inventory: 3 columns lists: agents, skills, commands
- Debug: table of key files with status/missing + mtime
**Step 3: Manual verification**
Run: `go run ./cmd/server --port 8080 --data /tmp/k8s --claude ~/.claude`
Expected: Data populates from your local `~/.claude/stats-cache.json`.
**Step 4: Commit**
```bash
git add cmd/server/web/static/js/app.js
git commit -m "feat: render claude usage and inventory data"
```
---
## Task 7: Add “cost optimization” signals (derived)
**Files:**
- Modify: `~/.claude/dashboard/internal/api/claude_handlers.go`
- Modify: `~/.claude/dashboard/internal/claude/models.go`
**Step 1: Write the failing test**
Add a test that expects derived fields:
- cache hit ratio estimate: `cacheReadInputTokens / (inputTokens + cacheReadInputTokens + cacheCreationInputTokens)` (best-effort)
- top model by output tokens
**Step 2: Run test to verify it fails**
Run: `go test ./...`
Expected: FAIL because fields arent present
**Step 3: Implement derived summary endpoint**
- `GET /api/claude/summary` returns:
- totals
- per-model tokens
- derived cost signals
**Step 4: Run tests**
Run: `go test ./...`
Expected: PASS
**Step 5: Commit**
```bash
git add internal/api/claude_handlers.go internal/claude/models.go internal/api/claude_handlers_test.go
git commit -m "feat: add claude summary and cache efficiency signals"
```
---
## Task 8: End-to-end check
**Files:**
- None
**Step 1: Run tests**
Run: `go test ./...`
Expected: PASS
**Step 2: Run server locally**
Run: `go run ./cmd/server --port 8080 --data /tmp/k8s --claude ~/.claude`
Expected: Browser shows Claude Ops tabs with live data.
---
## Notes / Follow-ups (later iterations)
- Parse `~/.claude/history.jsonl` into “sessions” and show recent slash commands usage (requires schema discovery)
- Add “top tools called” chart (requires richer history parsing)
- Add alert thresholds (e.g., token spikes day-over-day)