Files
supervisor-ui/NEXT_STEPS.md

887 lines
22 KiB
Markdown
Raw Normal View History

# 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<string, ProcessHistoryEntry[]>;
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<ProcessStateCode[]>([]);
const [groupFilter, setGroupFilter] = useState<string[]>([]);
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: <kbd>r</kbd>
```
#### 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