From 589cd2c2b7607b1092f6ee1ce4bf044269ba05e5 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 18 Dec 2025 01:26:30 -0500 Subject: [PATCH] feat(dashboard): add CLIProxy status widget with start button --- src/web-server/file-watcher.ts | 5 +- src/web-server/routes.ts | 30 +++++++ ui/src/components/proxy-status-widget.tsx | 100 ++++++++++++++++++++++ ui/src/hooks/use-cliproxy.ts | 31 +++++++ ui/src/hooks/use-websocket.ts | 4 + ui/src/lib/api-client.ts | 22 +++++ ui/src/pages/cliproxy.tsx | 6 ++ 7 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 ui/src/components/proxy-status-widget.tsx diff --git a/src/web-server/file-watcher.ts b/src/web-server/file-watcher.ts index cb4c425d..a57bce39 100644 --- a/src/web-server/file-watcher.ts +++ b/src/web-server/file-watcher.ts @@ -10,7 +10,7 @@ import * as path from 'path'; import { getCcsDir } from '../utils/config-manager'; export interface FileChangeEvent { - type: 'config-changed' | 'settings-changed' | 'profiles-changed'; + type: 'config-changed' | 'settings-changed' | 'profiles-changed' | 'proxy-status-changed'; path: string; timestamp: number; } @@ -25,6 +25,7 @@ export function createFileWatcher(onChange: FileChangeCallback): FSWatcher { path.join(ccsDir, 'config.json'), path.join(ccsDir, '*.settings.json'), path.join(ccsDir, 'profiles.json'), + path.join(ccsDir, 'cliproxy', 'sessions.json'), // Proxy session tracking ], { persistent: true, @@ -44,6 +45,8 @@ export function createFileWatcher(onChange: FileChangeCallback): FSWatcher { type = 'config-changed'; } else if (basename === 'profiles.json') { type = 'profiles-changed'; + } else if (basename === 'sessions.json') { + type = 'proxy-status-changed'; } else { type = 'settings-changed'; } diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index 493cc006..880faaa1 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -40,6 +40,8 @@ import { } from '../cliproxy/account-manager'; import type { CLIProxyProvider } from '../cliproxy/types'; import { getClaudeEnvVars } from '../cliproxy/config-generator'; +import { getProxyStatus as getProxyProcessStatus } from '../cliproxy/session-tracker'; +import { ensureCliproxyService } from '../cliproxy/service-manager'; // Unified config imports import { hasUnifiedConfig, @@ -1290,6 +1292,34 @@ apiRoutes.get('/cliproxy/status', async (_req: Request, res: Response): Promise< } }); +/** + * GET /api/cliproxy/proxy-status - Get detailed proxy process status + * Returns: { running, port?, pid?, sessionCount?, startedAt? } + * Uses session tracker for accurate multi-session status + */ +apiRoutes.get('/cliproxy/proxy-status', (_req: Request, res: Response): void => { + try { + const status = getProxyProcessStatus(); + res.json(status); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); + +/** + * POST /api/cliproxy/proxy-start - Start the CLIProxy service + * Returns: { started, alreadyRunning, port, error? } + * Starts proxy in background if not already running + */ +apiRoutes.post('/cliproxy/proxy-start', async (_req: Request, res: Response): Promise => { + try { + const result = await ensureCliproxyService(); + res.json(result); + } catch (error) { + res.status(500).json({ error: (error as Error).message }); + } +}); + /** * GET /api/cliproxy/models - Get available models from CLIProxyAPI * Returns: { models: CliproxyModel[], byCategory: Record, totalCount: number } diff --git a/ui/src/components/proxy-status-widget.tsx b/ui/src/components/proxy-status-widget.tsx new file mode 100644 index 00000000..a85981c2 --- /dev/null +++ b/ui/src/components/proxy-status-widget.tsx @@ -0,0 +1,100 @@ +/** + * Proxy Status Widget + * + * Displays CLIProxy process status with start button for recovery. + * Shows: running state, port, session count, uptime. + */ + +import { Activity, Power, RefreshCw, Clock, Users } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { useProxyStatus, useStartProxy } from '@/hooks/use-cliproxy'; +import { cn } from '@/lib/utils'; + +function formatUptime(startedAt?: string): string { + if (!startedAt) return ''; + const start = new Date(startedAt).getTime(); + const now = Date.now(); + const diff = now - start; + + const hours = Math.floor(diff / (1000 * 60 * 60)); + const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); + + if (hours > 0) { + return `${hours}h ${minutes}m`; + } + return `${minutes}m`; +} + +export function ProxyStatusWidget() { + const { data: status, isLoading } = useProxyStatus(); + const startProxy = useStartProxy(); + + const isRunning = status?.running ?? false; + + return ( +
+
+
+
+ CLIProxy Service +
+ +
+ {isLoading ? ( + + ) : isRunning ? ( + + ) : ( + + )} +
+
+ + {isRunning && status ? ( +
+ Port {status.port} + {status.sessionCount !== undefined && status.sessionCount > 0 && ( + + + {status.sessionCount} session{status.sessionCount !== 1 ? 's' : ''} + + )} + {status.startedAt && ( + + + {formatUptime(status.startedAt)} + + )} +
+ ) : ( +
+ Not running + +
+ )} +
+ ); +} diff --git a/ui/src/hooks/use-cliproxy.ts b/ui/src/hooks/use-cliproxy.ts index 5b92eb8f..9f23842e 100644 --- a/ui/src/hooks/use-cliproxy.ts +++ b/ui/src/hooks/use-cliproxy.ts @@ -209,3 +209,34 @@ export function useDeletePreset() { }, }); } + +// ==================== Proxy Process Status ==================== + +export function useProxyStatus() { + return useQuery({ + queryKey: ['proxy-status'], + queryFn: () => api.cliproxy.proxyStatus(), + refetchInterval: 30000, // Refresh every 30s as backup (websocket is primary) + }); +} + +export function useStartProxy() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: () => api.cliproxy.proxyStart(), + onSuccess: (data) => { + queryClient.invalidateQueries({ queryKey: ['proxy-status'] }); + if (data.alreadyRunning) { + toast.info('CLIProxy was already running'); + } else if (data.started) { + toast.success('CLIProxy started successfully'); + } else { + toast.error(data.error || 'Failed to start CLIProxy'); + } + }, + onError: (error: Error) => { + toast.error(error.message); + }, + }); +} diff --git a/ui/src/hooks/use-websocket.ts b/ui/src/hooks/use-websocket.ts index 8091ad13..f365bd1d 100644 --- a/ui/src/hooks/use-websocket.ts +++ b/ui/src/hooks/use-websocket.ts @@ -48,6 +48,10 @@ export function useWebSocket() { toast.info('Accounts updated'); break; + case 'proxy-status-changed': + queryClient.invalidateQueries({ queryKey: ['proxy-status'] }); + break; + case 'pong': // Heartbeat response break; diff --git a/ui/src/lib/api-client.ts b/ui/src/lib/api-client.ts index 3419796e..7ca955b4 100644 --- a/ui/src/lib/api-client.ts +++ b/ui/src/lib/api-client.ts @@ -154,6 +154,24 @@ export interface CreatePreset { haiku?: string; } +/** CLIProxy process status from session tracker */ +export interface ProxyProcessStatus { + running: boolean; + port?: number; + pid?: number; + sessionCount?: number; + startedAt?: string; +} + +/** Result from starting proxy service */ +export interface ProxyStartResult { + started: boolean; + alreadyRunning: boolean; + port: number; + configRegenerated?: boolean; + error?: string; +} + // API export const api = { profiles: { @@ -185,6 +203,10 @@ export const api = { }), delete: (name: string) => request(`/cliproxy/${name}`, { method: 'DELETE' }), + // Proxy process status and control + proxyStatus: () => request('/cliproxy/proxy-status'), + proxyStart: () => request('/cliproxy/proxy-start', { method: 'POST' }), + // Stats and models for Overview tab stats: () => request<{ usage: Record }>('/cliproxy/usage'), models: () => request('/cliproxy/models'), diff --git a/ui/src/pages/cliproxy.tsx b/ui/src/pages/cliproxy.tsx index 981ff7c1..b296c7fe 100644 --- a/ui/src/pages/cliproxy.tsx +++ b/ui/src/pages/cliproxy.tsx @@ -15,6 +15,7 @@ import { QuickSetupWizard } from '@/components/quick-setup-wizard'; import { AddAccountDialog } from '@/components/add-account-dialog'; import { ProviderEditor } from '@/components/cliproxy/provider-editor'; import { ProviderLogo } from '@/components/cliproxy/provider-logo'; +import { ProxyStatusWidget } from '@/components/proxy-status-widget'; import { useCliproxy, useCliproxyAuth, @@ -307,6 +308,11 @@ export function CliproxyPage() {
+ {/* Proxy Status Widget */} +
+ +
+ {/* Footer Stats */}