feat(ui): add Stop and Restart buttons to ProxyStatusWidget

- Add POST /api/cliproxy/proxy-stop API endpoint
- Add proxyStop method in api-client and useStopProxy hook
- Update ProxyStatusWidget with Stop and Restart controls when running
- Restart = stop + delay + start (for applying CLIProxyAPI updates)

Complements the fix in binary-manager.ts which now tells users to stop
proxy before updates can be applied.
This commit is contained in:
kaitranntt
2025-12-18 23:03:45 -05:00
parent 2adc272f27
commit c9ad0b0779
4 changed files with 107 additions and 19 deletions
+14 -1
View File
@@ -44,7 +44,7 @@ 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 { getProxyStatus as getProxyProcessStatus, stopProxy } from '../cliproxy/session-tracker';
import { ensureCliproxyService } from '../cliproxy/service-manager';
// Unified config imports
import {
@@ -1389,6 +1389,19 @@ apiRoutes.post('/cliproxy/proxy-start', async (_req: Request, res: Response): Pr
}
});
/**
* POST /api/cliproxy/proxy-stop - Stop the CLIProxy service
* Returns: { stopped, pid?, sessionCount?, error? }
*/
apiRoutes.post('/cliproxy/proxy-stop', (_req: Request, res: Response): void => {
try {
const result = stopProxy();
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<string, CliproxyModel[]>, totalCount: number }
+63 -18
View File
@@ -1,13 +1,13 @@
/**
* Proxy Status Widget
*
* Displays CLIProxy process status with start button for recovery.
* Displays CLIProxy process status with start/stop/restart controls.
* Shows: running state, port, session count, uptime.
*/
import { Activity, Power, RefreshCw, Clock, Users } from 'lucide-react';
import { Activity, Power, RefreshCw, Clock, Users, Square, RotateCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useProxyStatus, useStartProxy } from '@/hooks/use-cliproxy';
import { useProxyStatus, useStartProxy, useStopProxy } from '@/hooks/use-cliproxy';
import { cn } from '@/lib/utils';
function formatUptime(startedAt?: string): string {
@@ -28,8 +28,18 @@ function formatUptime(startedAt?: string): string {
export function ProxyStatusWidget() {
const { data: status, isLoading } = useProxyStatus();
const startProxy = useStartProxy();
const stopProxy = useStopProxy();
const isRunning = status?.running ?? false;
const isActioning = startProxy.isPending || stopProxy.isPending;
// Restart = stop then start
const handleRestart = async () => {
await stopProxy.mutateAsync();
// Small delay to ensure port is released
await new Promise((r) => setTimeout(r, 500));
startProxy.mutate();
};
return (
<div
@@ -61,21 +71,56 @@ export function ProxyStatusWidget() {
</div>
{isRunning && status ? (
<div className="mt-2 flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1">Port {status.port}</span>
{status.sessionCount !== undefined && status.sessionCount > 0 && (
<span className="flex items-center gap-1">
<Users className="w-3 h-3" />
{status.sessionCount} session{status.sessionCount !== 1 ? 's' : ''}
</span>
)}
{status.startedAt && (
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{formatUptime(status.startedAt)}
</span>
)}
</div>
<>
<div className="mt-2 flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1">Port {status.port}</span>
{status.sessionCount !== undefined && status.sessionCount > 0 && (
<span className="flex items-center gap-1">
<Users className="w-3 h-3" />
{status.sessionCount} session{status.sessionCount !== 1 ? 's' : ''}
</span>
)}
{status.startedAt && (
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{formatUptime(status.startedAt)}
</span>
)}
</div>
{/* Control buttons when running */}
<div className="mt-2 flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="h-7 text-xs gap-1 flex-1"
onClick={handleRestart}
disabled={isActioning}
title="Restart to apply updates"
>
{isActioning ? (
<RefreshCw className="w-3 h-3 animate-spin" />
) : (
<RotateCw className="w-3 h-3" />
)}
Restart
</Button>
<Button
variant="outline"
size="sm"
className="h-7 text-xs gap-1 hover:bg-destructive/10 hover:text-destructive hover:border-destructive/30"
onClick={() => stopProxy.mutate()}
disabled={isActioning}
title="Stop CLIProxy service"
>
{stopProxy.isPending ? (
<RefreshCw className="w-3 h-3 animate-spin" />
) : (
<Square className="w-3 h-3" />
)}
Stop
</Button>
</div>
</>
) : (
<div className="mt-2 flex items-center justify-between">
<span className="text-xs text-muted-foreground">Not running</span>
+21
View File
@@ -240,3 +240,24 @@ export function useStartProxy() {
},
});
}
export function useStopProxy() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => api.cliproxy.proxyStop(),
onSuccess: (data) => {
queryClient.invalidateQueries({ queryKey: ['proxy-status'] });
if (data.stopped) {
toast.success(
`CLIProxy stopped${data.sessionCount ? ` (${data.sessionCount} session(s) disconnected)` : ''}`
);
} else {
toast.error(data.error || 'Failed to stop CLIProxy');
}
},
onError: (error: Error) => {
toast.error(error.message);
},
});
}
+9
View File
@@ -182,6 +182,14 @@ export interface ProxyStartResult {
error?: string;
}
/** Result from stopping proxy service */
export interface ProxyStopResult {
stopped: boolean;
pid?: number;
sessionCount?: number;
error?: string;
}
// API
export const api = {
profiles: {
@@ -216,6 +224,7 @@ export const api = {
// Proxy process status and control
proxyStatus: () => request<ProxyProcessStatus>('/cliproxy/proxy-status'),
proxyStart: () => request<ProxyStartResult>('/cliproxy/proxy-start', { method: 'POST' }),
proxyStop: () => request<ProxyStopResult>('/cliproxy/proxy-stop', { method: 'POST' }),
// Stats and models for Overview tab
stats: () => request<{ usage: Record<string, unknown> }>('/cliproxy/usage'),