Add design plans for dashboard integration

Add implementation plans for morning report, Claude ops dashboard, and realtime monitoring features.
This commit is contained in:
OpenCode Test
2026-01-03 10:55:22 -08:00
parent 6ef58472cf
commit e43e052a32
4 changed files with 1488 additions and 0 deletions

View File

@@ -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
```

View File

@@ -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 (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)

View File

@@ -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 12s).
**Tech Stack:** Go 1.21+, `chi`, vanilla HTML/CSS/JS, optional `fsnotify`.
---
## Decisions (locked)
- Transport: SSE first (WebSockets later).
- Acceptable latency: 25 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: <type>
id: <id>
data: <json>
```
- 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 25s):
- Every 500ms1s, 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?