Files
porthole/@PLAN.md
OpenCode Test e95536c9f1 docs: add project planning and implementation documentation
Add PLAN.md with full project specification and @PLAN.md with
parallel build plan for multi-agent implementation. Also add
IMPLEMENTATION_SUMMARY.md documenting Tasks W, X, Y, Z completion
(rollup drill-down, theme toggle, P0 alerts).
2025-12-24 13:30:35 -08:00

916 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# @PLAN — ControlTower (Go TUI) parallel build plan
This file breaks the project into **independent, parallel tasks** that multiple agents can implement with minimal merge conflicts.
- Source spec: `PLAN.md`
- **Sub-agent models to use:**
- `anthropic/claude-haiku-4-5-20251001`
- `github-copilot/claude-haiku-4.5`
## 0) Ground rules (so work composes cleanly)
### 0.1 File ownership
Each task below “owns” a set of files. Agents should **only modify files they own**, except for the explicit shared-contract files listed next.
### 0.2 Shared contracts (edit carefully)
These files are the primary integration points; keep them stable and coordinate changes:
- `internal/model/issue.go` (Issue model + enums)
- `internal/collectors/collector.go` (collector interface + status)
- `cmd/controltower/main.go` (wiring + flags)
### 0.3 Go module + imports
- Use standard package layout from `PLAN.md`.
- No persistence; acknowledgements are in-memory only.
- Prefer small packages with narrow responsibilities.
## 1) Proposed repo layout (target)
- `cmd/controltower/main.go`
- `internal/model/` (Issue + enums)
- `internal/store/` (IssueStore)
- `internal/engine/` (scheduler + concurrency)
- `internal/collectors/collector.go` (shared interface)
- `internal/collectors/host/` (`disk.go`, `mem.go`, `load.go`, `net.go`)
- `internal/collectors/k8s/` (`client.go`, `informers.go`, `issues_*.go`, `rollup.go`, `unreachable.go`)
- `internal/ui/` (`app.go`, `table.go`, `details.go`, `keys.go`, `styles.go`)
- `internal/export/json.go`
## 2) Integration contracts (must align across tasks)
### 2.1 Issue model contract
`internal/model.Issue` must support (per `PLAN.md`):
- Stable `ID` for dedupe
- `Category`, `Priority`, `State` enums
- `Title`, `Details`, `Evidence` (key/value), `SuggestedFix`
- `FirstSeen`, `LastSeen`
Sorting defaults:
- `Priority` desc → `LastSeen` desc (recency)
### 2.2 Collector contract
Create `internal/collectors/collector.go` with:
- `type Collector interface { Name() string; Interval() time.Duration; Collect(ctx context.Context) ([]model.Issue, Status, error) }`
- `type Status` describing health (OK/Degraded/Error + message + last success time)
Collectors should:
- Be fast; respect context cancellation.
- Return issues that are “currently true” for this tick (the store handles resolve-after).
### 2.3 Store contract
`IssueStore.Upsert(now, issues)`:
- Dedupes by `Issue.ID`
- Tracks `FirstSeen`/`LastSeen`
- Maintains `State` (Open/Acknowledged/Resolved)
- Resolves only after `resolveAfter` (default 30s) of absence
Ack/unack should be in-memory and keyed by Issue ID.
### 2.4 Engine contract
Engine runs all collectors concurrently on their intervals with timeouts:
- Host collectors: `collector_timeout_fast` (250ms)
- K8s list/poll: `collector_timeout_k8s_list` (2s)
Engine produces periodic snapshots for UI/export:
- `[]model.Issue` sorted per default
- Collector health summary (Host OK / K8s OK/DEGRADED)
## 3) Task breakdown (parallelizable)
Each task includes: scope, owned files, external dependencies, and acceptance checks.
### Task A — Project scaffold + wiring (1 agent)
**Goal:** create runnable binary, shared contracts, and minimal plumbing.
- Owns:
- `go.mod`, `go.sum`
- `cmd/controltower/main.go`
- `internal/collectors/collector.go`
- (Optional) `internal/buildinfo/` or version constants
- Provides:
- CLI flag `--export /path/issues.json` (calls exporter and exits)
- Default config constants (refresh interval 1s, resolveAfter 30s, timeouts)
- K8s enabled only if kubeconfig exists (per spec)
- Must not implement heavy logic from other tasks; stub as needed.
**Acceptance:** `go test ./...` passes; `go run ./cmd/controltower` starts.
---
### Task B — Core model package (1 agent)
**Goal:** implement the type system used everywhere.
- Owns:
- `internal/model/issue.go` (+ small helper files if needed)
- Implements:
- Enums: `Category`, `Priority`, `State` (stringers / JSON marshal if needed)
- `Issue` struct + helpers:
- `func (p Priority) Weight() int` (for sorting)
- `func (i Issue) Age(now time.Time) time.Duration`
- `func SortIssuesDefault([]Issue)`
- Evidence representation: `map[string]string` (simple, JSON-friendly).
**Acceptance:** unit tests for sorting + JSON marshalling (if added).
---
### Task C — IssueStore (1 agent)
**Goal:** dedupe, state transitions, resolve-after, flap suppression.
- Owns:
- `internal/store/store.go`
- `internal/store/store_test.go`
- Needs:
- `resolveAfter` default 30s
- Flap suppression: dont resolve on a single missed tick
- `Acknowledge(id)`, `Unack(id)` (in-memory only)
- `Snapshot()` returning issues (optionally copy to avoid races)
**Acceptance:** tests covering:
- Dedup and timestamp updates
- Ack persists while issue present
- Resolve-after triggers only after absence window
---
### Task D — Engine scheduler (1 agent)
**Goal:** concurrency orchestration + snapshot publication.
- Owns:
- `internal/engine/engine.go`
- `internal/engine/engine_test.go`
- Integrates:
- Collectors from Task E (host) and Task F (k8s)
- Store from Task C
- Produces:
- periodic snapshots (channel or callback) for UI
- immediate refresh on demand (key `r`)
- Concurrency:
- Run collectors on their own tickers
- Per-collector context timeouts
- Track last error/health per collector
**Acceptance:** deterministic tests with fake collectors verifying:
- timeout handling
- interval scheduling
- merge/upsert calls
---
### Task E — Host collectors MVP (12 agents)
Split into sub-tasks to avoid conflicts:
#### E1 Disk collector
- Owns: `internal/collectors/host/disk.go` (+ tests)
- Reads `/proc/mounts`, uses `statfs`.
- Filters pseudo filesystems.
- Thresholds:
- `P1` if usage or inode usage ≥ 92%
- `P0` if ≥ 98%
#### E2 Memory + swap collector
- Owns: `internal/collectors/host/mem.go` (+ tests)
- Reads `/proc/meminfo`.
- Applies thresholds in `PLAN.md`.
#### E3 Load collector
- Owns: `internal/collectors/host/load.go` (+ tests)
- Reads `/proc/loadavg` + CPU count.
- Sustained windows: track 120s rolling window.
#### E4 Network collector
- Owns: `internal/collectors/host/net.go` (+ tests)
- Reads `/proc/net/route` for default route.
- Reads `/sys/class/net/*/operstate` for “any UP”.
- `P1` if no default route AND any non-loopback UP.
**Acceptance:** each collector has unit tests using fixture files or temp fakes.
---
### Task F — Kubernetes collector MVP (2 agents recommended)
This is the largest area; split by responsibility.
#### F1 K8s client + connectivity + unreachable grace
- Owns:
- `internal/collectors/k8s/client.go`
- `internal/collectors/k8s/unreachable.go`
- Responsibilities:
- Build client-go config from kubeconfig current context.
- Connectivity tracking: emit `Kubernetes/P0` only after **10s continuous failure**.
- RBAC errors: emit single `P2` “Insufficient RBAC …” issue per resource.
#### F2 Informers / polling fallback + issue rules + rollups
- Owns:
- `internal/collectors/k8s/informers.go`
- `internal/collectors/k8s/issues_pods.go`
- `internal/collectors/k8s/issues_nodes.go`
- `internal/collectors/k8s/issues_workloads.go`
- `internal/collectors/k8s/issues_events.go`
- `internal/collectors/k8s/rollup.go`
- Responsibilities:
- Prefer informers (LIST+WATCH caches).
- If watch fails repeatedly, degrade to periodic LIST and emit a `P1` degraded issue.
- High-signal rules per `PLAN.md`.
- Rollups:
- group by `(namespace, reason, kind)`
- if group size ≥ 20 emit rollup issue with samples
- Cap: `k8s.max_issues = 200` after rollups.
**Acceptance:**
- Unit tests for rule functions (pure logic tests with fake objects)
- Manual run against a cluster (optional) verifies unreachable grace.
---
### Task G — Bubble Tea TUI (2 agents recommended)
Split UI work to reduce merge conflicts.
#### G1 App model + layout framing
- Owns:
- `internal/ui/app.go`
- `internal/ui/styles.go`
- Responsibilities:
- Header bar (host/time/age/P0..P3 counts/collector health)
- Layout: header + table + details pane
- Wiring to engine snapshot updates (tea.Msg)
#### G2 Table + details + keybindings
- Owns:
- `internal/ui/table.go`
- `internal/ui/details.go`
- `internal/ui/keys.go`
- Responsibilities:
- Main table columns: `Pri | Cat | Title | Age | State`
- Search `/`, filter `p`/`c`, sort `s`, focus `tab`
- Ack/unack `a`
- Optional: Expand details with `enter`
**Acceptance:**
- UI runs and remains responsive with 1k synthetic issues.
---
### Task H — Exporter (1 agent)
**Goal:** JSON snapshot export, no persistence.
- Owns:
- `internal/export/json.go`
- Implements:
- `WriteIssues(path string, issues []model.Issue) error`
- Used by `--export` and (optional) UI hotkey `E`
**Acceptance:** `--export` writes valid JSON and exits 0.
---
### Task I — End-to-end integration pass (1 agent)
**Goal:** compose all pieces, fix mismatches, and ensure acceptance criteria.
- Owns (only light edits across packages):
- small wiring fixes in `cmd/controltower/main.go`
- integration tweaks in engine/UI
- Validates:
- Without kubeconfig: app runs, host collectors work
- With kubeconfig but unreachable cluster: `P0` appears only after 10s
- With reachable cluster: issues populate; rollups protect UI
## 4) 8-agent assignment (recommended)
This assignment uses exactly **8 agents** and keeps overlaps minimal. For each agent, you can use either model:
- `anthropic/claude-haiku-4-5-20251001`
- `github-copilot/claude-haiku-4.5`
**Phase 1 (parallel, no blocking):** Agents 16
- **Agent 1 (Scaffold/contracts):** Task A
- **Agent 2 (Model):** Task B
- **Agent 3 (Store):** Task C
- **Agent 4 (Engine):** Task D
- **Agent 5 (Host collectors):** Task E1E4 (own all `internal/collectors/host/*`)
- **Agent 6 (Exporter):** Task H
**Phase 2 (parallel, after A+B exist):** Agents 78
- **Agent 7 (Kubernetes collector):** Task F1 + F2 (own all `internal/collectors/k8s/*`)
- **Agent 8 (TUI + integration):** Task G1 + G2 + Task I (UI owns `internal/ui/*`; integration touches wiring only)
Notes:
- Agent 5 is bundled intentionally to avoid conflicts in `internal/collectors/host/`.
- K8s (Agent 7) is bundled intentionally to avoid informer/client contract churn.
- UI + integration (Agent 8) is bundled so UI/engine message types stay aligned.
**Start order (min waiting):**
- Start Agents **16** immediately.
- Start Agents **78** once Agent 1 has created `go.mod` + `internal/collectors/collector.go` and Agent 2 has created `internal/model/issue.go` (so imports compile).
**Merge order (min conflicts / keep build green):**
1) Agent 1 (scaffold/contracts) — establishes module + shared interfaces
2) Agent 2 (model) — unblocks compilation for most packages
3) Agent 3 (store) + Agent 6 (export) — mostly isolated, compile-time checks
4) Agent 4 (engine) — depends on store/model/contracts
5) Agent 5 (host collectors) — depends on model/contracts; can land anytime after (1,2)
6) Agent 7 (k8s) — depends on model/contracts; may need go.sum updates
7) Agent 8 (UI + integration) — land last; touches wiring and stabilizes API edges
**If you use a single shared branch:**
- Prefer merging frequently in the above order and rebasing lagging work onto the latest `main`.
- If a shared contract must change, have Agent 1 do the change and notify all other agents.
## 5) Suggested agent prompts (copy/paste)
Use each prompt with model `anthropic/claude-haiku-4-5-20251001` or `github-copilot/claude-haiku-4.5`.
### Prompt template
- Implement only the task scope.
- Touch only owned files.
- Add small unit tests where indicated.
- Keep APIs aligned with the shared contracts.
#### Agent prompt: Task B (model)
"Implement `internal/model/issue.go` per @PLAN.md §2.1. Add sorting helpers and minimal tests. Do not modify other packages."
#### Agent prompt: Task C (store)
"Implement `internal/store` IssueStore with dedupe, ack/unack, resolve-after (30s), flap suppression, and tests. Import and use `internal/model`."
#### Agent prompt: Task D (engine)
"Implement `internal/engine` scheduler running collectors concurrently with per-collector timeouts and health tracking. Provide snapshot publication for UI; add tests with fake collectors."
#### Agent prompt: Task E1E4 (host collectors)
"Implement the specified host collector in `internal/collectors/host/*.go` reading Linux proc/sys files as described. Emit Issues with correct IDs/evidence/priority and add small tests using fixtures/mocks."
#### Agent prompt: Task F1 (k8s base)
"Implement kubeconfig client creation and unreachable grace logic. Emit a single P0 issue only after 10s continuous failure; emit P2 on RBAC denial. Keep rule logic separate."
#### Agent prompt: Task F2 (k8s rules + rollups)
"Implement informer/polling collector and rule + rollup logic with caps. Keep rule functions testable. Coordinate only via shared contracts."
#### Agent prompt: Task G1/G2 (UI)
"Implement Bubble Tea UI pieces per @PLAN.md: header/table/details/keybindings. Subscribe to engine snapshots and keep rendering cheap."
#### Agent prompt: Task H (export)
"Implement JSON exporter used by `--export` and optional UI hotkey. Ensure output includes all Issue fields."
#### Agent prompt: Task I (integration)
"Integrate all packages, resolve API mismatches, and verify acceptance scenarios. Keep changes minimal and avoid refactors."
#### Agent prompt: Task J (Help overlay)
"Implement in-app help overlay showing all keybindings. Add `?` key binding in keys.go and create help.go with overlay model and render logic."
#### Agent prompt: Task K (Direct priority jump)
"Add numeric bindings `0`/`1`/`2`/`3` to jump directly to P0/P1/P2/P3 filter. Keep `p` cycle for `all`."
#### Agent prompt: Task L (Visual filter indicators)
"Add highlighted style for active filters in header. Apply FilterActive style when pri= or cat= are set."
#### Agent prompt: Task M (Better empty state)
"Show friendly message 'All systems healthy' when no issues instead of just 'No issues'."
#### Agent prompt: Task N (Bulk ack all visible)
"Add `A` key to acknowledge all currently visible issues at once. Optimize updates and refresh counts."
#### Agent prompt: Task O (Page navigation)
"Document page up/down keys already supported by bubbles/table. Ensure they work correctly in Update handler."
#### Agent prompt: Task P (Home/End jump)
"Add `g`/`G` bindings to jump to first/last row quickly like vim/less."
#### Agent prompt: Task Q (Vi-style nav)
"Add `j`/`k` bindings as alternatives to arrow keys for down/up navigation."
#### Agent prompt: Task R (Collector health icons)
"Replace text 'ok=1 deg=0 err=Z' with visual icons (✓ ⚠ ✗). Use lipgloss styles."
#### Agent prompt: Task S (Issue count warning near cap)
"Add warning indicator in header when approaching 200-issue cap (show at >= 180)."
#### Agent prompt: Task T (Copy to clipboard)
"Add `y` key to copy SuggestedFix (or Title) to system clipboard. Add atotto/clipboard dependency."
#### Agent prompt: Task U (Dynamic column width)
"Add `t` key to toggle between compact and wide Title column widths for long issue titles."
#### Agent prompt: Task V (Age format toggle)
"Add `d` key to toggle between compact (2dm) and relative (2m ago) age formats."
#### Agent prompt: Task W (Rollup drill-down)
"When rollup issue selected, show 'Affected Issues' list with sample IDs in details pane."
#### Agent prompt: Task X (Light/dark theme toggle)
"Add `T` key to toggle between light and dark themes. Define two theme sets in styles.go."
#### Agent prompt: Task Y (Sound/flash on new P0)
"Add terminal bell/flash when P0 count increases (new critical issue detected). Respect NO_BELL env var."
#### Agent prompt: Task Z (Theme, sound, flash)
"Add theme toggle (`T`) and P0 alert (bell/flash) with env var support. Test with `NO_BELL=1`."
## 6) Definition of done
- Meets `PLAN.md` Acceptance Criteria.
- `go test ./...` passes.
- `go run ./cmd/controltower` works without kubeconfig.
- With kubeconfig and unreachable cluster: `P0` only after 10s.
- UI remains responsive under high issue counts (rollups + caps).
## 7) TUI UX Improvements
This section defines UX enhancements that improve discoverability, navigation efficiency, and visual clarity. Tasks are designed for **parallel agent execution** (independent file ownership).
### 7.1 Quick wins (high-impact, low-effort)
#### Task J — Help overlay
**Goal:** Users can discover all keybindings in-app without external docs.
**Agent:** 1 (can be same agent that implements Task J in Phase 2)
**Owned files:**
- `internal/ui/help.go` (new)
- `internal/ui/keys.go` (add `Help` binding, update prompts)
- `internal/ui/app.go` (add help model + view routing)
**Implementation:**
- Add `Help` key binding (`?`) to `KeyMap`
- Create `help.go` with a `Model` struct:
- List of keybindings by group (Global, Filters, Navigation, Actions)
- `render()` function to display as overlay
- Add `helpRequestedMsg` type in `app.go` to trigger help overlay
- Update `keys.go`: add `Help` binding with help text
- Update `app.go`:
- Add `showHelp bool` field to Model
- Handle `?` key: toggle help overlay (hide table/details)
- Update `View()` to conditionally show help instead of main layout
**Acceptance:**
- Press `?` shows keybinding list overlay covering main layout
- Press `?` again or `esc` dismisses help
- `go test ./...` passes
---
#### Task K — Direct priority jump (`0`/`1`/`2`/`3`)
**Goal:** Jump directly to P0/P1/P2/P3 filter instead of cycling through all 5 options.
**Agent:** 1
**Owned files:**
- `internal/ui/keys.go` (add numeric bindings)
- `internal/ui/app.go` (update Update handler)
**Implementation:**
- Add bindings in `keys.go`:
- `PriorityP0: key.NewBinding(key.WithKeys("0"), key.WithHelp("0", "P0 only"))`
- `PriorityP1: key.NewBinding(key.WithKeys("1"), key.WithHelp("1", "P1 only"))`
- `PriorityP2: key.NewBinding(key.WithKeys("2"), key.WithHelp("2", "P2 only"))`
- `PriorityP3: key.NewBinding(key.WithKeys("3"), key.WithHelp("3", "P3 only"))`
- Update `app.go` Update handler:
- Match `keyMatch(msg, m.keys.PriorityP0)` → set `m.filterPri = model.PriorityP0`
- Match `keyMatch(msg, m.keys.PriorityP1)` → set `m.filterPri = model.PriorityP1`
- Match `keyMatch(msg, m.keys.PriorityP2)` → set `m.filterPri = model.PriorityP2`
- Match `keyMatch(msg, m.keys.PriorityP3)` → set `m.filterPri = model.PriorityP3`
- Keep existing `p` cycle behavior for `all` option
**Acceptance:**
- Pressing `0` shows only P0 issues; `1` shows only P1, etc.
- Pressing `p` cycles through: all → P0 → P1 → P2 → P3 → all → …
---
#### Task L — Visual filter indicators
**Goal:** Make active filters visually obvious in header (highlight vs. plain text).
**Agent:** 1
**Owned files:**
- `internal/ui/styles.go` (add filter highlight style)
- `internal/ui/app.go` (update `renderHeader()`)
**Implementation:**
- Add style in `styles.go`:
- `FilterActive lipgloss.Style` (e.g., bold + distinct foreground)
- Update `renderHeader()` in `app.go`:
- Apply `FilterActive` style to `pri=` or `cat=` portions when filter is active
- Keep `all` in plain `HeaderVal` style
**Acceptance:**
- `pri=P1` or `cat=Kubernetes` appears highlighted/colored in header
- `pri=all` or `cat=all` appears in plain text
---
#### Task M — Better empty state
**Goal:** When no issues, show helpful message instead of just “No issues”.
**Agent:** 1
**Owned files:**
- `internal/ui/app.go` (update `applyViewFromSnapshot()`)
**Implementation:**
- In `applyViewFromSnapshot()` (around row building), when `len(filtered) == 0`:
- Set details content to friendly message: “All systems healthy. No issues detected.”
- Optionally: add hint like “Press `r` to refresh, `/` to search past logs”
**Acceptance:**
- When snapshot has 0 issues, details pane shows friendly message
- Header still displays correct P0/P1/P2/P3 counts (all 0)
---
#### Task N — Bulk ack all visible
**Goal:** Allow user to acknowledge all currently visible issues at once.
**Agent:** 1
**Owned files:**
- `internal/ui/keys.go` (add bulk ack binding)
- `internal/ui/app.go` (add `AckAllVisible()` method + handler)
**Implementation:**
- Add binding in `keys.go`:
- `AckAll: key.NewBinding(key.WithKeys("A"), key.WithHelp("A", "ack all visible"))` (shift+a)
- Add method in `app.go`:
- `func (m *Model) AckAllVisible()`
- Iterate over `m.rowsIDs`, call `m.ack(id)` for each
- Optimistically update `issueByID` and table rows
- Update `Update()` handler:
- Match `keyMatch(msg, m.keys.AckAll)` → call `m.AckAllVisible()`
**Acceptance:**
- Pressing `A` acknowledges all issues currently visible in table
- Header P0/P1/P2/P3 counts remain correct
- `a` still toggles ack/unack for single selected issue
---
### 7.2 Navigation efficiency improvements
#### Task O — Page navigation (`PgUp`/`PgDn` or `Ctrl+u`/`Ctrl+d`)
**Goal:** Faster scrolling through long issue lists (200+ issues with rollups).
**Agent:** 1
**Owned files:**
- `internal/ui/table.go` (no changes needed; bubbles/table supports page keys by default)
**Implementation:**
- Bubbles/table already supports standard page keys:
- `pgup`/`pgdn`
- `ctrl+u`/`ctrl+d`
- `home`/`end`
- Ensure these are not overridden in `app.go` Update handler
- Optionally: update help overlay to document these keys
**Acceptance:**
- PageUp/PageDown jumps 10 rows in table
- Ctrl+u/Ctrl+d jumps half a screen
- Home/End jump to top/bottom
---
#### Task P — Home/End jump (`g`/`G` like vim/less)
**Goal:** Jump to first or last row quickly.
**Agent:** 1
**Owned files:**
- `internal/ui/keys.go` (add bindings)
- `internal/ui/app.go` (add handlers)
**Implementation:**
- Add bindings in `keys.go`:
- `JumpToTop: key.NewBinding(key.WithKeys("g"), key.WithHelp("g", "jump to first"))`
- `JumpToBottom: key.NewBinding(key.WithKeys("G"), key.WithHelp("G", "jump to last"))`
- Update `app.go`:
- Handle `JumpToTop` → set cursor to 0
- Handle `JumpToBottom` → set cursor to last row
**Acceptance:**
- Pressing `g` moves selection to first visible row
- Pressing `G` moves selection to last visible row
---
#### Task Q — Vi-style nav (`j`/`k` in addition to arrows)
**Goal:** Support vim-style navigation for users who prefer hjkl.
**Agent:** 1
**Owned files:**
- `internal/ui/keys.go` (add bindings)
- `internal/ui/app.go` (add handlers)
**Implementation:**
- Add bindings in `keys.go`:
- `Down: key.NewBinding(key.WithKeys("j"), key.WithHelp("j", "down"))`
- `Up: key.NewBinding(key.WithKeys("k"), key.WithHelp("k", "up"))`
- Update `app.go`:
- Match `Down` and pass to `m.table.Update(msg)` (bubbles/table handles both arrows and j/k)
- Update help overlay to document `j`/`k` as alternatives to arrows
**Acceptance:**
- `j` moves selection down, `k` moves up
- Arrows continue to work (both styles supported)
---
### 7.3 Visual polish & feedback
#### Task R — Collector health icons
**Goal:** Replace text “ok=1 deg=0 err=Z” with visual status icons.
**Agent:** 1
**Owned files:**
- `internal/ui/app.go` (update `renderHeader()`)
- `internal/ui/styles.go` (add icon styles if desired)
**Implementation:**
- Update `renderHeader()` health display:
- Use icon instead of text:
- `✓` for OK
- `⚠` for DEGRADED
- `✗` for ERROR
- Format: `collectors: ✓1 ⚠0 ✗0`
- Optional: use `lipgloss.SetGlyphWidth()` if icons appear misaligned
**Acceptance:**
- Header shows icon-based collector health (e.g., `collectors: ✓4`)
- Status still readable for screen readers (icons are supplementary)
---
#### Task S — Issue count warning near cap
**Goal:** Warn users when approaching the 200-issue cap (after rollups).
**Agent:** 1
**Owned files:**
- `internal/ui/app.go` (update `renderHeader()`)
**Implementation:**
- In `renderHeader()`, calculate total issues: `total := p0 + p1 + p2 + p3`
- If `total >= 180` (90% of cap):
- Append warning text to collector health line: ` [~180/200]`
- Use warning color from `styles.Error`
- Only show when near cap; dont clutter at low counts
**Acceptance:**
- When near 200 issues, header shows warning indicator
- Below 180 issues, no warning appears
---
#### Task T — Copy to clipboard (`y` on selected issue)
**Goal:** Quick copy of issue ID, Title, or SuggestedFix.
**Agent:** 1
**Owned files:**
- `internal/ui/keys.go` (add binding)
- `internal/ui/app.go` (add handler + clipboard dep if needed)
- `go.mod` (add clipboard dependency)
**Implementation:**
- Add binding in `keys.go`:
- `Copy: key.NewBinding(key.WithKeys("y"), key.WithHelp("y", "copy to clipboard"))`
- Add dependency in `go.mod` (pick one):
- `github.com/atotto/clipboard` (cross-platform, widely used)
- or use xclip/xsel via `exec.Command` for linux-only (no new dep)
- Update `app.go`:
- On `y` key, copy selected issues `SuggestedFix` to clipboard (most useful field)
- Show brief confirmation in details pane: “Copied to clipboard”
**Acceptance:**
- Pressing `y` copies SuggestedFix (or Title) to system clipboard
- Brief confirmation shown in details pane
---
### 7.4 Nice-to-have (higher effort)
#### Task U — Dynamic column width
**Goal:** Allow widening Title column on demand for long issue titles.
**Agent:** 1
**Owned files:**
- `internal/ui/keys.go` (add toggle binding)
- `internal/ui/app.go` (add `wideTitle` state + update layout())
**Implementation:**
- Add binding in `keys.go`:
- `ToggleWideTitle: key.NewBinding(key.WithKeys("t"), key.WithHelp("t", "toggle wide title"))`
- Add `wideTitle bool` field to Model
- Update `layout()` to compute two widths:
- Normal mode: compact Title width (current behavior)
- Wide mode: allocate more space to Title column
- On `t` toggle, recompute layout and rebuild rows
**Acceptance:**
- Pressing `t` expands Title column
- Pressing `t` again returns to compact width
- Header shows current state (e.g., `wide=on` or icon)
---
#### Task V — Age format toggle (compact vs. “X ago”)
**Goal:** Switch between “2dm” (compact) and “2m ago” (relative) formats.
**Agent:** 1
**Owned files:**
- `internal/ui/keys.go` (add toggle binding)
- `internal/ui/app.go` (add `ageMode` field + update rendering)
**Implementation:**
- Add binding in `keys.go`:
- `ToggleAgeFormat: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "toggle age format"))`
- Add `ageMode AgeMode` field to Model with values: `AgeCompact` (default), `AgeRelative`
- Update `table.go` `formatAge()` or `app.go` wrapper to support both modes
- In `renderHeader()`, keep compact age (0s, Xds, Xdm, Xdh, Xdd)
- In details pane, show full time with relative suffix
**Acceptance:**
- Default: table shows `2dm`, details shows “2m ago”
- Press `d`: table shows `2m ago`, details show same
- Press `d` again: revert to compact
---
#### Task W — Rollup drill-down in details
**Goal:** When a rollup issue is selected, show the individual issues it aggregates.
**Agent:** 1
**Owned files:**
- `internal/ui/model` (add rollup-specific types) — or keep minimal
- `internal/ui/app.go` (update `renderIssueDetails()` / `setDetailsToSelected()`)
**Implementation:**
- Add helper `getRollupSamples(iss model.Issue) []string`:
- Parse `iss.Evidence["samples"]` if it exists
- Return list of affected IDs/pod names
- Update `renderIssueDetails()` in `details.go`:
- Detect rollup (e.g., ID starts with `k8s:rollup:` or category=Kubernetes + “rollup” in title)
- Append section: `Affected Issues` with list of sample IDs (up to 10)
- Keep full rollup summary (count, namespace, reason)
**Acceptance:**
- Selecting a rollup issue shows “Affected Issues: [k8s:pod:default/x, k8s:pod:default/y, …]” in details
- Non-rollup issues show normal details without this section
---
#### Task X — Light/dark theme toggle
**Goal:** Respect users theme preference or allow toggling.
**Agent:** 1
**Owned files:**
- `internal/ui/styles.go` (add theme variants)
- `internal/ui/keys.go` (add toggle binding)
- `internal/ui/app.go` (add theme state)
- `cmd/controltower/main.go` (optional env var support)
**Implementation:**
- Define two theme sets in `styles.go`:
- `LightTheme` (current hardcoded colors)
- `DarkTheme` (inverted/better contrast)
- Add `themeMode ThemeMode` field to Model (`ThemeAuto`, `ThemeLight`, `ThemeDark`)
- Add binding in `keys.go`:
- `ToggleTheme: key.NewBinding(key.WithKeys("T"), key.WithHelp("T", "toggle theme"))`
- Optional: respect `NO_COLOR=1` env var (disable all colors)
- On theme toggle, update `styles := defaultStyles()` to apply selected theme
**Acceptance:**
- Default theme is current (light-like)
- Pressing `T` toggles light/dark
- `NO_COLOR=1` disables colors entirely
---
#### Task Y — Sound/flash on new P0
**Goal:** Alert user when critical issues appear.
**Agent:** 1
**Owned files:**
- `internal/ui/app.go` (track P0 count changes + trigger alert)
**Implementation:**
- Add `lastP0Count int` field to Model
- In `applyViewFromSnapshot()`:
- Compare `p0` with `m.lastP0Count`
- If `p0 > m.lastP0Count`: new critical issues appeared
- Update `m.lastP0Count = p0`
- Add alert function:
- Use bubbleteas `bell` (terminal bell) or flash:
- Send `tea.BellMsg` on P0 increase
- Optional: check for `NO_BELL=1` env var to disable
**Acceptance:**
- When P0 count increases, terminal bell/flash occurs (if not disabled)
- P0 decrease or no change does not trigger alert
- `NO_BELL=1` env var suppresses all alerts
---
### 8-agent assignment for UX tasks
To keep parallelism and avoid conflicts, assign as follows (can be done in Phase 3):
| Agent | Tasks | Owned Files | Notes |
|-------|-------|--------------|-------|
| **Agent 9** | Task J (Help overlay) | `internal/ui/help.go`, `keys.go`, `app.go` | High discoverability impact |
| **Agent 10** | Tasks K, L, M (Priority jump, visual filters, empty state) | `keys.go`, `styles.go`, `app.go` | Quick navigation wins |
| **Agent 11** | Tasks N, O, P (Bulk ack, page nav, home/end) | `keys.go`, `app.go` | Nav efficiency |
| **Agent 12** | Tasks Q, R (Vi nav, collector health icons) | `keys.go`, `app.go`, `styles.go` | Nav + visual polish |
| **Agent 13** | Tasks S, T, V (Count warning, copy clipboard, dynamic width) | `keys.go`, `app.go`, `go.mod`, `table.go` | Polish + utility |
| **Agent 14** | Tasks W, X (Rollup drill-down, age toggle) | `app.go`, `details.go`, `keys.go` | Details enhancements |
| **Agent 15** | Tasks Y, Z (Theme, sound/flash) | `styles.go`, `app.go`, `keys.go` | Theme + alerts |
**Execution order (minimal dependencies):**
1. Agents 912 run in parallel (all are independent file changes)
2. Agents 1314 run after 912 (may reference new keys/styles)
3. Agent 15 runs after 1314 (may reference theme from styles)
---
### 8-agent assignment (UX improvements, parallel)
| Agent | Tasks | Owned Files | Notes |
|-------|-------|--------------|-------|
| **Agent 9** | Task J (Help overlay) | `internal/ui/help.go`, `keys.go`, `app.go` | High discoverability impact |
| **Agent 10** | Tasks K, L, M (Priority jump, visual filters, empty state) | `keys.go`, `styles.go`, `app.go` | Quick navigation wins |
| **Agent 11** | Tasks N, O, P (Bulk ack, page nav, home/end) | `keys.go`, `app.go` | Nav efficiency |
| **Agent 12** | Tasks Q, R (Vi nav, collector health icons) | `keys.go`, `app.go`, `styles.go` | Nav + visual polish |
| **Agent 13** | Tasks S, T, V (Count warning, copy clipboard, dynamic width) | `keys.go`, `app.go`, `go.mod`, `table.go` | Polish + utility |
| **Agent 14** | Tasks W, X (Rollup drill-down, age toggle) | `app.go`, `details.go`, `keys.go` | Details enhancements |
| **Agent 15** | Tasks Y, Z (Theme, sound/flash) | `styles.go`, `app.go`, `keys.go` | Theme + alerts |
**Execution order (minimal dependencies):**
1. Agents 912 run in parallel (all are independent file changes)
2. Agents 1314 run after 912 (may reference new keys/styles)
3. Agent 15 runs after 1314 (may reference theme from styles)
**Coordinated changes:**
- `keys.go` is shared across Agents 915; each agent appends new bindings without removing others.
- `app.go` is shared; each agent adds new handlers in disjoint methods to avoid merge conflicts.
- `styles.go` is shared; agents add new styles without modifying existing.
- `details.go` is shared; Agents 1314 add optional sections.
- `help.go` is new (Task J) and owned entirely by Agent 9.
#### Agent prompt: Task J (Help overlay)
"Implement in-app help overlay showing all keybindings. Add `?` key in keys.go, create help.go with Model + render() function. Toggle overlay with `?` key and dismiss with `?` or `esc`."
#### Agent prompt: Task K (Direct priority jump)
"Add numeric bindings `0`/`1`/`2`/`3` in keys.go to jump directly to P0/P1/P2/P3. Keep `p` cycle for `all` option. Update app.go Update handler."
#### Agent prompt: Task L (Visual filter indicators)
"Add FilterActive style in styles.go (highlight color). Update renderHeader() to apply when pri= or cat= filters are active."
#### Agent prompt: Task M (Better empty state)
"Show friendly message 'All systems healthy' in applyViewFromSnapshot() when len(filtered) == 0."
#### Agent prompt: Task N (Bulk ack all visible)
"Add AckAll binding (shift+a) in keys.go. Implement AckAllVisible() method in app.go that iterates rowsIDs and calls ack(). Update handler."
#### Agent prompt: Task O (Page navigation)
"Document that bubbles/table supports page keys (pgup/pgdn, ctrl+u/ctrl+d). Ensure they are not overridden in app.go Update handler. Add to help overlay."
#### Agent prompt: Task P (Home/End jump)
"Add `g`/`G` bindings in keys.go. Add handlers in app.go: `g` sets cursor to 0, `G` sets cursor to last row. Add to help overlay."
#### Agent prompt: Task Q (Vi-style nav)
"Add `j`/`k` bindings in keys.go. Update app.go to pass to table.Update(); bubbles/table handles both arrows and j/k. Update help overlay."
#### Agent prompt: Task R (Collector health icons)
"Update renderHeader() to use icons (✓ for OK, ⚠ for DEGRADED, ✗ for ERROR) instead of text. Format: 'collectors: ✓1 ⚠0 ✗0'."
#### Agent prompt: Task S (Issue count warning near cap)
"In renderHeader(), calculate total issues. If total >= 180, append warning '[~180/200]' with error color. Show only when near cap."
#### Agent prompt: Task T (Copy to clipboard)
"Add `y` binding in keys.go. Add atotto/clipboard to go.mod. On `y` key, copy selected issue's SuggestedFix to clipboard. Show brief confirmation in details."
#### Agent prompt: Task U (Dynamic column width)
"Add ToggleWideTitle binding (`t`) in keys.go. Add wideTitle bool to Model. Update layout() to compute wide vs compact widths. Toggle with `t` key. Update help overlay."
#### Agent prompt: Task V (Age format toggle)
"Add ToggleAgeFormat binding (`d`) in keys.go. Add AgeMode field (Compact/Relative) to Model. Update formatAge() or wrapper in app.go to support both modes. Toggle with `d` key."
#### Agent prompt: Task W (Rollup drill-down)
"Add getRollupSamples() helper to parse Evidence['samples']. Update renderIssueDetails() in details.go to detect rollup and append 'Affected Issues' section with sample IDs."
#### Agent prompt: Task X (Light/dark theme toggle)
"Define LightTheme and DarkTheme in styles.go. Add themeMode field to Model with Auto/Light/Dark. Add ToggleTheme binding (`T`) in keys.go. Respect NO_COLOR env var."
#### Agent prompt: Task Y (Sound/flash on new P0)
"Add lastP0Count to Model. In applyViewFromSnapshot(), compare p0 with lastP0Count. If p0 increases, send bubbletea.BellMsg. Check NO_BELL env var."
#### Agent prompt: Task Z (Theme, sound, flash)
"Combine Task X (theme toggle) and Task Y (P0 alert). Test theme toggle works and P0 bell/flash. Add env var checks for NO_BELL."