887 lines
22 KiB
Markdown
887 lines
22 KiB
Markdown
|
|
# 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
|