mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 22:16:41 +00:00
feat(ui): show CLIProxyAPI update availability in dashboard
- Add GET /api/cliproxy/update-check endpoint - Export checkCliproxyUpdate() function from binary-manager - Add useCliproxyUpdateCheck hook with 1-hour cache - Update ProxyStatusWidget with amber "Update" badge when available - Highlight Restart button as "Update" with amber styling when update pending
This commit is contained in:
@@ -995,6 +995,23 @@ export async function fetchLatestCliproxyVersion(): Promise<string> {
|
||||
return result.latestVersion;
|
||||
}
|
||||
|
||||
/** Update check result for API response */
|
||||
export interface CliproxyUpdateCheckResult {
|
||||
hasUpdate: boolean;
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
fromCache: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for CLIProxyAPI binary updates
|
||||
* @returns Update check result with version info
|
||||
*/
|
||||
export async function checkCliproxyUpdate(): Promise<CliproxyUpdateCheckResult> {
|
||||
const manager = new BinaryManager();
|
||||
return manager.checkForUpdates();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get path to version pin file
|
||||
* @returns Absolute path to .version-pin file
|
||||
|
||||
@@ -46,6 +46,7 @@ import type { CLIProxyProvider } from '../cliproxy/types';
|
||||
import { getClaudeEnvVars } from '../cliproxy/config-generator';
|
||||
import { getProxyStatus as getProxyProcessStatus, stopProxy } from '../cliproxy/session-tracker';
|
||||
import { ensureCliproxyService } from '../cliproxy/service-manager';
|
||||
import { checkCliproxyUpdate } from '../cliproxy/binary-manager';
|
||||
// Unified config imports
|
||||
import {
|
||||
hasUnifiedConfig,
|
||||
@@ -1402,6 +1403,19 @@ apiRoutes.post('/cliproxy/proxy-stop', (_req: Request, res: Response): void => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/cliproxy/update-check - Check for CLIProxyAPI binary updates
|
||||
* Returns: { hasUpdate, currentVersion, latestVersion, fromCache }
|
||||
*/
|
||||
apiRoutes.get('/cliproxy/update-check', async (_req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const result = await checkCliproxyUpdate();
|
||||
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 }
|
||||
|
||||
@@ -2,12 +2,18 @@
|
||||
* Proxy Status Widget
|
||||
*
|
||||
* Displays CLIProxy process status with start/stop/restart controls.
|
||||
* Shows: running state, port, session count, uptime.
|
||||
* Shows: running state, port, session count, uptime, update availability.
|
||||
*/
|
||||
|
||||
import { Activity, Power, RefreshCw, Clock, Users, Square, RotateCw } from 'lucide-react';
|
||||
import { Activity, Power, RefreshCw, Clock, Users, Square, RotateCw, ArrowUp } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useProxyStatus, useStartProxy, useStopProxy } from '@/hooks/use-cliproxy';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import {
|
||||
useProxyStatus,
|
||||
useStartProxy,
|
||||
useStopProxy,
|
||||
useCliproxyUpdateCheck,
|
||||
} from '@/hooks/use-cliproxy';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
function formatUptime(startedAt?: string): string {
|
||||
@@ -27,11 +33,13 @@ function formatUptime(startedAt?: string): string {
|
||||
|
||||
export function ProxyStatusWidget() {
|
||||
const { data: status, isLoading } = useProxyStatus();
|
||||
const { data: updateCheck } = useCliproxyUpdateCheck();
|
||||
const startProxy = useStartProxy();
|
||||
const stopProxy = useStopProxy();
|
||||
|
||||
const isRunning = status?.running ?? false;
|
||||
const isActioning = startProxy.isPending || stopProxy.isPending;
|
||||
const hasUpdate = updateCheck?.hasUpdate ?? false;
|
||||
|
||||
// Restart = stop then start
|
||||
const handleRestart = async () => {
|
||||
@@ -57,6 +65,16 @@ export function ProxyStatusWidget() {
|
||||
)}
|
||||
/>
|
||||
<span className="text-sm font-medium">CLIProxy Service</span>
|
||||
{hasUpdate && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-[10px] h-4 px-1.5 gap-0.5 bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400"
|
||||
title={`Update: v${updateCheck?.currentVersion} -> v${updateCheck?.latestVersion}`}
|
||||
>
|
||||
<ArrowUp className="w-2.5 h-2.5" />
|
||||
Update
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -90,19 +108,28 @@ export function ProxyStatusWidget() {
|
||||
{/* Control buttons when running */}
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
variant={hasUpdate ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className="h-7 text-xs gap-1 flex-1"
|
||||
className={cn(
|
||||
'h-7 text-xs gap-1 flex-1',
|
||||
hasUpdate && 'bg-amber-600 hover:bg-amber-700 text-white'
|
||||
)}
|
||||
onClick={handleRestart}
|
||||
disabled={isActioning}
|
||||
title="Restart to apply updates"
|
||||
title={
|
||||
hasUpdate
|
||||
? `Restart to update: v${updateCheck?.currentVersion} -> v${updateCheck?.latestVersion}`
|
||||
: 'Restart CLIProxy service'
|
||||
}
|
||||
>
|
||||
{isActioning ? (
|
||||
<RefreshCw className="w-3 h-3 animate-spin" />
|
||||
) : hasUpdate ? (
|
||||
<ArrowUp className="w-3 h-3" />
|
||||
) : (
|
||||
<RotateCw className="w-3 h-3" />
|
||||
)}
|
||||
Restart
|
||||
{hasUpdate ? 'Update' : 'Restart'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
|
||||
@@ -261,3 +261,15 @@ export function useStopProxy() {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Update Check ====================
|
||||
|
||||
export function useCliproxyUpdateCheck() {
|
||||
return useQuery({
|
||||
queryKey: ['cliproxy-update-check'],
|
||||
queryFn: () => api.cliproxy.updateCheck(),
|
||||
staleTime: 60 * 60 * 1000, // 1 hour (matches backend cache)
|
||||
refetchInterval: 60 * 60 * 1000, // Refresh every hour
|
||||
refetchOnWindowFocus: false, // Don't refresh on window focus (save API calls)
|
||||
});
|
||||
}
|
||||
|
||||
@@ -190,6 +190,14 @@ export interface ProxyStopResult {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/** Result from checking for CLIProxyAPI updates */
|
||||
export interface CliproxyUpdateCheckResult {
|
||||
hasUpdate: boolean;
|
||||
currentVersion: string;
|
||||
latestVersion: string;
|
||||
fromCache: boolean;
|
||||
}
|
||||
|
||||
// API
|
||||
export const api = {
|
||||
profiles: {
|
||||
@@ -225,6 +233,7 @@ export const api = {
|
||||
proxyStatus: () => request<ProxyProcessStatus>('/cliproxy/proxy-status'),
|
||||
proxyStart: () => request<ProxyStartResult>('/cliproxy/proxy-start', { method: 'POST' }),
|
||||
proxyStop: () => request<ProxyStopResult>('/cliproxy/proxy-stop', { method: 'POST' }),
|
||||
updateCheck: () => request<CliproxyUpdateCheckResult>('/cliproxy/update-check'),
|
||||
|
||||
// Stats and models for Overview tab
|
||||
stats: () => request<{ usage: Record<string, unknown> }>('/cliproxy/usage'),
|
||||
|
||||
Reference in New Issue
Block a user