Add implementation plans for morning report, Claude ops dashboard, and realtime monitoring features.
443 lines
11 KiB
Markdown
443 lines
11 KiB
Markdown
# 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 (we’ll 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 aren’t 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)
|