Files
supervisor-ui/NEXT_STEPS.md
Sebastian Krüger 5c028cdc11
All checks were successful
Build and Push Docker Image to Gitea / build-and-push (push) Successful in 58s
feat: implement Phase 2 - Process Groups Management
Features added:
- Group-based process organization with collapsible cards
- Batch operations for groups (Start All, Stop All, Restart All)
- Group statistics display (running, stopped, fatal counts)
- Dedicated /groups page for group-centric management
- View toggle in /processes page (Flat view | Grouped view)

Implementation details:
- Created group API routes: /api/supervisor/groups/[name]/{start,stop,restart}
- Added React Query hooks: useStartProcessGroup, useStopProcessGroup, useRestartProcessGroup
- Created components: GroupCard, GroupView, GroupSelector
- Updated Navbar with Groups navigation link
- Integrated grouped view in processes page with toggle

Phase 2 complete (6-8 hours estimated)
2025-11-23 19:08:10 +01:00

22 KiB

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

'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

'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

'use client';
// Toggle button: Flat View | Grouped View
// Updates state to switch between views

4. app/groups/page.tsx

'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

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

// Stop then start the group
const results = await client.stopProcessGroup(name, true);
await client.startProcessGroup(name, true);

Hooks to Add (lib/hooks/useSupervisor.ts):

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

'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

'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

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

// Stop all, then start all
await client.stopAllProcesses(true);
const results = await client.startAllProcesses(true);

Hooks to Add:

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

'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

// Table component with sorting
// Shows all config fields from ConfigInfo type

3. components/config/ProcessGroupForm.tsx

'use client';
// Form to add a new process group
// Input: group name
// Calls useAddProcessGroup hook

4. components/config/ReloadConfigButton.tsx

'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

'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

export async function GET() {
  const client = createSupervisorClient();
  const config = await client.getAllConfigInfo();
  return NextResponse.json(config);
}

7. app/api/supervisor/config/reload/route.ts

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

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

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

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:

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

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

'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

'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

'use client';
import { PieChart, Pie, Cell } from 'recharts';
// Pie chart showing: Running, Stopped, Fatal
// Color-coded by state

5. components/charts/EventTimeline.tsx

'use client';
// Timeline showing state changes
// Events: started, stopped, restarted, fatal
// Scrollable timeline with timestamps

6. app/metrics/page.tsx

'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:

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

'use client';
// Search input similar to LogSearch
// Filters processes by name or group

2. components/process/ProcessFilters.tsx

'use client';
// Dropdown or chips for filtering
// Options: State (running/stopped/fatal), Group
// Multiple filters can be active

Update app/processes/page.tsx:

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:

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

'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

'use client';
// Button that opens SignalSender modal
// Icon: Zap or Command

API Routes to Create:

3. app/api/supervisor/processes/[name]/signal/route.ts

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:

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

'use client';
// Modal with textarea for multi-line input
// Send button
// Confirmation before sending

2. components/process/StdinButton.tsx

'use client';
// Button that opens StdinInput modal
// Icon: Terminal or Keyboard

API Route:

3. app/api/supervisor/processes/[name]/stdin/route.ts

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:

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:

pnpm add react-hotkeys-hook

Files to Create:

1. lib/hooks/useKeyboardShortcuts.ts

'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

'use client';
// Modal showing all keyboard shortcuts
// Organized by category
// Opened with "?" key

3. components/ui/KeyboardShortcutBadge.tsx

// Small badge showing keyboard shortcut
// Used in tooltips
// Example: <kbd>r</kbd>

Update app/layout.tsx:

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:

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)

Files to Create:

1. app/api/supervisor/events/route.ts

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

'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

'use client';
import { useSupervisorSSE } from '@/lib/hooks/useSupervisorSSE';
// Green dot: connected
// Yellow dot: connecting
// Red dot: disconnected

Update Providers.tsx:

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

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

'use client';
import { useConnectionsStore } from '@/lib/stores/connectionsStore';
// Dropdown in navbar
// Shows all connections
// Click to switch active instance

3. components/instances/ConnectionManager.tsx

'use client';
// Table of all connections
// Edit, Delete, Test buttons
// Add new connection button

4. components/instances/AddConnectionModal.tsx

'use client';
// Form: Name, Host, Port, Username (optional), Password (optional)
// Test connection button
// Save button

5. app/instances/page.tsx

'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

const headers = { 'X-Instance-Id': instanceId };
fetch('/api/supervisor/processes', { headers });

Then in API route:

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:

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:

    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:

    pnpm build  # Test compilation
    
  5. Commit when phase complete:

    git add -A
    git commit -m "feat: complete Phase X - [description]"
    git push
    

📊 Progress Tracking

  • 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

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