Add design plans for dashboard integration
Add implementation plans for morning report, Claude ops dashboard, and realtime monitoring features.
This commit is contained in:
213
docs/plans/2025-01-02-morning-report-design.md
Normal file
213
docs/plans/2025-01-02-morning-report-design.md
Normal 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
|
||||
```
|
||||
442
docs/plans/2026-01-01-claude-ops-dashboard-implementation.md
Normal file
442
docs/plans/2026-01-01-claude-ops-dashboard-implementation.md
Normal 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 (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)
|
||||
@@ -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: <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 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?
|
||||
Reference in New Issue
Block a user