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:
kaitranntt
2025-12-18 23:17:05 -05:00
parent c9ad0b0779
commit 96762a9f6e
5 changed files with 86 additions and 7 deletions
+17
View File
@@ -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
+14
View 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 }
+34 -7
View File
@@ -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"
+12
View File
@@ -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)
});
}
+9
View File
@@ -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'),