# Homelab Topology - Remaining Features Implementation ## TL;DR > **Quick Summary**: Implement 11 remaining features for the homelab topology visualizer, including backend API for SSH discovery, real data tabs (Config/Files/Usage), filters, persistence, and UX enhancements. > > **Deliverables**: > - Express backend API server with SSH discovery endpoints > - Config Tab showing real docker-compose.yml content > - Files Tab showing real mounted volumes > - Usage Tab showing live docker stats > - Type and Status filters in Header > - Configurable poll interval UI > - LocalStorage persistence for settings > - Command palette (Cmd+K) > - Stale data warning banner > - Path highlighting for selected nodes > > **Estimated Effort**: Large > **Parallel Execution**: YES - 3 waves > **Critical Path**: Backend API → SSH Discovery → Real Data Tabs → Filters → UX Enhancements --- ## Context ### Original Request Implement all remaining tasks for the homelab topology visualizer: - High Priority: SSH Discovery, Config Tab, Files Tab, Usage Tab - Medium Priority: Type Filter, Status Filter, Configurable Poll, LocalStorage - Lower Priority: Command Palette, Stale Warning, Path Highlighting ### Interview Summary **Key Discussions**: - SSH Discovery: Requires backend API (browser can't SSH directly) - Config/Files/Usage Tabs: Need real data via SSH + docker commands - Filters: Toggle buttons in Header for type/status filtering - Persistence: Zustand persist middleware for localStorage - UX: Command palette, stale warning, path highlighting **Research Findings**: - Current project uses React + Vite + Zustand + React Flow + dagre - `sshDiscovery.ts` exists but not connected to frontend - App.tsx has 30s polling with countdown timer - RightPanel tabs show placeholder data ### Metis Review **Identified Gaps** (addressed): - SSH Authentication: Using environment variables/config file, NOT localStorage - Error Handling: Graceful degradation for partial failures - Security: All SSH operations server-side only - Performance: Discovery timeout of 30s per host --- ## Work Objectives ### Core Objective Complete the homelab topology visualizer with real infrastructure data via SSH discovery and enhanced UX features. ### Concrete Deliverables 1. `server/` - Express backend with SSH discovery API 2. `server/routes/discover.ts` - SSH discovery endpoints 3. `server/routes/config.ts` - Config file retrieval 4. `server/routes/files.ts` - Volume discovery 5. `server/routes/stats.ts` - Docker stats endpoint 6. `src/components/Header.tsx` - Updated with filters and poll interval 7. `src/components/RightPanel.tsx` - Config/Files/Usage tabs with real data 8. `src/components/CommandPalette.tsx` - New Cmd+K modal 9. `src/components/StaleWarning.tsx` - New warning banner 10. `src/store/topologyStore.ts` - Updated with persist middleware ### Definition of Done - [ ] All 11 features implemented and functional - [ ] Backend API running on port 3001 - [ ] Frontend connects to backend for discovery - [ ] All filters work and persist across refresh - [ ] Command palette functional with Cmd+K - [ ] Stale warning appears after 3 failed discoveries - [ ] Path highlighting shows ancestor nodes ### Must Have - Backend API for SSH operations (security: server-side only) - Real data in Config/Files/Usage tabs - Type and Status filters functional - Settings persist in localStorage ### Must NOT Have (Guardrails) - **SECURITY**: NEVER store SSH credentials in localStorage or frontend - **SECURITY**: NEVER expose SSH credentials in API responses - **SCOPE**: No user authentication system (single-user local tool) - **SCOPE**: No WebSocket (polling is sufficient) - **SCOPE**: No historical metrics storage (live data only) - **SCOPE**: No alerting/notification system - **IMPLEMENTATION**: No database (file-based config is sufficient) - **AI-SLOP**: No over-engineering - simple Express server, minimal routes --- ## Verification Strategy ### Test Decision - **Infrastructure exists**: YES (frontend has test config) - **Automated tests**: NO (tests-after approach) - **Framework**: bun test - **Agent-Executed QA**: ALWAYS (mandatory for all tasks) ### QA Policy Every task includes agent-executed QA scenarios: - **Backend API**: Use Bash (curl) — Send requests, assert status + response fields - **Frontend UI**: Use Playwright — Navigate, interact, assert DOM, screenshot - **Integration**: Use Playwright + curl combined Evidence saved to `.sisyphus/evidence/task-{N}-{scenario-slug}.{ext}` --- ## Execution Strategy ### Parallel Execution Waves ``` Wave 1 (Start Immediately — 5 parallel tasks): ├── Task 1: Express Backend Foundation [quick] ├── Task 2: Backend Types & Config Schema [quick] ├── Task 3: Type Filter UI [quick] ├── Task 4: Status Filter UI [quick] └── Task 5: LocalStorage Persistence [quick] Wave 2 (After Wave 1 — SSH discovery, then parallel data tabs): ├── Task 6: SSH Discovery API Endpoint [deep] (blocks 7-10) ├── Task 7: Wire Frontend to Discovery API [quick] ├── Task 8: Config Tab with Real Data [quick] ├── Task 9: Files Tab with Real Data [quick] └── Task 10: Usage Tab with Real Data [quick] Wave 3 (After Wave 2 — 4 parallel UX enhancements): ├── Task 11: Configurable Poll Interval [quick] ├── Task 12: Command Palette [visual-engineering] ├── Task 13: Stale Data Warning [quick] └── Task 14: Path Highlighting [visual-engineering] Wave FINAL (After ALL tasks — 4 parallel reviews): ├── Task F1: Plan Compliance Audit (oracle) ├── Task F2: Code Quality Review (unspecified-high) ├── Task F3: Real Manual QA (unspecified-high) └── Task F4: Scope Fidelity Check (deep) Critical Path: Task 1 → Task 6 → Task 7 → Task 8/9/10 → Task 11/12/13/14 → F1-F4 Parallel Speedup: ~60% faster than sequential Max Concurrent: 5 (Wave 1) ``` ### Dependency Matrix | Task | Depends On | Blocks | |------|------------|--------| | 1 | — | 6, 8, 9, 10 | | 2 | — | 6, 8, 9, 10 | | 3 | — | 12 | | 4 | — | 12 | | 5 | — | — | | 6 | 1, 2 | 7, 8, 9, 10 | | 7 | 6 | 11, 13, 14 | | 8 | 6, 7 | — | | 9 | 6, 7 | — | | 10 | 6, 7 | — | | 11 | 7 | — | | 12 | 3, 4 | — | | 13 | 7 | — | | 14 | 7 | — | ### Agent Dispatch Summary - **Wave 1**: 5 tasks — T1 → `quick` + `system-admin`, T2 → `quick`, T3-T4 → `quick` + `frontend-ui-ux`, T5 → `quick` - **Wave 2**: 5 tasks — T6 → `deep` + `system-admin` + `ssh`, T7-T10 → `quick` - **Wave 3**: 4 tasks — T11/T13 → `quick` + `frontend-ui-ux`, T12/T14 → `visual-engineering` - **FINAL**: 4 tasks — F1 → `oracle`, F2-F3 → `unspecified-high`, F4 → `deep` --- ## TODOs - [ ] 1. **Create Express Backend Foundation** **What to do**: - Create `server/` directory at project root - Create `server/index.ts` with Express app listening on port 3001 - Add CORS middleware for frontend communication - Create `/api/health` endpoint returning `{"status":"ok"}` - Add npm script `"server": "tsx server/index.ts"` to package.json - Install dependencies: `express`, `cors`, `@types/express`, `@types/cors` **Must NOT do**: - Do NOT add authentication middleware (out of scope) - Do NOT add WebSocket support (polling is sufficient) - Do NOT over-engineer with middleware layers **Recommended Agent Profile**: - **Category**: `quick` - Reason: Standard Express server setup, well-documented pattern - **Skills**: [`system-admin`] - `system-admin`: Server setup and port configuration **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 2, 3, 4, 5) - **Blocks**: Tasks 6, 8, 9, 10 - **Blocked By**: None **References**: - `package.json:13-22` - Existing dependencies, add express/cors - `src/services/sshDiscovery.ts:1-44` - SSH client setup pattern to reference - Existing Vite config shows project structure **Acceptance Criteria**: - [ ] `server/index.ts` exists with Express app - [ ] `npm run server` starts server on port 3001 - [ ] `curl http://localhost:3001/api/health` returns `{"status":"ok"}` - [ ] CORS enabled for `http://localhost:3000` **QA Scenarios**: ``` Scenario: Backend health check works Tool: Bash (curl) Steps: 1. cd /home/christopher/homelab-topology 2. bun run server & 3. sleep 2 4. curl -s http://localhost:3001/api/health Expected Result: {"status":"ok"} Evidence: .sisyphus/evidence/task-1-health-check.json ``` **Commit**: YES - Message: `feat(backend): add Express server foundation` - Files: `server/index.ts`, `package.json` --- - [ ] 2. **Define Backend Types and Config Schema** **What to do**: - Create `server/types.ts` with TypeScript interfaces: - `HostConfig`: name, ip, sshUser, sshKeyPath (optional) - `DiscoveryResponse`: hosts, timestamp, errors - `ConfigResponse`: yaml, path, error - `FilesResponse`: volumes array (source, destination, mode) - `StatsResponse`: cpu, memory, network (rx/tx) - Create `server/config.ts` with host configuration loader: - Load from environment variables first (SSH_HOSTS, SSH_USER, SSH_KEY) - Fallback to `server/config.json` file - Export `getHostConfigs(): HostConfig[]` - Create sample `server/config.example.json` with structure **Must NOT do**: - Do NOT store credentials in code - Do NOT create encrypted vault (out of scope) - Do NOT add credential rotation logic **Recommended Agent Profile**: - **Category**: `quick` - Reason: Type definitions and simple config loading - **Skills**: [] **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 1, 3, 4, 5) - **Blocks**: Tasks 6, 8, 9, 10 - **Blocked By**: None **References**: - `src/types/index.ts` - Existing type patterns to match - `src/services/sshDiscovery.ts:3-28` - Existing SSH config interface - `src/data/staticConfig.ts:58-101` - Existing host definitions **Acceptance Criteria**: - [ ] `server/types.ts` exists with all interfaces - [ ] `server/config.ts` exports `getHostConfigs()` - [ ] `server/config.example.json` shows expected format - [ ] Types compile without errors: `tsc --noEmit` **QA Scenarios**: ``` Scenario: Config loader returns host configs Tool: Bash (node) Steps: 1. cd /home/christopher/homelab-topology 2. Create test config: echo '{"hosts":[{"name":"test","ip":"127.0.0.1"}]}' > server/config.json 3. node -e "import('./server/config.ts').then(m => console.log(m.getHostConfigs()))" Expected Result: Array with host configs Evidence: .sisyphus/evidence/task-2-config-loader.txt ``` **Commit**: YES - Message: `feat(backend): add types and config schema` - Files: `server/types.ts`, `server/config.ts`, `server/config.example.json` --- - [ ] 3. **Implement Type Filter UI** **What to do**: - Add `typeFilters: NodeType[]` to topologyStore (default: all types) - Add `toggleTypeFilter(type: NodeType)` action to store - Update `getFilteredNodes()` to filter by typeFilters - Add filter toggle buttons in Header.tsx after orientation dropdown - Each button shows node type icon and is toggleable (active/inactive state) - Use existing node type colors for button styling **Must NOT do**: - Do NOT add "select all/none" buttons (keep it simple) - Do NOT create a dropdown menu (use inline toggles) - Do NOT add filter descriptions/tooltips (icons sufficient) **Recommended Agent Profile**: - **Category**: `quick` - Reason: Simple toggle buttons with store integration - **Skills**: [`frontend-ui-ux`] - `frontend-ui-ux`: Filter button design and styling **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 1, 2, 4, 5) - **Blocks**: Task 12 - **Blocked By**: None **References**: - `src/store/topologyStore.ts:79-125` - Existing getFilteredNodes() to extend - `src/types/index.ts:1-11` - NodeType definitions - `src/utils/colors.ts` - Node type colors for styling - `src/components/Header.tsx:58-73` - Existing button styling pattern **Acceptance Criteria**: - [ ] `typeFilters` added to store with all types as default - [ ] Toggle buttons visible in Header - [ ] Clicking filter hides corresponding nodes - [ ] Multiple filters can be active simultaneously - [ ] Filter state persists (will be tested in Task 5) **QA Scenarios**: ``` Scenario: Type filter hides gateway nodes Tool: Playwright Steps: 1. Navigate to http://localhost:3000 2. Click type filter button for "gateway" 3. Wait for graph to update 4. Check if gateway node is visible Expected Result: Gateway node not in DOM Evidence: .sisyphus/evidence/task-3-type-filter.png Scenario: Multiple type filters work together Tool: Playwright Steps: 1. Navigate to http://localhost:3000 2. Click filter for "vlan" and "wifi" 3. Count visible nodes Expected Result: Only nodes NOT of type vlan/wifi visible Evidence: .sisyphus/evidence/task-3-multi-filter.png ``` **Commit**: YES - Message: `feat(ui): add type filter toggles` - Files: `src/store/topologyStore.ts`, `src/components/Header.tsx` --- - [ ] 4. **Implement Status Filter UI** **What to do**: - Add `statusFilter: 'all' | 'running' | 'stopped'` to topologyStore (default: 'all') - Add `setStatusFilter(status: string)` action to store - Update `getFilteredNodes()` to filter by status - Add dropdown in Header.tsx after type filters with options: All, Running, Stopped - Use existing status colors for visual indication **Must NOT do**: - Do NOT add "unknown" status filter (not useful) - Do NOT create multi-select (single status at a time) - Do NOT add status counts (complexity) **Recommended Agent Profile**: - **Category**: `quick` - Reason: Simple dropdown with store integration - **Skills**: [`frontend-ui-ux`] - `frontend-ui-ux`: Dropdown styling **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 1, 2, 3, 5) - **Blocks**: Task 12 - **Blocked By**: None **References**: - `src/store/topologyStore.ts:79-125` - Existing getFilteredNodes() to extend - `src/types/index.ts:15` - Status type definition - `src/utils/colors.ts` - getStatusColor function - `src/components/Header.tsx:78-89` - Existing dropdown pattern (orientation) **Acceptance Criteria**: - [ ] `statusFilter` added to store with 'all' as default - [ ] Dropdown visible in Header with 3 options - [ ] Selecting "Running" shows only running nodes - [ ] Selecting "Stopped" shows only stopped nodes - [ ] Filter state persists (will be tested in Task 5) **QA Scenarios**: ``` Scenario: Status filter shows only running nodes Tool: Playwright Steps: 1. Navigate to http://localhost:3000 2. Click status dropdown, select "Running" 3. Check all visible nodes have status "running" Expected Result: All visible nodes show running status Evidence: .sisyphus/evidence/task-4-running-filter.png Scenario: Status filter shows only stopped nodes Tool: Playwright Steps: 1. Navigate to http://localhost:3000 2. Click status dropdown, select "Stopped" 3. Verify no running nodes visible Expected Result: Only stopped/unknown nodes visible Evidence: .sisyphus/evidence/task-4-stopped-filter.png ``` **Commit**: YES - Message: `feat(ui): add status filter dropdown` - Files: `src/store/topologyStore.ts`, `src/components/Header.tsx` --- - [ ] 5. **Implement LocalStorage Persistence** **What to do**: - Add `zustand/middleware` persist to topologyStore - Persist these keys: viewMode, orientation, typeFilters, statusFilter, leftPanelOpen, rightPanelOpen, pollInterval - Set storage key as `homelab-topology-settings` - Exclude from persistence: nodes, edges, selectedNodeId, isLoading, networkInfo, hosts - Add version number for future migrations **Must NOT do**: - Do NOT persist SSH credentials (SECURITY) - Do NOT persist topology data (nodes/edges) - Do NOT add export/import functionality (out of scope) - Do NOT add profile switching (out of scope) **Recommended Agent Profile**: - **Category**: `quick` - Reason: Zustand persist middleware is straightforward - **Skills**: [] **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Tasks 1, 2, 3, 4) - **Blocks**: None - **Blocked By**: None **References**: - `src/store/topologyStore.ts:1-127` - Existing store structure - Zustand persist docs: https://zustand.docs.pmnd.rs/middleware/persist - `package.json:21` - Zustand already installed **Acceptance Criteria**: - [ ] Persist middleware added to store - [ ] Settings saved to localStorage on change - [ ] Settings restored on page load - [ ] Sensitive data NOT persisted - [ ] Page refresh preserves filters and view mode **QA Scenarios**: ``` Scenario: Settings persist across page refresh Tool: Playwright Steps: 1. Navigate to http://localhost:3000 2. Set view mode to "Network" 3. Set orientation to "Top to Bottom" 4. Toggle left panel closed 5. Refresh page (F5) 6. Check view mode, orientation, panel state Expected Result: All settings match pre-refresh state Evidence: .sisyphus/evidence/task-5-persistence.png Scenario: Nodes NOT persisted in localStorage Tool: Playwright Steps: 1. Navigate to http://localhost:3000 2. Open browser DevTools > Application > Local Storage 3. Check homelab-topology-settings value Expected Result: No 'nodes' or 'edges' keys in storage Evidence: .sisyphus/evidence/task-5-no-sensitive-data.txt ``` **Commit**: YES - Message: `feat(ui): add localStorage persistence` - Files: `src/store/topologyStore.ts` --- - [ ] 6. **Implement SSH Discovery API Endpoint** **What to do**: - Create `server/routes/discover.ts` with Express router - Implement `POST /api/discover` endpoint: - Load host configs from `getHostConfigs()` - Call `discoverHostViaSSH()` from existing sshDiscovery.ts for each host - Run discoveries in parallel with Promise.all - Handle errors gracefully (partial success) - Return DiscoveryResponse with hosts, timestamp, errors array - Add 30-second timeout per host - Import and use existing sshDiscovery functions **Must NOT do**: - Do NOT expose SSH credentials in response - Do NOT add retry logic (keep simple) - Do NOT cache results (always fresh discovery) - Do NOT add WebSocket push (polling is sufficient) **Recommended Agent Profile**: - **Category**: `deep` - Reason: Complex async operations, error handling, SSH integration - **Skills**: [`system-admin`, `ssh`] - `system-admin`: Understanding SSH operations - `ssh`: SSH client configuration and error handling **Parallelization**: - **Can Run In Parallel**: NO (depends on Tasks 1, 2) - **Parallel Group**: Wave 2 (must complete before 7-10) - **Blocks**: Tasks 7, 8, 9, 10 - **Blocked By**: Tasks 1, 2 **References**: - `src/services/sshDiscovery.ts:61-121` - Existing discoverHostViaSSH function - `src/services/sshDiscovery.ts:114-132` - discoverAllHostsSSH pattern - `server/types.ts` - HostConfig, DiscoveryResponse interfaces - `server/config.ts` - getHostConfigs() function **Acceptance Criteria**: - [ ] `server/routes/discover.ts` exists - [ ] `POST /api/discover` endpoint functional - [ ] Returns JSON with hosts array, timestamp, errors - [ ] Handles SSH failures gracefully (partial success) - [ ] No SSH credentials in response **QA Scenarios**: ``` Scenario: Discovery API returns host data Tool: Bash (curl) Steps: 1. Ensure server running: bun run server 2. curl -X POST http://localhost:3001/api/discover 3. Parse JSON response Expected Result: Response has hosts array with host objects Evidence: .sisyphus/evidence/task-6-discover-success.json Scenario: Discovery handles SSH failure gracefully Tool: Bash (curl) Steps: 1. Add unreachable host to config 2. curl -X POST http://localhost:3001/api/discover 3. Check response has errors array Expected Result: Response includes error for unreachable host, other hosts succeed Evidence: .sisyphus/evidence/task-6-discover-partial.json ``` **Commit**: YES - Message: `feat(api): implement SSH discovery endpoint` - Files: `server/routes/discover.ts`, `server/index.ts` (register router) --- - [ ] 7. **Wire Frontend to Discovery API** **What to do**: - Update `App.tsx` `loadData()` to call backend API instead of simulated discovery - Add API_BASE_URL constant (default: `http://localhost:3001`) - Handle API errors with fallback to simulated data - Add loading states per host (show which hosts are still loading) - Update Footer to show "Live data" or "Simulated data" indicator **Must NOT do**: - Do NOT hardcode API URL (use environment variable) - Do NOT add complex error UI (console.error + fallback is enough) - Do NOT add retry logic in frontend (keep simple) **Recommended Agent Profile**: - **Category**: `quick` - Reason: Simple fetch call with error handling - **Skills**: [] **Parallelization**: - **Can Run In Parallel**: NO (depends on Task 6) - **Parallel Group**: Wave 2 (runs after Task 6) - **Blocks**: Tasks 8, 9, 10, 11, 13, 14 - **Blocked By**: Task 6 **References**: - `src/App.tsx:33-73` - Existing loadData() function to modify - `src/services/discovery.ts:264-340` - Existing discoverHosts (will be fallback) - `src/services/sshDiscovery.ts` - Will be called via API instead **Acceptance Criteria**: - [ ] Frontend calls backend API for discovery - [ ] Falls back to simulated data on API error - [ ] Loading state shows during API call - [ ] Footer shows data source indicator **QA Scenarios**: ``` Scenario: Frontend shows real data from API Tool: Playwright Steps: 1. Start backend: bun run server 2. Start frontend: bun run dev 3. Navigate to http://localhost:3000 4. Check footer for "Live data" indicator Expected Result: Topology shows real SSH-discovered data Evidence: .sisyphus/evidence/task-7-live-data.png Scenario: Frontend falls back on API error Tool: Playwright Steps: 1. Stop backend server 2. Navigate to http://localhost:3000 3. Check footer for "Simulated data" indicator Expected Result: Topology shows simulated data, no crash Evidence: .sisyphus/evidence/task-7-fallback.png ``` **Commit**: YES - Message: `feat(ui): wire frontend to discovery API` - Files: `src/App.tsx` --- - [ ] 8. **Implement Config Tab with Real Data** **What to do**: - Create `server/routes/config.ts` with Express router - Implement `GET /api/config/:host/:container` endpoint: - SSH to host - Find docker-compose.yml location (check common paths) - Read file content - Parse YAML to find specific service - Return ConfigResponse with yaml string, path, error if any - Update `RightPanel.tsx` ConfigTab: - Fetch config from API when service node selected - Display YAML with syntax highlighting (use `
` with monospace)
    - Show error message if config not found
    - Show "Loading..." state during fetch

  **Must NOT do**:
  - Do NOT add YAML editing (read-only)
  - Do NOT show full docker-compose (only selected service section)
  - Do NOT add config validation (just display)

  **Recommended Agent Profile**:
  - **Category**: `quick`
    - Reason: Simple file read and display
  - **Skills**: []

  **Parallelization**:
  - **Can Run In Parallel**: YES
  - **Parallel Group**: Wave 2 (with Tasks 9, 10, after Task 7)
  - **Blocks**: None
  - **Blocked By**: Tasks 6, 7

  **References**:
  - `src/components/RightPanel.tsx:122-141` - Existing ConfigTab to update
  - `src/services/sshDiscovery.ts:46-59` - SSH exec pattern
  - Common docker-compose paths: `./docker-compose.yml`, `/opt/docker-compose.yml`

  **Acceptance Criteria**:
  - [ ] `GET /api/config/:host/:container` endpoint exists
  - [ ] ConfigTab fetches and displays YAML
  - [ ] Shows error message when config not found
  - [ ] Shows "Loading..." during fetch

  **QA Scenarios**:
  ```
  Scenario: Config tab shows docker-compose content
    Tool: Playwright
    Steps:
      1. Navigate to http://localhost:3000
      2. Click on a service node (e.g., traefik)
      3. Click "Config" tab in right panel
    Expected Result: YAML content displayed in monospace font
    Evidence: .sisyphus/evidence/task-8-config-display.png

  Scenario: Config tab handles missing file
    Tool: Bash (curl)
    Steps:
      1. curl http://localhost:3001/api/config/nonexistent/container
    Expected Result: Response with error field, not 500
    Evidence: .sisyphus/evidence/task-8-config-error.json
  ```

  **Commit**: YES
  - Message: `feat(ui): add real config data to Config tab`
  - Files: `server/routes/config.ts`, `src/components/RightPanel.tsx`, `server/index.ts`

---

- [ ] 9. **Implement Files Tab with Real Data**

  **What to do**:
  - Create `server/routes/files.ts` with Express router
  - Implement `GET /api/files/:host/:container` endpoint:
    - SSH to host
    - Run `docker inspect  --format '{{json .Mounts}}'`
    - Parse JSON array of mounts
    - Return FilesResponse with volumes array (source, destination, mode)
  - Update `RightPanel.tsx` FilesTab:
    - Fetch files from API when service node selected
    - Display list of volume mounts with Source → Destination
    - Show "No volumes" message if empty
    - Show "Loading..." state during fetch

  **Must NOT do**:
  - Do NOT browse files (just show mount points)
  - Do NOT add file content preview
  - Do NOT follow symlinks (show as-is)

  **Recommended Agent Profile**:
  - **Category**: `quick`
    - Reason: Simple docker inspect parsing
  - **Skills**: []

  **Parallelization**:
  - **Can Run In Parallel**: YES
  - **Parallel Group**: Wave 2 (with Tasks 8, 10, after Task 7)
  - **Blocks**: None
  - **Blocked By**: Tasks 6, 7

  **References**:
  - `src/components/RightPanel.tsx:143-163` - Existing FilesTab to update
  - `src/services/sshDiscovery.ts:46-59` - SSH exec pattern
  - Docker inspect format: `{{json .Mounts}}`

  **Acceptance Criteria**:
  - [ ] `GET /api/files/:host/:container` endpoint exists
  - [ ] FilesTab fetches and displays volume mounts
  - [ ] Shows "No volumes" when container has none
  - [ ] Shows "Loading..." during fetch

  **QA Scenarios**:
  ```
  Scenario: Files tab shows volume mounts
    Tool: Playwright
    Steps:
      1. Navigate to http://localhost:3000
      2. Click on a service node with volumes (e.g., jellyfin)
      3. Click "Files" tab in right panel
    Expected Result: List of volume mounts with source and destination
    Evidence: .sisyphus/evidence/task-9-files-display.png

  Scenario: Files API returns mount data
    Tool: Bash (curl)
    Steps:
      1. curl http://localhost:3001/api/files/ubuntu/jellyfin
    Expected Result: JSON array with volume objects
    Evidence: .sisyphus/evidence/task-9-files-api.json
  ```

  **Commit**: YES
  - Message: `feat(ui): add real volume data to Files tab`
  - Files: `server/routes/files.ts`, `src/components/RightPanel.tsx`, `server/index.ts`

---

- [ ] 10. **Implement Usage Tab with Real Data**

  **What to do**:
  - Create `server/routes/stats.ts` with Express router
  - Implement `GET /api/stats/:host/:container` endpoint:
    - SSH to host
    - Run `docker stats  --no-stream --format '{"cpu":"{{.CPUPerc}}","mem":"{{.MemPerc}}","net":"{{.NetIO}}"}'`
    - Parse output (remove % signs, convert to numbers)
    - Return StatsResponse with cpu, memory, network values
  - Update `RightPanel.tsx` UsageTab:
    - Fetch stats from API when service node selected
    - Display progress bars for CPU and Memory
    - Show network I/O as text
    - Auto-refresh stats every 5 seconds while tab is visible
    - Show "N/A" for stopped containers

  **Must NOT do**:
  - Do NOT add historical charts (live only)
  - Do NOT add alerting thresholds
  - Do NOT store stats data

  **Recommended Agent Profile**:
  - **Category**: `quick`
    - Reason: Simple docker stats parsing and display
  - **Skills**: [`system-admin`]
    - `system-admin`: Understanding docker stats format

  **Parallelization**:
  - **Can Run In Parallel**: YES
  - **Parallel Group**: Wave 2 (with Tasks 8, 9, after Task 7)
  - **Blocks**: None
  - **Blocked By**: Tasks 6, 7

  **References**:
  - `src/components/RightPanel.tsx:165-209` - Existing UsageTab to update
  - `src/services/sshDiscovery.ts:46-59` - SSH exec pattern
  - Docker stats format: `--format` flag with Go templates

  **Acceptance Criteria**:
  - [ ] `GET /api/stats/:host/:container` endpoint exists
  - [ ] UsageTab fetches and displays live stats
  - [ ] Stats auto-refresh every 5 seconds
  - [ ] Shows "N/A" for stopped containers

  **QA Scenarios**:
  ```
  Scenario: Usage tab shows live docker stats
    Tool: Playwright
    Steps:
      1. Navigate to http://localhost:3000
      2. Click on a running service node
      3. Click "Usage" tab in right panel
      4. Wait 5 seconds for refresh
    Expected Result: CPU/Memory bars update with real values
    Evidence: .sisyphus/evidence/task-10-usage-live.png

  Scenario: Stats API returns numeric data
    Tool: Bash (curl)
    Steps:
      1. curl http://localhost:3001/api/stats/ubuntu/traefik
    Expected Result: JSON with cpu, memory as numbers (0-100)
    Evidence: .sisyphus/evidence/task-10-stats-api.json
  ```

  **Commit**: YES
  - Message: `feat(ui): add live docker stats to Usage tab`
  - Files: `server/routes/stats.ts`, `src/components/RightPanel.tsx`, `server/index.ts`

---

- [ ] 11. **Implement Configurable Poll Interval**

  **What to do**:
  - Add `pollInterval: number` to topologyStore (default: 30000ms)
  - Add `setPollInterval(ms: number)` action
  - Update `App.tsx` to use store's pollInterval instead of hardcoded constant
  - Add settings dropdown in Header (gear icon) with poll interval options:
    - 10 seconds, 30 seconds, 1 minute, 5 minutes
  - Update Footer countdown to reflect new interval
  - Include pollInterval in localStorage persistence

  **Must NOT do**:
  - Do NOT add freeform input (use preset options only)
  - Do NOT add "pause polling" button (keep simple)
  - Do NOT show interval in seconds (human-friendly format)

  **Recommended Agent Profile**:
  - **Category**: `quick`
    - Reason: Simple dropdown and store integration
  - **Skills**: [`frontend-ui-ux`]
    - `frontend-ui-ux`: Settings UI design

  **Parallelization**:
  - **Can Run In Parallel**: YES
  - **Parallel Group**: Wave 3 (with Tasks 12, 13, 14)
  - **Blocks**: None
  - **Blocked By**: Task 7

  **References**:
  - `src/App.tsx:15` - POLLING_INTERVAL_MS constant to replace
  - `src/App.tsx:78-81` - setInterval to make dynamic
  - `src/components/Header.tsx:78-89` - Dropdown pattern to reference
  - `src/store/topologyStore.ts` - Add pollInterval state

  **Acceptance Criteria**:
  - [ ] pollInterval in store with 30s default
  - [ ] Settings dropdown in Header with 4 options
  - [ ] Polling interval updates when changed
  - [ ] Footer countdown reflects new interval
  - [ ] Setting persists across refresh

  **QA Scenarios**:
  ```
  Scenario: Poll interval can be changed
    Tool: Playwright
    Steps:
      1. Navigate to http://localhost:3000
      2. Click settings icon in Header
      3. Select "5 minutes" option
      4. Check footer countdown shows "299s" → "298s"
    Expected Result: Countdown reflects 5-minute interval
    Evidence: .sisyphus/evidence/task-11-poll-5min.png

  Scenario: Poll interval persists
    Tool: Playwright
    Steps:
      1. Set poll interval to "1 minute"
      2. Refresh page
      3. Check footer countdown
    Expected Result: Countdown starts at 59s (1 minute)
    Evidence: .sisyphus/evidence/task-11-poll-persist.png
  ```

  **Commit**: YES
  - Message: `feat(ui): add configurable poll interval`
  - Files: `src/store/topologyStore.ts`, `src/App.tsx`, `src/components/Header.tsx`

---

- [ ] 12. **Implement Command Palette**

  **What to do**:
  - Create `src/components/CommandPalette.tsx` modal component
  - Add `commandPaletteOpen: boolean` to topologyStore
  - Implement Cmd+K keyboard shortcut to toggle palette
  - Palette shows searchable list of commands:
    - "Refresh discovery" → triggers refresh
    - "Toggle [type] filter" → toggles each type filter
    - "Set view: Full/Network/Hosts/Services/Files" → changes view
    - "Set orientation: LR/TB" → changes orientation
  - Fuzzy search filters commands as user types
  - Arrow keys navigate, Enter executes, Escape closes
  - Click outside or Escape to close

  **Must NOT do**:
  - Do NOT add command history
  - Do NOT add custom commands
  - Do NOT add macro recording

  **Recommended Agent Profile**:
  - **Category**: `visual-engineering`
    - Reason: Modal UI with keyboard navigation, fuzzy search
  - **Skills**: [`frontend-ui-ux`]
    - `frontend-ui-ux`: Modal design, keyboard interactions

  **Parallelization**:
  - **Can Run In Parallel**: YES
  - **Parallel Group**: Wave 3 (with Tasks 11, 13, 14)
  - **Blocks**: None
  - **Blocked By**: Tasks 3, 4 (needs filter state)

  **References**:
  - `src/components/Header.tsx` - View mode and filter actions to wire
  - `src/store/topologyStore.ts` - Actions to call from commands
  - Cmd+K pattern: Standard in VS Code, Linear, etc.

  **Acceptance Criteria**:
  - [ ] Cmd+K opens command palette modal
  - [ ] Fuzzy search filters commands
  - [ ] Arrow keys navigate, Enter executes
  - [ ] Escape or click-outside closes
  - [ ] Commands execute correct actions

  **QA Scenarios**:
  ```
  Scenario: Command palette opens with Cmd+K
    Tool: Playwright
    Steps:
      1. Navigate to http://localhost:3000
      2. Press Meta+K (or Ctrl+K on Windows)
    Expected Result: Modal appears with search input focused
    Evidence: .sisyphus/evidence/task-12-palette-open.png

  Scenario: Search and execute command
    Tool: Playwright
    Steps:
      1. Open command palette
      2. Type "network"
      3. Press Enter
    Expected Result: View mode changes to Network
    Evidence: .sisyphus/evidence/task-12-palette-execute.png

  Scenario: Escape closes palette
    Tool: Playwright
    Steps:
      1. Open command palette
      2. Press Escape
    Expected Result: Modal closes, focus returns to graph
    Evidence: .sisyphus/evidence/task-12-palette-close.png
  ```

  **Commit**: YES
  - Message: `feat(ux): add command palette with Cmd+K`
  - Files: `src/components/CommandPalette.tsx`, `src/App.tsx`, `src/store/topologyStore.ts`

---

- [ ] 13. **Implement Stale Data Warning**

  **What to do**:
  - Add `consecutiveFailures: number` and `lastSuccessfulDiscovery: Date | null` to topologyStore
  - Increment consecutiveFailures on API error, reset to 0 on success
  - Create `src/components/StaleWarning.tsx` banner component
  - Show banner when consecutiveFailures >= 3
  - Banner shows: "Data may be stale - Last successful: [timestamp]"
  - Banner has dismiss button (X) that temporarily hides it
  - Banner auto-dismisses on successful discovery
  - Style: Yellow/warning background, at top of main content area

  **Must NOT do**:
  - Do NOT add retry button (use main Refresh button)
  - Do NOT show detailed error messages
  - Do NOT add notification sound

  **Recommended Agent Profile**:
  - **Category**: `quick`
    - Reason: Simple conditional banner with dismiss
  - **Skills**: [`frontend-ui-ux`]
    - `frontend-ui-ux`: Warning banner styling

  **Parallelization**:
  - **Can Run In Parallel**: YES
  - **Parallel Group**: Wave 3 (with Tasks 11, 12, 14)
  - **Blocks**: None
  - **Blocked By**: Task 7

  **References**:
  - `src/App.tsx:63-69` - Error handling in loadData to update
  - `src/store/topologyStore.ts` - Add failure tracking state
  - Warning banner pattern: Common UI pattern

  **Acceptance Criteria**:
  - [ ] consecutiveFailures tracked in store
  - [ ] Banner shows after 3 consecutive failures
  - [ ] Banner shows last successful timestamp
  - [ ] Dismiss button hides banner temporarily
  - [ ] Banner auto-dismisses on success

  **QA Scenarios**:
  ```
  Scenario: Warning appears after failures
    Tool: Playwright
    Steps:
      1. Stop backend server
      2. Navigate to http://localhost:3000
      3. Wait for 3 discovery attempts (90 seconds or speed up polling)
    Expected Result: Yellow warning banner visible at top
    Evidence: .sisyphus/evidence/task-13-stale-warning.png

  Scenario: Warning dismisses on success
    Tool: Playwright
    Steps:
      1. Show warning banner (3+ failures)
      2. Start backend server
      3. Wait for next discovery
    Expected Result: Banner disappears automatically
    Evidence: .sisyphus/evidence/task-13-warning-dismiss.png
  ```

  **Commit**: YES
  - Message: `feat(ux): add stale data warning banner`
  - Files: `src/components/StaleWarning.tsx`, `src/App.tsx`, `src/store/topologyStore.ts`

---

- [ ] 14. **Implement Path Highlighting**

  **What to do**:
  - Add `highlightPath: string[]` to topologyStore (array of node IDs from root to selected)
  - Add `setHighlightPath(ids: string[])` action
  - When node selected, calculate path from gateway to that node:
    - Walk up parentId chain from selected node to root
    - Store all ancestor IDs in highlightPath
  - Update `TopologyGraph.tsx` to style highlighted nodes:
    - Highlighted nodes get brighter border (2px solid, accent color)
    - Non-highlighted nodes get dimmed opacity (0.5)
    - Highlighted edges get accent color
  - Clear highlightPath when clicking background (deselect)

  **Must NOT do**:
  - Do NOT animate the highlighting
  - Do NOT add "highlight path" button (automatic on select)
  - Do NOT highlight children (only ancestors)

  **Recommended Agent Profile**:
  - **Category**: `visual-engineering`
    - Reason: Graph styling with conditional classes
  - **Skills**: [`frontend-ui-ux`]
    - `frontend-ui-ux`: Visual highlighting design

  **Parallelization**:
  - **Can Run In Parallel**: YES
  - **Parallel Group**: Wave 3 (with Tasks 11, 12, 13)
  - **Blocks**: None
  - **Blocked By**: Task 7

  **References**:
  - `src/components/Graph/TopologyGraph.tsx` - Node styling to update
  - `src/store/topologyStore.ts:71-77` - getChildNodes pattern to reference
  - Node data has `parentId` field for walking ancestors

  **Acceptance Criteria**:
  - [ ] highlightPath calculated on node selection
  - [ ] Ancestor nodes have highlighted style
  - [ ] Non-ancestor nodes dimmed
  - [ ] Edges to ancestors highlighted
  - [ ] Clicking background clears highlight

  **QA Scenarios**:
  ```
  Scenario: Path highlighting shows ancestors
    Tool: Playwright
    Steps:
      1. Navigate to http://localhost:3000
      2. Click on a container node (e.g., ubuntu-traefik)
      3. Check visual state of ancestor nodes (ubuntu, vlan-50, gateway)
    Expected Result: Ancestors have bright border, others dimmed
    Evidence: .sisyphus/evidence/task-14-path-highlight.png

  Scenario: Clicking background clears highlight
    Tool: Playwright
    Steps:
      1. Select a node (path highlighted)
      2. Click on empty graph background
    Expected Result: All nodes return to normal opacity
    Evidence: .sisyphus/evidence/task-14-highlight-clear.png
  ```

  **Commit**: YES
  - Message: `feat(ux): add path highlighting for selected nodes`
  - Files: `src/components/Graph/TopologyGraph.tsx`, `src/store/topologyStore.ts`

---

## Final Verification Wave

- [ ] F1. **Plan Compliance Audit** — `oracle`
  Read the plan end-to-end. For each "Must Have": verify implementation exists. For each "Must NOT Have": search codebase for forbidden patterns. Check evidence files exist in .sisyphus/evidence/. Compare deliverables against plan.
  Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT`

