mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 12:14:19 +00:00
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:
1 parent
2adc272f27
commit
c9ad0b0779
4 files changed
+107
-19
No files matched your search
@@ -44,7 +44,7 @@ import {
|
|||||||
} from '../cliproxy/account-manager';
|
} from '../cliproxy/account-manager';
|
||||||
import type { CLIProxyProvider } from '../cliproxy/types';
|
import type { CLIProxyProvider } from '../cliproxy/types';
|
||||||
import { getClaudeEnvVars } from '../cliproxy/config-generator';
|
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';
|
import { ensureCliproxyService } from '../cliproxy/service-manager';
|
||||||
// Unified config imports
|
// Unified config imports
|
||||||
import {
|
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
|
* GET /api/cliproxy/models - Get available models from CLIProxyAPI
|
||||||
* Returns: { models: CliproxyModel[], byCategory: Record<string, CliproxyModel[]>, totalCount: number }
|
* Returns: { models: CliproxyModel[], byCategory: Record<string, CliproxyModel[]>, totalCount: number }
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Proxy Status Widget
|
* 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.
|
* 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 { 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';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function formatUptime(startedAt?: string): string {
|
function formatUptime(startedAt?: string): string {
|
||||||
@@ -28,8 +28,18 @@ function formatUptime(startedAt?: string): string {
|
|||||||
export function ProxyStatusWidget() {
|
export function ProxyStatusWidget() {
|
||||||
const { data: status, isLoading } = useProxyStatus();
|
const { data: status, isLoading } = useProxyStatus();
|
||||||
const startProxy = useStartProxy();
|
const startProxy = useStartProxy();
|
||||||
|
const stopProxy = useStopProxy();
|
||||||
|
|
||||||
const isRunning = status?.running ?? false;
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -61,21 +71,56 @@ export function ProxyStatusWidget() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isRunning && status ? (
|
{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>
|
<div className="mt-2 flex items-center gap-4 text-xs text-muted-foreground">
|
||||||
{status.sessionCount !== undefined && status.sessionCount > 0 && (
|
<span className="flex items-center gap-1">Port {status.port}</span>
|
||||||
<span className="flex items-center gap-1">
|
{status.sessionCount !== undefined && status.sessionCount > 0 && (
|
||||||
<Users className="w-3 h-3" />
|
<span className="flex items-center gap-1">
|
||||||
{status.sessionCount} session{status.sessionCount !== 1 ? 's' : ''}
|
<Users className="w-3 h-3" />
|
||||||
</span>
|
{status.sessionCount} session{status.sessionCount !== 1 ? 's' : ''}
|
||||||
)}
|
</span>
|
||||||
{status.startedAt && (
|
)}
|
||||||
<span className="flex items-center gap-1">
|
{status.startedAt && (
|
||||||
<Clock className="w-3 h-3" />
|
<span className="flex items-center gap-1">
|
||||||
{formatUptime(status.startedAt)}
|
<Clock className="w-3 h-3" />
|
||||||
</span>
|
{formatUptime(status.startedAt)}
|
||||||
)}
|
</span>
|
||||||
</div>
|
)}
|
||||||
|
</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">
|
<div className="mt-2 flex items-center justify-between">
|
||||||
<span className="text-xs text-muted-foreground">Not running</span>
|
<span className="text-xs text-muted-foreground">Not running</span>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -182,6 +182,14 @@ export interface ProxyStartResult {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Result from stopping proxy service */
|
||||||
|
export interface ProxyStopResult {
|
||||||
|
stopped: boolean;
|
||||||
|
pid?: number;
|
||||||
|
sessionCount?: number;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// API
|
// API
|
||||||
export const api = {
|
export const api = {
|
||||||
profiles: {
|
profiles: {
|
||||||
@@ -216,6 +224,7 @@ export const api = {
|
|||||||
// Proxy process status and control
|
// Proxy process status and control
|
||||||
proxyStatus: () => request<ProxyProcessStatus>('/cliproxy/proxy-status'),
|
proxyStatus: () => request<ProxyProcessStatus>('/cliproxy/proxy-status'),
|
||||||
proxyStart: () => request<ProxyStartResult>('/cliproxy/proxy-start', { method: 'POST' }),
|
proxyStart: () => request<ProxyStartResult>('/cliproxy/proxy-start', { method: 'POST' }),
|
||||||
|
proxyStop: () => request<ProxyStopResult>('/cliproxy/proxy-stop', { method: 'POST' }),
|
||||||
|
|
||||||
// Stats and models for Overview tab
|
// Stats and models for Overview tab
|
||||||
stats: () => request<{ usage: Record<string, unknown> }>('/cliproxy/usage'),
|
stats: () => request<{ usage: Record<string, unknown> }>('/cliproxy/usage'),
|
||||||
|
|||||||
Reference in new issue
Block a user