# Supervisor UI - Implementation Guide for Phases 2-12 ## ✅ Phase 1 Complete: Log Viewer - Real-time log viewing with syntax highlighting - Play/pause controls and auto-scroll - Search and filtering - Download and clear logs - Process selector with stdout/stderr switching --- ## 📋 Remaining Implementation Plan ### **Phase 2: Process Groups** (6-8 hours) #### Files to Create: **1. components/groups/GroupCard.tsx** ```typescript 'use client'; import { ProcessInfo } from '@/lib/supervisor/types'; // Card showing group with expandable process list // Group-level start/stop/restart buttons // Group statistics (X running, Y stopped, Z fatal) ``` **2. components/groups/GroupView.tsx** ```typescript 'use client'; // Container for displaying processes grouped by group name // Collapsible sections for each group // Uses GroupCard for each group ``` **3. components/groups/GroupSelector.tsx** ```typescript 'use client'; // Toggle button: Flat View | Grouped View // Updates state to switch between views ``` **4. app/groups/page.tsx** ```typescript 'use client'; // Dedicated page for group-centric management // Shows all groups with their processes // Group-level actions prominently displayed ``` #### API Routes to Create: **5. app/api/supervisor/groups/[name]/start/route.ts** ```typescript import { createSupervisorClient } from '@/lib/supervisor/client'; export async function POST(request, { params }) { const { name } = await params; const body = await request.json().catch(() => ({})); const client = createSupervisorClient(); const results = await client.startProcessGroup(name, body.wait ?? true); return NextResponse.json({ success: true, results }); } ``` **6. app/api/supervisor/groups/[name]/stop/route.ts** - Same as start, call `stopProcessGroup` **7. app/api/supervisor/groups/[name]/restart/route.ts** ```typescript // Stop then start the group const results = await client.stopProcessGroup(name, true); await client.startProcessGroup(name, true); ``` #### Hooks to Add (lib/hooks/useSupervisor.ts): ```typescript export function useStartProcessGroup() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ name, wait }: { name: string; wait?: boolean }) => fetch(`/api/supervisor/groups/${name}/start`, { method: 'POST', body: JSON.stringify({ wait }), }).then(r => r.json()), onSuccess: () => { toast.success('Process group started'); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); }, }); } // Add useStopProcessGroup and useRestartProcessGroup similarly ``` #### Update Existing Files: **8. app/processes/page.tsx** - Add view toggle button and conditional rendering --- ### **Phase 3: Batch Operations** (4-6 hours) #### Files to Create: **1. components/process/BatchActions.tsx** ```typescript 'use client'; // Toolbar that appears when processes are selected // Shows: "X selected" + Start All | Stop All | Restart All buttons // Position: Fixed at bottom or floating ``` **2. components/process/ProcessSelector.tsx** ```typescript 'use client'; // Checkbox component for ProcessCard // Manages selection state // "Select All" checkbox for bulk selection ``` #### API Routes to Create: **3. app/api/supervisor/processes/start-all/route.ts** ```typescript export async function POST(request) { const body = await request.json().catch(() => ({})); const client = createSupervisorClient(); const results = await client.startAllProcesses(body.wait ?? true); return NextResponse.json({ success: true, results }); } ``` **4. app/api/supervisor/processes/stop-all/route.ts** - Similar, use `stopAllProcesses` **5. app/api/supervisor/processes/restart-all/route.ts** ```typescript // Stop all, then start all await client.stopAllProcesses(true); const results = await client.startAllProcesses(true); ``` #### Hooks to Add: ```typescript export function useStartAllProcesses() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (wait: boolean = true) => fetch('/api/supervisor/processes/start-all', { method: 'POST', body: JSON.stringify({ wait }), }).then(r => r.json()), onSuccess: () => { toast.success('All processes started'); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); }, }); } // Add useStopAllProcesses and useRestartAllProcesses ``` #### Update ProcessCard.tsx: - Add checkbox in corner - Pass selection state from parent - Add onClick handler to toggle selection --- ### **Phase 4: Configuration Management** (8-10 hours) #### Files to Create: **1. components/config/ConfigViewer.tsx** ```typescript 'use client'; import { useConfigInfo } from '@/lib/hooks/useSupervisor'; // Displays all process configurations in a table // Columns: Name, Group, Command, Directory, Autostart, etc. // Sortable columns ``` **2. components/config/ConfigTable.tsx** ```typescript // Table component with sorting // Shows all config fields from ConfigInfo type ``` **3. components/config/ProcessGroupForm.tsx** ```typescript 'use client'; // Form to add a new process group // Input: group name // Calls useAddProcessGroup hook ``` **4. components/config/ReloadConfigButton.tsx** ```typescript 'use client'; import { useReloadConfig } from '@/lib/hooks/useSupervisor'; // Button that reloads configuration // Shows confirmation dialog // Displays results: added, changed, removed groups ``` **5. components/config/DangerZone.tsx** ```typescript 'use client'; // Red-bordered section at bottom of config page // Contains: Shutdown Supervisor, Restart Supervisor buttons // Strong confirmation dialogs (type "CONFIRM") ``` #### API Routes to Create: **6. app/api/supervisor/config/route.ts** ```typescript export async function GET() { const client = createSupervisorClient(); const config = await client.getAllConfigInfo(); return NextResponse.json(config); } ``` **7. app/api/supervisor/config/reload/route.ts** ```typescript export async function POST() { const client = createSupervisorClient(); const result = await client.reloadConfig(); return NextResponse.json(result); // { added, changed, removed } } ``` **8. app/api/supervisor/groups/add/route.ts** ```typescript export async function POST(request) { const { name } = await request.json(); const client = createSupervisorClient(); const result = await client.addProcessGroup(name); return NextResponse.json({ success: result }); } ``` **9. app/api/supervisor/groups/[name]/route.ts** ```typescript export async function DELETE(request, { params }) { const { name } = await params; const client = createSupervisorClient(); const result = await client.removeProcessGroup(name); return NextResponse.json({ success: result }); } ``` **10. app/api/supervisor/shutdown/route.ts** ```typescript export async function POST() { const client = createSupervisorClient(); const result = await client.shutdown(); return NextResponse.json({ success: result }); } ``` **11. app/api/supervisor/restart/route.ts** - Similar, use `client.restart()` #### Hooks to Add: ```typescript export function useConfigInfo() { return useQuery({ queryKey: [...supervisorKeys.all, 'config'], queryFn: () => fetch('/api/supervisor/config').then(r => r.json()), refetchInterval: 30000, // 30 seconds }); } export function useReloadConfig() { const queryClient = useQueryClient(); return useMutation({ mutationFn: () => fetch('/api/supervisor/config/reload', { method: 'POST' }).then(r => r.json()), onSuccess: (data) => { toast.success(`Config reloaded: ${data.added.length} added, ${data.changed.length} changed, ${data.removed.length} removed`); queryClient.invalidateQueries({ queryKey: supervisorKeys.all }); }, }); } // Add useAddProcessGroup, useRemoveProcessGroup, useShutdownSupervisor, useRestartSupervisor ``` #### Update app/config/page.tsx: - Replace placeholder with full implementation - Use ConfigViewer, ReloadConfigButton, ProcessGroupForm - Add DangerZone at bottom --- ### **Phase 5: Charts & Metrics** (8-12 hours) #### Files to Create: **1. lib/stores/metricsStore.ts** ```typescript import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface MetricsState { processHistory: Record; addSnapshot: (processes: ProcessInfo[]) => void; clearHistory: () => void; } // Store process snapshots every X seconds // Keep last N entries (configurable) // Persist to localStorage ``` **2. components/charts/UptimeChart.tsx** ```typescript 'use client'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts'; // Show uptime over time for selected process // X-axis: Time, Y-axis: Uptime (seconds) ``` **3. components/charts/RestartFrequencyChart.tsx** ```typescript 'use client'; import { BarChart, Bar, XAxis, YAxis } from 'recharts'; // Show number of restarts per process // X-axis: Process name, Y-axis: Restart count ``` **4. components/charts/StateDistributionChart.tsx** ```typescript 'use client'; import { PieChart, Pie, Cell } from 'recharts'; // Pie chart showing: Running, Stopped, Fatal // Color-coded by state ``` **5. components/charts/EventTimeline.tsx** ```typescript 'use client'; // Timeline showing state changes // Events: started, stopped, restarted, fatal // Scrollable timeline with timestamps ``` **6. app/metrics/page.tsx** ```typescript 'use client'; import { useMetricsStore } from '@/lib/stores/metricsStore'; // Dashboard with all charts // Time range selector (1h, 24h, 7d, 30d) // Process selector for uptime chart ``` #### Update Navbar: - Add "Metrics" link to navigation #### Data Collection: Add to Providers.tsx or create useMetricsCollection hook: ```typescript useEffect(() => { const interval = setInterval(() => { const processes = queryClient.getQueryData(supervisorKeys.processes()); if (processes) { metricsStore.addSnapshot(processes); } }, 60000); // Every minute return () => clearInterval(interval); }, []); ``` --- ### **Phase 6: Search & Filtering** (2-3 hours) #### Files to Create: **1. components/process/ProcessSearch.tsx** ```typescript 'use client'; // Search input similar to LogSearch // Filters processes by name or group ``` **2. components/process/ProcessFilters.tsx** ```typescript 'use client'; // Dropdown or chips for filtering // Options: State (running/stopped/fatal), Group // Multiple filters can be active ``` #### Update app/processes/page.tsx: ```typescript const [searchTerm, setSearchTerm] = useState(''); const [stateFilter, setStateFilter] = useState([]); const [groupFilter, setGroupFilter] = useState([]); const filteredProcesses = processes?.filter(proc => { if (searchTerm && !proc.name.toLowerCase().includes(searchTerm.toLowerCase())) return false; if (stateFilter.length > 0 && !stateFilter.includes(proc.state)) return false; if (groupFilter.length > 0 && !groupFilter.includes(proc.group)) return false; return true; }); ``` #### Persist Filters: ```typescript useEffect(() => { localStorage.setItem('processFilters', JSON.stringify({ searchTerm, stateFilter, groupFilter })); }, [searchTerm, stateFilter, groupFilter]); ``` --- ### **Phase 7: Signal Operations** (3-4 hours) #### Files to Create: **1. components/process/SignalSender.tsx** ```typescript 'use client'; // Modal dialog with signal dropdown // Common signals: HUP, USR1, USR2, TERM, KILL, INT // Confirmation for TERM and KILL // Input for custom signal ``` **2. components/process/SignalButton.tsx** ```typescript 'use client'; // Button that opens SignalSender modal // Icon: Zap or Command ``` #### API Routes to Create: **3. app/api/supervisor/processes/[name]/signal/route.ts** ```typescript export async function POST(request, { params }) { const { name } = await params; const { signal } = await request.json(); const client = createSupervisorClient(); const result = await client.signalProcess(name, signal); return NextResponse.json({ success: result }); } ``` **4. app/api/supervisor/groups/[name]/signal/route.ts** - Use `signalProcessGroup` **5. app/api/supervisor/processes/signal-all/route.ts** - Use `signalAllProcesses` #### Hooks to Add: ```typescript export function useSignalProcess() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ name, signal }: { name: string; signal: string }) => fetch(`/api/supervisor/processes/${name}/signal`, { method: 'POST', body: JSON.stringify({ signal }), }).then(r => r.json()), onSuccess: (_, { name, signal }) => { toast.success(`Signal ${signal} sent to ${name}`); queryClient.invalidateQueries({ queryKey: supervisorKeys.process(name) }); }, }); } ``` #### Update ProcessCard.tsx: - Add signal button in actions section - Use SignalButton component --- ### **Phase 8: Process Stdin** (2-3 hours) #### Files to Create: **1. components/process/StdinInput.tsx** ```typescript 'use client'; // Modal with textarea for multi-line input // Send button // Confirmation before sending ``` **2. components/process/StdinButton.tsx** ```typescript 'use client'; // Button that opens StdinInput modal // Icon: Terminal or Keyboard ``` #### API Route: **3. app/api/supervisor/processes/[name]/stdin/route.ts** ```typescript export async function POST(request, { params }) { const { name } = await params; const { chars } = await request.json(); const client = createSupervisorClient(); const result = await client.sendProcessStdin(name, chars); return NextResponse.json({ success: result }); } ``` #### Hook: ```typescript export function useSendProcessStdin() { return useMutation({ mutationFn: ({ name, chars }: { name: string; chars: string }) => fetch(`/api/supervisor/processes/${name}/stdin`, { method: 'POST', body: JSON.stringify({ chars }), }).then(r => r.json()), onSuccess: (_, { name }) => { toast.success(`Input sent to ${name}`); }, }); } ``` #### Update ProcessCard.tsx: - Add stdin button (collapsed/advanced section) --- ### **Phase 9: Keyboard Shortcuts** (3-4 hours) #### Installation: ```bash pnpm add react-hotkeys-hook ``` #### Files to Create: **1. lib/hooks/useKeyboardShortcuts.ts** ```typescript 'use client'; import { useHotkeys } from 'react-hotkeys-hook'; import { useRouter } from 'next/navigation'; export function useGlobalKeyboardShortcuts() { const router = useRouter(); useHotkeys('/', () => { /* Focus search */ }, { preventDefault: true }); useHotkeys('r', () => { /* Refresh page */ }); useHotkeys('g,h', () => router.push('/')); useHotkeys('g,p', () => router.push('/processes')); useHotkeys('g,l', () => router.push('/logs')); useHotkeys('g,c', () => router.push('/config')); useHotkeys('?', () => { /* Open shortcuts modal */ }); } ``` **2. components/ui/KeyboardShortcutsModal.tsx** ```typescript 'use client'; // Modal showing all keyboard shortcuts // Organized by category // Opened with "?" key ``` **3. components/ui/KeyboardShortcutBadge.tsx** ```typescript // Small badge showing keyboard shortcut // Used in tooltips // Example: r ``` #### Update app/layout.tsx: ```typescript import { useGlobalKeyboardShortcuts } from '@/lib/hooks/useKeyboardShortcuts'; // Inside layout component (need to make it a client component wrapper) useGlobalKeyboardShortcuts(); ``` --- ### **Phase 10: Supervisor Control** (2-3 hours) Already covered in Phase 4 (DangerZone component). Implement strong confirmations: ```typescript const handleShutdown = () => { const confirmation = prompt('Type "CONFIRM" to shutdown Supervisor:'); if (confirmation !== 'CONFIRM') { toast.error('Shutdown cancelled'); return; } shutdownMutation.mutate(); }; ``` --- ### **Phase 11: WebSocket/SSE Real-time** (12-16 hours) #### Recommended Approach: Server-Sent Events (SSE) #### Files to Create: **1. app/api/supervisor/events/route.ts** ```typescript export async function GET() { const encoder = new TextEncoder(); const stream = new ReadableStream({ async start(controller) { const client = createSupervisorClient(); let lastState = ''; const interval = setInterval(async () => { try { const processes = await client.getAllProcessInfo(); const state = JSON.stringify(processes); if (state !== lastState) { lastState = state; const data = `data: ${state}\n\n`; controller.enqueue(encoder.encode(data)); } } catch (error) { console.error('SSE error:', error); } }, 1000); // Poll every second // Cleanup on close return () => clearInterval(interval); }, }); return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', }, }); } ``` **2. lib/hooks/useSupervisorSSE.ts** ```typescript 'use client'; import { useEffect, useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; export function useSupervisorSSE() { const queryClient = useQueryClient(); const [connected, setConnected] = useState(false); useEffect(() => { const eventSource = new EventSource('/api/supervisor/events'); eventSource.onopen = () => { setConnected(true); }; eventSource.onmessage = (event) => { const processes = JSON.parse(event.data); queryClient.setQueryData(supervisorKeys.processes(), processes); }; eventSource.onerror = () => { setConnected(false); eventSource.close(); }; return () => eventSource.close(); }, []); return { connected }; } ``` **3. components/ui/ConnectionStatus.tsx** ```typescript 'use client'; import { useSupervisorSSE } from '@/lib/hooks/useSupervisorSSE'; // Green dot: connected // Yellow dot: connecting // Red dot: disconnected ``` #### Update Providers.tsx: ```typescript export function Providers({ children }) { useSupervisorSSE(); // Enable SSE return ( {/* ... */} ); } ``` #### Update Navbar: - Add ConnectionStatus indicator --- ### **Phase 12: Multi-Instance Support** (16-20 hours) #### Files to Create: **1. lib/stores/connectionsStore.ts** ```typescript import { create } from 'zustand'; import { persist } from 'zustand/middleware'; interface Connection { id: string; name: string; host: string; port: number; username?: string; password?: string; } interface ConnectionsState { connections: Connection[]; activeConnectionId: string | null; addConnection: (conn: Connection) => void; removeConnection: (id: string) => void; setActiveConnection: (id: string) => void; getActiveConnection: () => Connection | null; } ``` **2. components/instances/InstanceSwitcher.tsx** ```typescript 'use client'; import { useConnectionsStore } from '@/lib/stores/connectionsStore'; // Dropdown in navbar // Shows all connections // Click to switch active instance ``` **3. components/instances/ConnectionManager.tsx** ```typescript 'use client'; // Table of all connections // Edit, Delete, Test buttons // Add new connection button ``` **4. components/instances/AddConnectionModal.tsx** ```typescript 'use client'; // Form: Name, Host, Port, Username (optional), Password (optional) // Test connection button // Save button ``` **5. app/instances/page.tsx** ```typescript 'use client'; // Page to manage all connections // Uses ConnectionManager ``` #### Update API Routes: All routes need to accept instance configuration. Two approaches: **Option A:** Pass instance ID in header ```typescript const headers = { 'X-Instance-Id': instanceId }; fetch('/api/supervisor/processes', { headers }); ``` Then in API route: ```typescript const instanceId = request.headers.get('X-Instance-Id'); const connection = getConnectionById(instanceId); // from store const client = createSupervisorClient(connection); ``` **Option B:** Pass connection config in body (less secure) #### Update All Hooks: Add optional `instanceId` parameter: ```typescript export function useProcesses(options?: { instanceId?: string }) { const activeInstance = useConnectionsStore(s => s.getActiveConnection()); const instance = options?.instanceId || activeInstance?.id; return useQuery({ queryKey: [...supervisorKeys.all, instance, 'processes'], queryFn: () => fetch('/api/supervisor/processes', { headers: { 'X-Instance-Id': instance }, }).then(r => r.json()), }); } ``` #### Update Dashboard: - Show overview of all instances - Quick stats for each - Click to view instance details --- ## 🚀 Quick Start for Next Session 1. **Start where we left off:** ```bash cd /home/valknar/Projects/supervisor-ui git pull pnpm dev ``` 2. **Pick a phase** from above (recommend Phase 2 or 3 next) 3. **Follow the file-by-file instructions** - each phase is self-contained 4. **Test as you go:** ```bash pnpm build # Test compilation ``` 5. **Commit when phase complete:** ```bash git add -A git commit -m "feat: complete Phase X - [description]" git push ``` --- ## 📊 Progress Tracking - [x] **Phase 1:** Log Viewer ✅ - [ ] **Phase 2:** Process Groups (6-8h) - [ ] **Phase 3:** Batch Operations (4-6h) - [ ] **Phase 4:** Configuration Management (8-10h) - [ ] **Phase 5:** Charts & Metrics (8-12h) - [ ] **Phase 6:** Search & Filtering (2-3h) - [ ] **Phase 7:** Signal Operations (3-4h) - [ ] **Phase 8:** Process Stdin (2-3h) - [ ] **Phase 9:** Keyboard Shortcuts (3-4h) - [ ] **Phase 10:** Supervisor Control (included in Phase 4) - [ ] **Phase 11:** WebSocket/SSE (12-16h) - [ ] **Phase 12:** Multi-Instance (16-20h) **Total Remaining:** ~75 hours --- ## 💡 Tips for Implementation 1. **Work in small commits** - One feature at a time 2. **Test the build frequently** - `pnpm build` catches type errors 3. **Follow the type signatures** - TypeScript will guide you 4. **Reuse existing patterns** - Look at Phase 1 code for reference 5. **Don't over-engineer** - Implement exactly what's described 6. **Update README** - Document new features as you add them --- ## 🎯 Recommended Order If you want to maximize value with minimal time: 1. **Phase 6** (2-3h) - Search & Filtering → Immediate productivity boost 2. **Phase 3** (4-6h) - Batch Operations → High user value 3. **Phase 7** (3-4h) - Signal Operations → Complete process control 4. **Phase 2** (6-8h) - Process Groups → Better organization 5. **Phase 4** (8-10h) - Config Management → Production ready 6. **Phase 5** (8-12h) - Charts & Metrics → Visual appeal This gets you 80% of the value in ~40 hours instead of 75. --- **Last Updated:** November 23, 2025 **Current Version:** 0.2.0 (Phase 1 Complete) **Repository:** ssh://dev.pivoine.art:2222/valknar/supervisor-ui.git