mirror of
https://github.com/tiennm99/ccs.git
synced 2026-08-17 18:22:56 +00:00
feat(dashboard): add CLIProxy status widget with start button
This commit is contained in:
@@ -10,7 +10,7 @@ import * as path from 'path';
|
|||||||
import { getCcsDir } from '../utils/config-manager';
|
import { getCcsDir } from '../utils/config-manager';
|
||||||
|
|
||||||
export interface FileChangeEvent {
|
export interface FileChangeEvent {
|
||||||
type: 'config-changed' | 'settings-changed' | 'profiles-changed';
|
type: 'config-changed' | 'settings-changed' | 'profiles-changed' | 'proxy-status-changed';
|
||||||
path: string;
|
path: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,7 @@ export function createFileWatcher(onChange: FileChangeCallback): FSWatcher {
|
|||||||
path.join(ccsDir, 'config.json'),
|
path.join(ccsDir, 'config.json'),
|
||||||
path.join(ccsDir, '*.settings.json'),
|
path.join(ccsDir, '*.settings.json'),
|
||||||
path.join(ccsDir, 'profiles.json'),
|
path.join(ccsDir, 'profiles.json'),
|
||||||
|
path.join(ccsDir, 'cliproxy', 'sessions.json'), // Proxy session tracking
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
persistent: true,
|
persistent: true,
|
||||||
@@ -44,6 +45,8 @@ export function createFileWatcher(onChange: FileChangeCallback): FSWatcher {
|
|||||||
type = 'config-changed';
|
type = 'config-changed';
|
||||||
} else if (basename === 'profiles.json') {
|
} else if (basename === 'profiles.json') {
|
||||||
type = 'profiles-changed';
|
type = 'profiles-changed';
|
||||||
|
} else if (basename === 'sessions.json') {
|
||||||
|
type = 'proxy-status-changed';
|
||||||
} else {
|
} else {
|
||||||
type = 'settings-changed';
|
type = 'settings-changed';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ 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 { ensureCliproxyService } from '../cliproxy/service-manager';
|
||||||
// Unified config imports
|
// Unified config imports
|
||||||
import {
|
import {
|
||||||
hasUnifiedConfig,
|
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<void> => {
|
||||||
|
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
|
* 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 }
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'rounded-lg border p-3 transition-colors',
|
||||||
|
isRunning ? 'border-green-500/30 bg-green-500/5' : 'border-muted bg-muted/30'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'w-2 h-2 rounded-full',
|
||||||
|
isRunning ? 'bg-green-500 animate-pulse' : 'bg-muted-foreground/30'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-medium">CLIProxy Service</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{isLoading ? (
|
||||||
|
<RefreshCw className="w-3 h-3 animate-spin text-muted-foreground" />
|
||||||
|
) : isRunning ? (
|
||||||
|
<Activity className="w-3 h-3 text-green-600" />
|
||||||
|
) : (
|
||||||
|
<Power className="w-3 h-3 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</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 justify-between">
|
||||||
|
<span className="text-xs text-muted-foreground">Not running</span>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 text-xs gap-1"
|
||||||
|
onClick={() => startProxy.mutate()}
|
||||||
|
disabled={startProxy.isPending}
|
||||||
|
>
|
||||||
|
{startProxy.isPending ? (
|
||||||
|
<RefreshCw className="w-3 h-3 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Power className="w-3 h-3" />
|
||||||
|
)}
|
||||||
|
Start
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ export function useWebSocket() {
|
|||||||
toast.info('Accounts updated');
|
toast.info('Accounts updated');
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'proxy-status-changed':
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['proxy-status'] });
|
||||||
|
break;
|
||||||
|
|
||||||
case 'pong':
|
case 'pong':
|
||||||
// Heartbeat response
|
// Heartbeat response
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -154,6 +154,24 @@ export interface CreatePreset {
|
|||||||
haiku?: string;
|
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
|
// API
|
||||||
export const api = {
|
export const api = {
|
||||||
profiles: {
|
profiles: {
|
||||||
@@ -185,6 +203,10 @@ export const api = {
|
|||||||
}),
|
}),
|
||||||
delete: (name: string) => request(`/cliproxy/${name}`, { method: 'DELETE' }),
|
delete: (name: string) => request(`/cliproxy/${name}`, { method: 'DELETE' }),
|
||||||
|
|
||||||
|
// Proxy process status and control
|
||||||
|
proxyStatus: () => request<ProxyProcessStatus>('/cliproxy/proxy-status'),
|
||||||
|
proxyStart: () => request<ProxyStartResult>('/cliproxy/proxy-start', { 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'),
|
||||||
models: () => request<CliproxyModelsResponse>('/cliproxy/models'),
|
models: () => request<CliproxyModelsResponse>('/cliproxy/models'),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { QuickSetupWizard } from '@/components/quick-setup-wizard';
|
|||||||
import { AddAccountDialog } from '@/components/add-account-dialog';
|
import { AddAccountDialog } from '@/components/add-account-dialog';
|
||||||
import { ProviderEditor } from '@/components/cliproxy/provider-editor';
|
import { ProviderEditor } from '@/components/cliproxy/provider-editor';
|
||||||
import { ProviderLogo } from '@/components/cliproxy/provider-logo';
|
import { ProviderLogo } from '@/components/cliproxy/provider-logo';
|
||||||
|
import { ProxyStatusWidget } from '@/components/proxy-status-widget';
|
||||||
import {
|
import {
|
||||||
useCliproxy,
|
useCliproxy,
|
||||||
useCliproxyAuth,
|
useCliproxyAuth,
|
||||||
@@ -307,6 +308,11 @@ export function CliproxyPage() {
|
|||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|
||||||
|
{/* Proxy Status Widget */}
|
||||||
|
<div className="p-3 border-t">
|
||||||
|
<ProxyStatusWidget />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Footer Stats */}
|
{/* Footer Stats */}
|
||||||
<div className="p-3 border-t bg-background text-xs text-muted-foreground">
|
<div className="p-3 border-t bg-background text-xs text-muted-foreground">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
Reference in New Issue
Block a user