42 KiB
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.tsexists 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
server/- Express backend with SSH discovery APIserver/routes/discover.ts- SSH discovery endpointsserver/routes/config.ts- Config file retrievalserver/routes/files.ts- Volume discoveryserver/routes/stats.ts- Docker stats endpointsrc/components/Header.tsx- Updated with filters and poll intervalsrc/components/RightPanel.tsx- Config/Files/Usage tabs with real datasrc/components/CommandPalette.tsx- New Cmd+K modalsrc/components/StaleWarning.tsx- New warning bannersrc/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.tswith Express app listening on port 3001 - Add CORS middleware for frontend communication
- Create
/api/healthendpoint 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/corssrc/services/sshDiscovery.ts:1-44- SSH client setup pattern to reference- Existing Vite config shows project structure
Acceptance Criteria:
server/index.tsexists with Express appnpm run serverstarts server on port 3001curl http://localhost:3001/api/healthreturns{"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.jsonCommit: YES
- Message:
feat(backend): add Express server foundation - Files:
server/index.ts,package.json
- Create
-
2. Define Backend Types and Config Schema
What to do:
- Create
server/types.tswith TypeScript interfaces:HostConfig: name, ip, sshUser, sshKeyPath (optional)DiscoveryResponse: hosts, timestamp, errorsConfigResponse: yaml, path, errorFilesResponse: volumes array (source, destination, mode)StatsResponse: cpu, memory, network (rx/tx)
- Create
server/config.tswith host configuration loader:- Load from environment variables first (SSH_HOSTS, SSH_USER, SSH_KEY)
- Fallback to
server/config.jsonfile - Export
getHostConfigs(): HostConfig[]
- Create sample
server/config.example.jsonwith 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 matchsrc/services/sshDiscovery.ts:3-28- Existing SSH config interfacesrc/data/staticConfig.ts:58-101- Existing host definitions
Acceptance Criteria:
server/types.tsexists with all interfacesserver/config.tsexportsgetHostConfigs()server/config.example.jsonshows 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.txtCommit: YES
- Message:
feat(backend): add types and config schema - Files:
server/types.ts,server/config.ts,server/config.example.json
- Create
-
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 extendsrc/types/index.ts:1-11- NodeType definitionssrc/utils/colors.ts- Node type colors for stylingsrc/components/Header.tsx:58-73- Existing button styling pattern
Acceptance Criteria:
typeFiltersadded 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.pngCommit: YES
- Message:
feat(ui): add type filter toggles - Files:
src/store/topologyStore.ts,src/components/Header.tsx
- Add
-
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 extendsrc/types/index.ts:15- Status type definitionsrc/utils/colors.ts- getStatusColor functionsrc/components/Header.tsx:78-89- Existing dropdown pattern (orientation)
Acceptance Criteria:
statusFilteradded 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.pngCommit: YES
- Message:
feat(ui): add status filter dropdown - Files:
src/store/topologyStore.ts,src/components/Header.tsx
- Add
-
5. Implement LocalStorage Persistence
What to do:
- Add
zustand/middlewarepersist 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.txtCommit: YES
- Message:
feat(ui): add localStorage persistence - Files:
src/store/topologyStore.ts
- Add
-
6. Implement SSH Discovery API Endpoint
What to do:
- Create
server/routes/discover.tswith Express router - Implement
POST /api/discoverendpoint:- 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
- Load host configs from
- 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 operationsssh: 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 functionsrc/services/sshDiscovery.ts:114-132- discoverAllHostsSSH patternserver/types.ts- HostConfig, DiscoveryResponse interfacesserver/config.ts- getHostConfigs() function
Acceptance Criteria:
server/routes/discover.tsexistsPOST /api/discoverendpoint 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.jsonCommit: YES
- Message:
feat(api): implement SSH discovery endpoint - Files:
server/routes/discover.ts,server/index.ts(register router)
- Create
-
7. Wire Frontend to Discovery API
What to do:
- Update
App.tsxloadData()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 modifysrc/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.pngCommit: YES
- Message:
feat(ui): wire frontend to discovery API - Files:
src/App.tsx
- Update
-
8. Implement Config Tab with Real Data
What to do:
- Create
server/routes/config.tswith Express router - Implement
GET /api/config/:host/:containerendpoint:- 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.tsxConfigTab:- Fetch config from API when service node selected
- Display YAML with syntax highlighting (use
<pre>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 updatesrc/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/:containerendpoint 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.jsonCommit: YES
- Message:
feat(ui): add real config data to Config tab - Files:
server/routes/config.ts,src/components/RightPanel.tsx,server/index.ts
- Create
-
9. Implement Files Tab with Real Data
What to do:
- Create
server/routes/files.tswith Express router - Implement
GET /api/files/:host/:containerendpoint:- SSH to host
- Run
docker inspect <container> --format '{{json .Mounts}}' - Parse JSON array of mounts
- Return FilesResponse with volumes array (source, destination, mode)
- Update
RightPanel.tsxFilesTab:- 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 updatesrc/services/sshDiscovery.ts:46-59- SSH exec pattern- Docker inspect format:
{{json .Mounts}}
Acceptance Criteria:
GET /api/files/:host/:containerendpoint 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.jsonCommit: YES
- Message:
feat(ui): add real volume data to Files tab - Files:
server/routes/files.ts,src/components/RightPanel.tsx,server/index.ts
- Create
-
10. Implement Usage Tab with Real Data
What to do:
- Create
server/routes/stats.tswith Express router - Implement
GET /api/stats/:host/:containerendpoint:- SSH to host
- Run
docker stats <container> --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.tsxUsageTab:- 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 updatesrc/services/sshDiscovery.ts:46-59- SSH exec pattern- Docker stats format:
--formatflag with Go templates
Acceptance Criteria:
GET /api/stats/:host/:containerendpoint 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.jsonCommit: YES
- Message:
feat(ui): add live docker stats to Usage tab - Files:
server/routes/stats.ts,src/components/RightPanel.tsx,server/index.ts
- Create
-
11. Implement Configurable Poll Interval
What to do:
- Add
pollInterval: numberto topologyStore (default: 30000ms) - Add
setPollInterval(ms: number)action - Update
App.tsxto 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 replacesrc/App.tsx:78-81- setInterval to make dynamicsrc/components/Header.tsx:78-89- Dropdown pattern to referencesrc/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.pngCommit: YES
- Message:
feat(ui): add configurable poll interval - Files:
src/store/topologyStore.ts,src/App.tsx,src/components/Header.tsx
- Add
-
12. Implement Command Palette
What to do:
- Create
src/components/CommandPalette.tsxmodal component - Add
commandPaletteOpen: booleanto 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 wiresrc/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.pngCommit: YES
- Message:
feat(ux): add command palette with Cmd+K - Files:
src/components/CommandPalette.tsx,src/App.tsx,src/store/topologyStore.ts
- Create
-
13. Implement Stale Data Warning
What to do:
- Add
consecutiveFailures: numberandlastSuccessfulDiscovery: Date | nullto topologyStore - Increment consecutiveFailures on API error, reset to 0 on success
- Create
src/components/StaleWarning.tsxbanner 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 updatesrc/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.pngCommit: YES
- Message:
feat(ux): add stale data warning banner - Files:
src/components/StaleWarning.tsx,src/App.tsx,src/store/topologyStore.ts
- Add
-
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.tsxto 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 updatesrc/store/topologyStore.ts:71-77- getChildNodes pattern to reference- Node data has
parentIdfield 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.pngCommit: YES
- Message:
feat(ux): add path highlighting for selected nodes - Files:
src/components/Graph/TopologyGraph.tsx,src/store/topologyStore.ts
- Add
Final Verification Wave
-
F1. Plan Compliance Audit —
oracleRead 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-highRuntsc --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(+playwrightskill) 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 —
deepFor 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
# 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