mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-13 08:15:17 +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:
1 parent
c9ad0b0779
commit
96762a9f6e
5 files changed
+86
-7
No files matched your search
@@ -995,6 +995,23 @@ export async function fetchLatestCliproxyVersion(): Promise<string> {
|
|||||||
return result.latestVersion;
|
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
|
* Get path to version pin file
|
||||||
* @returns Absolute 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 { getClaudeEnvVars } from '../cliproxy/config-generator';
|
||||||
import { getProxyStatus as getProxyProcessStatus, stopProxy } 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';
|
||||||
|
import { checkCliproxyUpdate } from '../cliproxy/binary-manager';
|
||||||
// Unified config imports
|
// Unified config imports
|
||||||
import {
|
import {
|
||||||
hasUnifiedConfig,
|
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
|
* 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 }
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
* Proxy Status Widget
|
* Proxy Status Widget
|
||||||
*
|
*
|
||||||
* Displays CLIProxy process status with start/stop/restart controls.
|
* 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 { 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';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
function formatUptime(startedAt?: string): string {
|
function formatUptime(startedAt?: string): string {
|
||||||
@@ -27,11 +33,13 @@ function formatUptime(startedAt?: string): string {
|
|||||||
|
|
||||||
export function ProxyStatusWidget() {
|
export function ProxyStatusWidget() {
|
||||||
const { data: status, isLoading } = useProxyStatus();
|
const { data: status, isLoading } = useProxyStatus();
|
||||||
|
const { data: updateCheck } = useCliproxyUpdateCheck();
|
||||||
const startProxy = useStartProxy();
|
const startProxy = useStartProxy();
|
||||||
const stopProxy = useStopProxy();
|
const stopProxy = useStopProxy();
|
||||||
|
|
||||||
const isRunning = status?.running ?? false;
|
const isRunning = status?.running ?? false;
|
||||||
const isActioning = startProxy.isPending || stopProxy.isPending;
|
const isActioning = startProxy.isPending || stopProxy.isPending;
|
||||||
|
const hasUpdate = updateCheck?.hasUpdate ?? false;
|
||||||
|
|
||||||
// Restart = stop then start
|
// Restart = stop then start
|
||||||
const handleRestart = async () => {
|
const handleRestart = async () => {
|
||||||
@@ -57,6 +65,16 @@ export function ProxyStatusWidget() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm font-medium">CLIProxy Service</span>
|
<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>
|
||||||
|
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -90,19 +108,28 @@ export function ProxyStatusWidget() {
|
|||||||
{/* Control buttons when running */}
|
{/* Control buttons when running */}
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant={hasUpdate ? 'default' : 'outline'}
|
||||||
size="sm"
|
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}
|
onClick={handleRestart}
|
||||||
disabled={isActioning}
|
disabled={isActioning}
|
||||||
title="Restart to apply updates"
|
title={
|
||||||
|
hasUpdate
|
||||||
|
? `Restart to update: v${updateCheck?.currentVersion} -> v${updateCheck?.latestVersion}`
|
||||||
|
: 'Restart CLIProxy service'
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isActioning ? (
|
{isActioning ? (
|
||||||
<RefreshCw className="w-3 h-3 animate-spin" />
|
<RefreshCw className="w-3 h-3 animate-spin" />
|
||||||
|
) : hasUpdate ? (
|
||||||
|
<ArrowUp className="w-3 h-3" />
|
||||||
) : (
|
) : (
|
||||||
<RotateCw className="w-3 h-3" />
|
<RotateCw className="w-3 h-3" />
|
||||||
)}
|
)}
|
||||||
Restart
|
{hasUpdate ? 'Update' : 'Restart'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
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;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Result from checking for CLIProxyAPI updates */
|
||||||
|
export interface CliproxyUpdateCheckResult {
|
||||||
|
hasUpdate: boolean;
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion: string;
|
||||||
|
fromCache: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
// API
|
// API
|
||||||
export const api = {
|
export const api = {
|
||||||
profiles: {
|
profiles: {
|
||||||
@@ -225,6 +233,7 @@ export const api = {
|
|||||||
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' }),
|
proxyStop: () => request<ProxyStopResult>('/cliproxy/proxy-stop', { method: 'POST' }),
|
||||||
|
updateCheck: () => request<CliproxyUpdateCheckResult>('/cliproxy/update-check'),
|
||||||
|
|
||||||
// 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