- [ ] F2. **Code Quality Review** — `unspecified-high`
  Run `tsc --noEmit` + `bun run lint` + `bun test`. Review all changed files for: `as any`/`@ts-ignore`, empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction.
  Output: `Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | VERDICT`

- [ ] F3. **Real Manual QA** — `unspecified-high` (+ `playwright` skill)
  Start from clean state. Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration. Test edge cases: empty state, invalid input. Save to `.sisyphus/evidence/final-qa/`.
  Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT`

- [ ] F4. **Scope Fidelity Check** — `deep`
  For each task: read "What to do", read actual diff. Verify 1:1 — everything in spec was built, nothing beyond spec. Check "Must NOT do" compliance. Detect cross-task contamination.
  Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT`

---

## Commit Strategy

- **Wave 1**: `feat(backend): add Express server foundation with types`
- **Wave 1**: `feat(ui): add type and status filters, localStorage persistence`
- **Wave 2**: `feat(api): implement SSH discovery and data endpoints`
- **Wave 2**: `feat(ui): wire real data to Config/Files/Usage tabs`
- **Wave 3**: `feat(ux): add command palette, stale warning, path highlighting`
- **Final**: `feat: complete remaining features implementation`

---

## Success Criteria

### Verification Commands
```bash
# Backend health check
curl http://localhost:3001/api/health
# Expected: {"status":"ok"}

# SSH discovery
curl http://localhost:3001/api/discover | jq '.hosts | length'
# Expected: 6 (number of configured hosts)

# Config retrieval
curl http://localhost:3001/api/config/ubuntu/traefik
# Expected: YAML content of docker-compose.yml

# Frontend build
cd /home/christopher/homelab-topology && bun run build
# Expected: Build succeeds with no errors

# Type check
bun run tsc --noEmit
# Expected: No type errors
```

### Final Checklist
- [ ] All "Must Have" features present
- [ ] All "Must NOT Have" constraints respected
- [ ] No SSH credentials in localStorage
- [ ] Backend API functional on port 3001
- [ ] Frontend connects to backend
- [ ] All filters functional and persistent
- [ ] Command palette works with Cmd+K
- [ ] Stale warning appears on failures
- [ ] Path highlighting shows ancestors