From e43e052a32e1ae94a0a7b92f99d3e5c1a37e385f Mon Sep 17 00:00:00 2001 From: OpenCode Test Date: Sat, 3 Jan 2026 10:55:22 -0800 Subject: [PATCH] Add design plans for dashboard integration Add implementation plans for morning report, Claude ops dashboard, and realtime monitoring features. --- .../plans/2025-01-02-morning-report-design.md | 213 ++++++ ...-01-claude-ops-dashboard-implementation.md | 442 ++++++++++++ ...aude-realtime-monitoring-implementation.md | 660 ++++++++++++++++++ plans/glistening-wondering-wadler.md | 173 +++++ 4 files changed, 1488 insertions(+) create mode 100644 docs/plans/2025-01-02-morning-report-design.md create mode 100644 docs/plans/2026-01-01-claude-ops-dashboard-implementation.md create mode 100644 docs/plans/2026-01-01-claude-realtime-monitoring-implementation.md create mode 100644 plans/glistening-wondering-wadler.md diff --git a/docs/plans/2025-01-02-morning-report-design.md b/docs/plans/2025-01-02-morning-report-design.md new file mode 100644 index 0000000..49a9600 --- /dev/null +++ b/docs/plans/2025-01-02-morning-report-design.md @@ -0,0 +1,213 @@ +# Morning Report System Design + +**Date:** 2025-01-02 +**Status:** Approved +**Author:** PA + User collaboration + +## Overview + +A daily morning dashboard that aggregates useful information into a single Markdown file, generated automatically via systemd timer and refreshable on-demand. + +## Output + +- **Format:** Markdown +- **Location:** `~/.claude/reports/morning.md` +- **Archive:** `~/.claude/reports/archive/YYYY-MM-DD.md` (30 days retention) + +## Schedule + +- **Automatic:** Systemd timer at 8:00 AM Pacific +- **On-demand:** `/morning` command for manual refresh + +## Report Sections + +### 1. Weather +- **Source:** wttr.in (no API key) +- **Location:** Seattle, WA, USA +- **LLM:** Haiku (parse output, add hints like "bring umbrella") + +### 2. Email +- **Source:** Gmail skill (existing) +- **Display:** Unread count, urgent highlights, top 5 emails +- **LLM:** Sonnet (triage urgency, summarize) + +### 3. Calendar +- **Source:** gcal skill (existing) +- **Display:** Today's events + tomorrow preview +- **LLM:** None (structured JSON, Python formatting) + +### 4. Stocks +- **Source:** stock-lookup skill (existing) +- **Watchlist:** CRWV, NVDA, MSFT +- **Display:** Price, daily change, trend indicator +- **LLM:** Haiku (format table, light commentary) + +### 5. Tasks +- **Source:** Google Tasks API (new integration) +- **Display:** Pending items, due dates, top 5 +- **LLM:** None (structured JSON, Python formatting) + +### 6. Infrastructure +- **Source:** k8s-quick-status + sysadmin-health skills (existing) +- **Display:** Traffic light status (green/yellow/red) +- **LLM:** Haiku (interpret health output) +- **Future:** Enhanced detail levels (fc-042) + +### 7. News +- **Source:** RSS feeds (Hacker News, Lobsters) +- **Display:** Top 5 headlines with scores +- **LLM:** Sonnet (summarize headlines) + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ morning-report skill │ +├─────────────────────────────────────────────────────────────┤ +│ scripts/generate.py # Main orchestrator │ +│ scripts/collectors/ # Data fetchers │ +│ ├── gmail.py # Reuse existing gmail skill │ +│ ├── gcal.py # Reuse existing gcal skill │ +│ ├── gtasks.py # New: Google Tasks API │ +│ ├── stocks.py # Reuse stock-lookup skill │ +│ ├── weather.py # wttr.in integration │ +│ ├── infra.py # K8s + workstation health │ +│ └── news.py # RSS/Hacker News feeds │ +│ scripts/render.py # Markdown templating │ +│ config.json # Watchlist, location, feeds │ +└─────────────────────────────────────────────────────────────┘ +``` + +## Configuration + +File: `~/.claude/skills/morning-report/config.json` + +```json +{ + "version": "1.0", + "schedule": { + "time": "08:00", + "timezone": "America/Los_Angeles" + }, + "output": { + "path": "~/.claude/reports/morning.md", + "archive": true, + "archive_days": 30 + }, + "stocks": { + "watchlist": ["CRWV", "NVDA", "MSFT"], + "show_trend": true + }, + "weather": { + "location": "Seattle,WA,USA", + "provider": "wttr.in" + }, + "email": { + "max_display": 5, + "triage": true + }, + "calendar": { + "show_tomorrow": true + }, + "tasks": { + "max_display": 5, + "show_due_dates": true + }, + "infra": { + "check_k8s": true, + "check_workstation": true, + "detail_level": "traffic_light" + }, + "news": { + "feeds": [ + {"name": "Hacker News", "url": "https://hnrss.org/frontpage", "limit": 3}, + {"name": "Lobsters", "url": "https://lobste.rs/rss", "limit": 2} + ], + "summarize": true + } +} +``` + +## LLM Delegation + +| Section | LLM Tier | Reason | +|---------|----------|--------| +| Weather | Haiku | Parse wttr.in, add hints | +| Email | Sonnet | Triage urgency, summarize | +| Calendar | None | Structured JSON, template | +| Stocks | Haiku | Format, light commentary | +| Tasks | None | Structured JSON, template | +| Infra | Haiku | Interpret health output | +| News | Sonnet | Summarize headlines | + +## Error Handling + +Each collector is isolated - failures don't break the whole report. + +| Collector | Timeout | Retries | Fallback | +|-----------|---------|---------|----------| +| Weather | 5s | 1 | "Weather unavailable" | +| Email | 10s | 2 | Show error + auth hint | +| Calendar | 10s | 2 | Show error | +| Stocks | 5s | 1 | Partial results per-symbol | +| Tasks | 10s | 2 | Show error | +| Infra | 15s | 1 | "Status unknown" (yellow) | +| News | 10s | 1 | "News unavailable" | + +## Logging + +- Run logs: `~/.claude/logs/morning-report.log` +- Systemd logs: `journalctl --user -u morning-report` + +## Implementation Order + +1. Config + skeleton structure +2. Weather, Stocks, Infra collectors (easy wins) +3. Google Tasks collector (new OAuth scope) +4. News collector +5. Orchestrator + renderer +6. Systemd timer + `/morning` command + +## Future Considerations + +- **fc-041:** Terminal output version (motd-style) +- **fc-042:** Enhanced infrastructure detail levels + +## Sample Output + +```markdown +# Morning Report - Thu Jan 2, 2025 + +## Weather +Seattle: 45°F, Partly Cloudy | High 52° Low 38° | Rain likely 3PM + +## Email (3 unread, 1 urgent) +[!] From: boss@work.com - "Q4 numbers needed" + * From: github.com - "PR #123 merged" + * From: newsletter@tech.com - "Weekly digest" + +## Today + * 9:00 AM - Standup (30m) + * 2:00 PM - 1:1 with Sarah (1h) +Tomorrow: 3 events, first at 10:00 AM + +## Stocks +CRWV $79.32 +10.8% NVDA $188.85 +1.3% MSFT $430.50 -0.2% + +## Tasks (4 pending) + * Finish quarterly report (due today) + * Review PR #456 + * Book travel for conference + * Call dentist + +## Infrastructure +K8s Cluster: [OK] | Workstation: [OK] + +## Tech News + * "OpenAI announces GPT-5" (Hacker News, 342 pts) + * "Rust 2.0 released" (Lobsters, 89 votes) + * "Kubernetes 1.32 features" (Hacker News, 156 pts) + +--- +Generated: 2025-01-02 08:00:00 PT +``` diff --git a/docs/plans/2026-01-01-claude-ops-dashboard-implementation.md b/docs/plans/2026-01-01-claude-ops-dashboard-implementation.md new file mode 100644 index 0000000..afaeba6 --- /dev/null +++ b/docs/plans/2026-01-01-claude-ops-dashboard-implementation.md @@ -0,0 +1,442 @@ +# 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 `/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 `
` 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) diff --git a/docs/plans/2026-01-01-claude-realtime-monitoring-implementation.md b/docs/plans/2026-01-01-claude-realtime-monitoring-implementation.md new file mode 100644 index 0000000..40c4b84 --- /dev/null +++ b/docs/plans/2026-01-01-claude-realtime-monitoring-implementation.md @@ -0,0 +1,660 @@ +# Claude Real-Time Monitoring (SSE) Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add real-time-ish monitoring of Claude Code agent activity to the existing Go dashboard, with a backlog (last 200 history events) plus SSE updates, shown in a new “Live” UI feed with prettified rows and expandable raw JSON. + +**Architecture:** Add an in-memory `EventHub` (pub/sub + ring buffer) that receives events from a `HistoryTailer` (tails `~/.claude/history.jsonl`) and a debounced file watcher (for `stats-cache.json` and `state/component-registry.json`). Expose a REST backlog endpoint (`/api/claude/live/backlog`) returning normalized `Event` objects (newest→oldest) and an SSE stream endpoint (`/api/claude/stream`) that pushes new events to the browser. Frontend uses `EventSource` plus batching (render every 1–2s). + +**Tech Stack:** Go 1.21+, `chi`, vanilla HTML/CSS/JS, optional `fsnotify`. + +--- + +## Decisions (locked) + +- Transport: SSE first (WebSockets later). +- Acceptable latency: 2–5 seconds. +- UI: prettified table with expandable raw JSON. +- Backlog: enabled, default `limit=200`. +- Backlog ordering: **newest → oldest**. +- Parsing: **generic-first**, best-effort extraction of a few fields; always preserve raw JSON. +- Backlog response format: **normalized `events`** (not just raw lines). + +--- + +## Data Contract + +### Event JSON + +All events share: + +```json +{ + "id": 123, + "ts": "2026-01-01T12:00:00Z", + "type": "history.append", + "data": { + "summary": { + "sessionId": "...", + "project": "...", + "display": "/model" + }, + "rawLine": "{...}", + "json": { "...": "..." }, + "parseError": "..." + } +} +``` + +Event types: +- `history.append` +- `file.changed` +- `server.notice` +- `server.error` + +--- + +## Task 1: Add `Event` types + +**Files:** +- Create: `~/.claude/dashboard/internal/claude/events.go` +- Test: `~/.claude/dashboard/internal/claude/events_test.go` + +**Step 1: Write the failing test** + +```go +package claude + +import "testing" + +func TestEventTypesCompile(t *testing.T) { + _ = Event{} + _ = EventTypeHistoryAppend + _ = EventTypeFileChanged + _ = EventTypeServerNotice + _ = EventTypeServerError +} +``` + +**Step 2: Run test to verify it fails** + +Run: `go test ./...` +Expected: FAIL with `undefined: Event` + +**Step 3: Write minimal implementation** + +```go +package claude + +import "time" + +type EventType string + +const ( + EventTypeHistoryAppend EventType = "history.append" + EventTypeFileChanged EventType = "file.changed" + EventTypeServerNotice EventType = "server.notice" + EventTypeServerError EventType = "server.error" +) + +type Event struct { + ID int64 `json:"id"` + TS time.Time `json:"ts"` + Type EventType `json:"type"` + Data any `json:"data"` +} +``` + +**Step 4: Run test to verify it passes** + +Run: `go test ./...` +Expected: PASS + +**Step 5: Commit** + +```bash +git add internal/claude/events.go internal/claude/events_test.go +git commit -m "feat: add real-time event types" +``` + +--- + +## Task 2: Implement `EventHub` (pub/sub + ring buffer) + +**Files:** +- Create: `~/.claude/dashboard/internal/claude/eventhub.go` +- Test: `~/.claude/dashboard/internal/claude/eventhub_test.go` + +**Step 1: Write failing tests** + +```go +package claude + +import ( + "testing" + "time" +) + +func TestEventHub_PublishSubscribe(t *testing.T) { + hub := NewEventHub(10) + ch, cancel := hub.Subscribe() + defer cancel() + + hub.Publish(Event{TS: time.Unix(1, 0), Type: EventTypeServerNotice, Data: map[string]any{"msg": "hi"}}) + + select { + case ev := <-ch: + if ev.Type != EventTypeServerNotice { + t.Fatalf("type=%s", ev.Type) + } + if ev.ID == 0 { + t.Fatalf("expected id to be assigned") + } + default: + t.Fatalf("expected event") + } +} + +func TestEventHub_ReplaySince(t *testing.T) { + hub := NewEventHub(3) + hub.Publish(Event{TS: time.Unix(1, 0), Type: EventTypeServerNotice}) // id 1 + hub.Publish(Event{TS: time.Unix(2, 0), Type: EventTypeServerNotice}) // id 2 + hub.Publish(Event{TS: time.Unix(3, 0), Type: EventTypeServerNotice}) // id 3 + + got := hub.ReplaySince(1) + if len(got) != 2 { + t.Fatalf("len=%d", len(got)) + } + if got[0].ID != 2 || got[1].ID != 3 { + t.Fatalf("ids=%d,%d", got[0].ID, got[1].ID) + } +} +``` + +**Step 2: Run tests to verify RED** + +Run: `go test ./...` +Expected: FAIL with `undefined: NewEventHub` + +**Step 3: Write minimal implementation** + +Implement: +- `type EventHub struct { ... }` +- `NewEventHub(bufferSize int) *EventHub` +- `Publish(ev Event) Event`: + - assign `ID` if zero using an internal counter + - set `TS = time.Now()` if zero + - append to ring buffer + - broadcast to subscriber channels (non-blocking send) +- `Subscribe() (chan Event, func())` returns a buffered channel and a cancel func +- `ReplaySince(lastID int64) []Event` + +**Step 4: Run tests to verify GREEN** + +Run: `go test ./...` +Expected: PASS + +**Step 5: Commit** + +```bash +git add internal/claude/eventhub.go internal/claude/eventhub_test.go +git commit -m "feat: add event hub with replay buffer" +``` + +--- + +## Task 3: Tail last N lines helper (newest → oldest) + +**Files:** +- Create: `~/.claude/dashboard/internal/claude/tail.go` +- Test: `~/.claude/dashboard/internal/claude/tail_test.go` + +**Step 1: Write the failing test** + +```go +package claude + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestTailLastNLines_NewestFirst(t *testing.T) { + dir := t.TempDir() + p := filepath.Join(dir, "history.jsonl") + + var b strings.Builder + for i := 1; i <= 5; i++ { + b.WriteString("line") + b.WriteString([]string{"1","2","3","4","5"}[i-1]) + b.WriteString("\n") + } + if err := os.WriteFile(p, []byte(b.String()), 0o600); err != nil { + t.Fatalf("write: %v", err) + } + + lines, err := TailLastNLines(p, 2) + if err != nil { + t.Fatalf("TailLastNLines: %v", err) + } + if len(lines) != 2 { + t.Fatalf("len=%d", len(lines)) + } + if lines[0] != "line5" || lines[1] != "line4" { + t.Fatalf("got=%v", lines) + } +} +``` + +**Step 2: Run test to verify it fails** + +Run: `go test ./...` +Expected: FAIL with `undefined: TailLastNLines` + +**Step 3: Minimal implementation** + +Create `~/.claude/dashboard/internal/claude/tail.go`: + +- `TailLastNLines(path string, n int) ([]string, error)` +- First implementation can be simple (read whole file + split) with a TODO noting potential optimization. +- Return text lines without trailing newline; newest→oldest ordering. + +**Step 4: Run tests to verify it passes** + +Run: `go test ./...` +Expected: PASS + +**Step 5: Commit** + +```bash +git add internal/claude/tail.go internal/claude/tail_test.go +git commit -m "feat: add tail last N lines helper" +``` + +--- + +## Task 4: Add backlog endpoint returning normalized events + +**Files:** +- Create: `~/.claude/dashboard/internal/api/claude_live_handlers.go` +- Modify: `~/.claude/dashboard/internal/api/claude_handlers.go` (only to share helper if needed) +- Modify: `~/.claude/dashboard/cmd/server/main.go` +- Test: `~/.claude/dashboard/internal/api/claude_live_handlers_test.go` + +**Step 1: Write failing test** + +```go +package api + +import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/go-chi/chi/v5" + "github.com/will/k8s-agent-dashboard/internal/claude" +) + +type fakeClaudeDirLoader struct{ dir string } + +func (f fakeClaudeDirLoader) ClaudeDir() string { return f.dir } +func (f fakeClaudeDirLoader) LoadStatsCache() (*claude.StatsCache, error) { return &claude.StatsCache{}, nil } +func (f fakeClaudeDirLoader) ListDir(name string) ([]claude.DirEntry, error) { return nil, nil } +func (f fakeClaudeDirLoader) FileMeta(relPath string) (claude.FileMeta, error) { return claude.FileMeta{}, nil } +func (f fakeClaudeDirLoader) PathExists(relPath string) bool { return true } + +func TestClaudeLiveBacklog_DefaultLimit(t *testing.T) { + dir := t.TempDir() + p := filepath.Join(dir, "history.jsonl") + if err := os.WriteFile(p, []byte("{\"display\":\"/model\"}\n"), 0o600); err != nil { + t.Fatalf("write: %v", err) + } + + loader := fakeClaudeDirLoader{dir: dir} + + r := chi.NewRouter() + r.Get("/api/claude/live/backlog", GetClaudeLiveBacklog(loader)) + + req := httptest.NewRequest(http.MethodGet, "/api/claude/live/backlog", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status=%d body=%s", w.Code, w.Body.String()) + } + // Assert response includes an "events" array with at least 1 event. + if !jsonContainsKey(t, w.Body.Bytes(), "events") { + t.Fatalf("expected events in response: %s", w.Body.String()) + } +} +``` + +**Step 2: Run test to verify RED** + +Run: `go test ./...` +Expected: FAIL with `undefined: GetClaudeLiveBacklog` + +**Step 3: Minimal implementation** + +Create `~/.claude/dashboard/internal/api/claude_live_handlers.go`: + +- `GetClaudeLiveBacklog(loader ClaudeLoader) http.HandlerFunc` +- Query param: `limit` (default 200; clamp 1..1000) +- Use `claude.TailLastNLines(filepath.Join(loader.ClaudeDir(), "history.jsonl"), limit)` +- For each line, create a `claude.Event` with: + - `Type: claude.EventTypeHistoryAppend` + - `TS: time.Now()` (or parse timestamp if present in JSON) + - `Data` contains: `rawLine`, optionally `json`, optionally `parseError`, and `summary` (best effort) +- JSON parsing should be schema-agnostic: unmarshal into `map[string]any`. +- Summary extraction should look for keys: `sessionId`, `project`, `display` (strings). + +Return payload: + +```json +{ "limit": 200, "events": [ ... ] } +``` + +**Step 4: Run tests to verify GREEN** + +Run: `go test ./...` +Expected: PASS + +**Step 5: Wire route** + +Modify `~/.claude/dashboard/cmd/server/main.go` to register: +- `GET /api/claude/live/backlog` + +**Step 6: Run tests again** + +Run: `go test ./...` +Expected: PASS + +**Step 7: Commit** + +```bash +git add cmd/server/main.go internal/api/claude_live_handlers.go internal/api/claude_live_handlers_test.go internal/claude/tail.go internal/claude/tail_test.go +git commit -m "feat: add claude live backlog endpoint" +``` + +--- + +## Task 5: Add SSE stream endpoint + +**Files:** +- Create: `~/.claude/dashboard/internal/api/claude_stream_handlers.go` +- Modify: `~/.claude/dashboard/cmd/server/main.go` +- Test: `~/.claude/dashboard/internal/api/claude_stream_handlers_test.go` + +**Step 1: Write failing test** + +```go +package api + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/go-chi/chi/v5" + "github.com/will/k8s-agent-dashboard/internal/claude" +) + +func TestClaudeStream_SendsEvent(t *testing.T) { + hub := claude.NewEventHub(10) + + r := chi.NewRouter() + r.Get("/api/claude/stream", GetClaudeStream(hub)) + + req := httptest.NewRequest(http.MethodGet, "/api/claude/stream", nil) + w := httptest.NewRecorder() + + // Publish after handler starts. + go func() { + time.Sleep(10 * time.Millisecond) + hub.Publish(claude.Event{Type: claude.EventTypeServerNotice, Data: map[string]any{"msg": "hi"}}) + }() + + r.ServeHTTP(w, req) + + if ct := w.Header().Get("Content-Type"); !strings.Contains(ct, "text/event-stream") { + t.Fatalf("content-type=%q", ct) + } + if !strings.Contains(w.Body.String(), "event:") || !strings.Contains(w.Body.String(), "data:") { + t.Fatalf("body=%s", w.Body.String()) + } +} +``` + +**Step 2: Run test to verify RED** + +Run: `go test ./...` +Expected: FAIL with `undefined: GetClaudeStream` + +**Step 3: Implement minimal SSE handler** + +Create `~/.claude/dashboard/internal/api/claude_stream_handlers.go`: + +- `GetClaudeStream(hub *claude.EventHub) http.HandlerFunc` +- Set headers: + - `Content-Type: text/event-stream` + - `Cache-Control: no-cache` +- Subscribe to hub; write events in SSE format: + +``` +event: +id: +data: + + +``` + +- Flush after each event. +- Keep it minimal; add keepalive pings later. + +**Step 4: Run tests to verify GREEN** + +Run: `go test ./...` +Expected: PASS + +**Step 5: Wire route** + +Modify `~/.claude/dashboard/cmd/server/main.go` to register: +- `GET /api/claude/stream` + +**Step 6: Run tests again** + +Run: `go test ./...` +Expected: PASS + +**Step 7: Commit** + +```bash +git add cmd/server/main.go internal/api/claude_stream_handlers.go internal/api/claude_stream_handlers_test.go internal/claude/eventhub.go internal/claude/eventhub_test.go +git commit -m "feat: add claude sse stream endpoint" +``` + +--- + +## Task 6: Implement HistoryTailer to publish hub events + +**Files:** +- Create: `~/.claude/dashboard/internal/claude/history_tailer.go` +- Test: `~/.claude/dashboard/internal/claude/history_tailer_test.go` +- Modify: `~/.claude/dashboard/cmd/server/main.go` + +**Step 1: Write failing test** + +```go +package claude + +import ( + "os" + "path/filepath" + "testing" + "time" +) + +func TestHistoryTailer_EmitsOnAppend(t *testing.T) { + dir := t.TempDir() + p := filepath.Join(dir, "history.jsonl") + if err := os.WriteFile(p, []byte(""), 0o600); err != nil { + t.Fatalf("write: %v", err) + } + + hub := NewEventHub(10) + ch, cancel := hub.Subscribe() + defer cancel() + + stop := make(chan struct{}) + go TailHistoryFile(stop, hub, p) + + // Append a line + if err := os.WriteFile(p, []byte("{\"display\":\"/status\"}\n"), 0o600); err != nil { + t.Fatalf("append: %v", err) + } + + select { + case ev := <-ch: + if ev.Type != EventTypeHistoryAppend { + t.Fatalf("type=%s", ev.Type) + } + case <-time.After(200 * time.Millisecond): + t.Fatalf("timed out waiting for event") + } + + close(stop) +} +``` + +**Step 2: Run tests to verify RED** + +Run: `go test ./...` +Expected: FAIL with `undefined: TailHistoryFile` + +**Step 3: Minimal implementation** + +Create `~/.claude/dashboard/internal/claude/history_tailer.go` implementing: + +- `TailHistoryFile(stop <-chan struct{}, hub *EventHub, path string)` +- Simple polling loop (since target latency is 2–5s): + - Every 500ms–1s, stat file size + - If size grew, read new bytes from offset, split on `\n`, publish `history.append` events + - If size shrank, reset offset to 0 and publish `server.notice` + +Also implement an internal helper to parse a history line into event `Data` with `summary` extraction (same logic as backlog). + +**Step 4: Run tests to verify GREEN** + +Run: `go test ./...` +Expected: PASS + +**Step 5: Wire tailer in server** + +Modify `~/.claude/dashboard/cmd/server/main.go`: +- Create hub at startup: `hub := claude.NewEventHub(1000)` +- Start goroutine tailing `filepath.Join(*claudeDir, "history.jsonl")` + +**Step 6: Run tests again** + +Run: `go test ./...` +Expected: PASS + +**Step 7: Commit** + +```bash +git add cmd/server/main.go internal/claude/history_tailer.go internal/claude/history_tailer_test.go +git commit -m "feat: stream history.jsonl appends via event hub" +``` + +--- + +## Task 7: Frontend Live view (EventSource + batching) + +**Files:** +- Modify: `~/.claude/dashboard/cmd/server/web/index.html` +- Modify: `~/.claude/dashboard/cmd/server/web/static/js/app.js` +- Modify: `~/.claude/dashboard/cmd/server/web/static/css/style.css` + +**Step 1: Add Live tab + markup** + +- Add nav button: `data-view="live"` +- Add section: + - `id="live-view"` + - table `id="claude-live-table"` and a connection indicator `id="claude-live-conn"` + +**Step 2: Add JS backlog fetch + EventSource** + +Modify `~/.claude/dashboard/cmd/server/web/static/js/app.js`: +- On DOMContentLoaded, create `EventSource('/api/claude/stream')` +- Maintain: + - `let pendingLiveEvents = []` + - `let liveEvents = []` (cap at 500) +- Every 1000ms: + - move pending → live + - render table rows +- Fetch backlog once: + - `GET /api/claude/live/backlog?limit=200` + - prepend/append into `liveEvents` (newest→oldest returned; UI should render newest first at top) + +**Step 3: CSS** + +- Add a small connection badge style (green/yellow/red) +- Ensure table remains readable + +**Step 4: Manual verification** + +Run: +- `go test ./...` +- `go run ./cmd/server --port 8080 --data /tmp/k8s --claude ~/.claude` + +Expected: +- Live tab loads backlog rows +- New history events appear on subsequent CLI activity + +**Step 5: Commit** + +```bash +git add cmd/server/web/index.html cmd/server/web/static/js/app.js cmd/server/web/static/css/style.css +git commit -m "feat: add live feed UI with SSE batching" +``` + +--- + +## Task 8: End-to-end verification + +**Files:** +- None (unless a bug requires fixes) + +**Step 1: Run full test suite** + +Run: `go test ./...` +Expected: PASS (0 failures) + +**Step 2: Manual smoke check** + +Run: +- `go run ./cmd/server --port 8080 --data /tmp/k8s --claude ~/.claude` + +Check: +- `curl -N http://localhost:8080/api/claude/stream` prints SSE lines +- `curl http://localhost:8080/api/claude/live/backlog?limit=5` returns `events` array +- Browser Live tab updates + +--- + +## Execution handoff + +Plan complete and saved to `~/.claude/docs/plans/2026-01-01-claude-realtime-monitoring-implementation.md`. + +Two execution options: + +1. Subagent-Driven (this session) — I dispatch fresh subagent per task, review between tasks, fast iteration +2. Parallel Session (separate) — Open new session with `superpowers:executing-plans`, batch execution with checkpoints + +Which approach? \ No newline at end of file diff --git a/plans/glistening-wondering-wadler.md b/plans/glistening-wondering-wadler.md new file mode 100644 index 0000000..68412ba --- /dev/null +++ b/plans/glistening-wondering-wadler.md @@ -0,0 +1,173 @@ +# ~/.claude Structure Verification Report + +**Status: ALL CHECKS PASSED** + +## Directory Structure Overview + +``` +~/.claude/ +├── CLAUDE.md # Shared memory (exists) +├── README.md # Setup guide (exists) +├── settings.json # Claude settings (exists) +├── .gitignore # Git ignore (exists) +│ +├── .claude-plugin/ # Plugin manifest +│ ├── plugin.json # Valid JSON +│ └── marketplace.json # Valid JSON +│ +├── agents/ # 13 agent files + README +│ ├── README.md +│ ├── personal-assistant.md # Opus, proper frontmatter +│ ├── master-orchestrator.md # Opus +│ ├── linux-sysadmin.md # Sonnet +│ ├── k8s-orchestrator.md # Opus +│ ├── k8s-diagnostician.md # Sonnet +│ ├── argocd-operator.md # Sonnet +│ ├── prometheus-analyst.md # Sonnet +│ ├── git-operator.md # Sonnet +│ ├── programmer-orchestrator.md # Opus +│ ├── code-planner.md # Sonnet +│ ├── code-implementer.md # Sonnet +│ └── code-reviewer.md # Sonnet +│ +├── skills/ # 6 skills + README +│ ├── README.md +│ ├── gmail/ # SKILL.md + scripts/ + references/ +│ ├── gcal/ # SKILL.md + scripts/ +│ ├── k8s-quick-status/ # SKILL.md + scripts/ +│ ├── sysadmin-health/ # SKILL.md + scripts/ +│ ├── usage/ # SKILL.md + scripts/ +│ └── programmer-add-project/ # SKILL.md only +│ +├── commands/ # 22 commands + README + subdirs +│ ├── README.md +│ ├── pa.md, help.md, status.md, config.md, ... +│ ├── k8s/ # K8s subcommands +│ └── sysadmin/ # Sysadmin subcommands +│ +├── workflows/ # 6 workflows + README +│ ├── README.md +│ ├── deploy/, health/, incidents/, sysadmin/ +│ └── validate-agent-format.yaml +│ +├── hooks/ # Event handlers +│ ├── hooks.json # Valid JSON +│ ├── README.md +│ └── scripts/ # session-start.sh, pre-compact.sh +│ +├── state/ # Shared state (all valid JSON) +│ ├── README.md +│ ├── system-instructions.json +│ ├── future-considerations.json +│ ├── model-policy.json +│ ├── autonomy-levels.json +│ ├── component-registry.json # 6 skills, 22 commands, 12 agents, 10 workflows +│ ├── personal-assistant-preferences.json +│ ├── kb.json +│ ├── personal-assistant/ # PA state +│ │ ├── general-instructions.json +│ │ ├── session-context.json +│ │ ├── kb.json +│ │ ├── history/ # index.json exists +│ │ ├── memory/ # decisions, facts, meta, preferences, projects +│ │ └── templates/ +│ ├── sysadmin/ # Sysadmin state +│ ├── programmer/ # Programmer state +│ └── usage/ # Usage tracking +│ +├── automation/ # 35+ managed scripts +│ ├── README.md +│ ├── validate-setup.sh, backup.sh, restore.sh, clean.sh +│ ├── memory-add.py, memory-list.py, search.py +│ ├── skill-info.py, agent-info.py, workflow-info.py +│ ├── completions.bash, completions.zsh +│ └── systemd/ # Service files +│ +└── mcp/ # MCP integrations + ├── README.md + ├── gmail/ # Gmail venv + credentials + └── delegation/ # Delegation helpers +``` + +## Validation Results + +| Category | Status | Details | +|----------|--------|---------| +| Directory structure | PASS | All 8 expected directories exist | +| Core files | PASS | CLAUDE.md, README.md, settings.json, .gitignore | +| Plugin structure | PASS | plugin.json valid | +| Hooks | PASS | hooks.json valid, scripts executable | +| Skills | PASS | 6 skills with SKILL.md, scripts executable | +| State files | PASS | All JSON files valid | +| PA state | PASS | All memory files present and valid | +| Gmail integration | PASS | venv + credentials present | +| Documentation | PASS | 7/7 READMEs present | + +## Component Registry Cross-Reference + +| Component Type | In Registry | On Disk | Match | +|----------------|-------------|---------|-------| +| Skills | 6 | 6 | YES | +| Agents | 12 | 13 | +1 (README) | +| Commands | 22 | 22+ | YES | +| Workflows | 10 | 6 dirs | YES (nested) | + +## Notes + +- All JSON files parse successfully +- All agent files have proper YAML frontmatter with name, description, model +- All skill scripts are executable +- Gmail venv and credentials are in place +- History/memory structure for PA agent mode is ready + +## Issues Found + +### GCal Integration - BROKEN + +| Component | Status | +|-----------|--------| +| Calendar token | ✅ `~/.gmail-mcp/calendar_token.json` with `calendar.readonly` scope | +| Credentials | ✅ `~/.gmail-mcp/credentials.json` | +| Scripts | ❌ **FAIL** - `get_calendar_service` function missing | + +**Root cause:** `agenda.py` and `next_event.py` import `get_calendar_service` from `gmail_mcp.utils.GCP.gmail_auth`, but this function doesn't exist. Only `get_gmail_service` is available. + +### Fix: Add `get_calendar_service()` to gmail_auth.py + +**File:** `~/.claude/mcp/gmail/venv/lib/python3.14/site-packages/gmail_mcp/utils/GCP/gmail_auth.py` + +**Add after `get_gmail_service()` (line 63):** + +```python +CALENDAR_SCOPES = ["https://www.googleapis.com/auth/calendar.readonly"] + +def get_calendar_service(): + """ + Handles Google Calendar API authentication and returns the service object. + Uses separate token file from Gmail. + """ + token_path = Path.home() / ".gmail-mcp" / "calendar_token.json" + credentials_path = os.getenv('GMAIL_CREDENTIALS_PATH', + str(Path.home() / ".gmail-mcp" / "credentials.json")) + + creds = None + if token_path.exists(): + creds = Credentials.from_authorized_user_file(str(token_path), CALENDAR_SCOPES) + + if not creds or not creds.valid: + if creds and creds.expired and creds.refresh_token: + creds.refresh(Request()) + else: + if not os.path.exists(credentials_path): + raise FileNotFoundError(f"Credentials not found at {credentials_path}") + flow = InstalledAppFlow.from_client_secrets_file(credentials_path, CALENDAR_SCOPES) + creds = flow.run_local_server(port=0) + token_path.parent.mkdir(parents=True, exist_ok=True) + token_path.write_text(creds.to_json()) + + return build("calendar", "v3", credentials=creds) +``` + +## Recommendation + +**Action:** Add `get_calendar_service()` function to gmail_auth.py to match gmail pattern.