feat(cliproxy): add dashboard UI parity for version stability

Add version stability warnings to Dashboard API and UI:
- Extend /api/cliproxy/update-check with isStable, maxStableVersion, stabilityMessage
- Health check shows warning status for v81+ installations
- UI header displays version badge with amber warning for unstable

Closes: relates to #269
This commit is contained in:
kaitranntt
2026-01-05 12:10:32 -05:00
parent 212aef81bc
commit c5621dab51
3 changed files with 75 additions and 3 deletions
+20 -2
View File
@@ -8,7 +8,7 @@
import { info, warn } from '../utils/ui';
import { getBinDir, CLIPROXY_DEFAULT_PORT } from './config-generator';
import { BinaryInfo, BinaryManagerConfig } from './types';
import { CLIPROXY_FALLBACK_VERSION } from './platform-detector';
import { CLIPROXY_FALLBACK_VERSION, CLIPROXY_MAX_STABLE_VERSION } from './platform-detector';
import { isProxyRunning, stopProxy } from './services/proxy-lifecycle-service';
import { waitForPortFree } from '../utils/port-utils';
import {
@@ -151,11 +151,29 @@ export interface CliproxyUpdateCheckResult {
latestVersion: string;
fromCache: boolean;
checkedAt: number;
// Stability fields
isStable: boolean;
maxStableVersion: string;
stabilityMessage?: string;
}
/** Check for CLIProxyAPI binary updates */
export async function checkCliproxyUpdate(): Promise<CliproxyUpdateCheckResult> {
return new BinaryManager().checkForUpdates();
const result = await new BinaryManager().checkForUpdates();
// Import isNewerVersion for stability check
const { isNewerVersion } = await import('./binary/version-checker');
const isStable = !isNewerVersion(result.currentVersion, CLIPROXY_MAX_STABLE_VERSION);
const stabilityMessage = isStable
? undefined
: `v${result.currentVersion} has known stability issues. Max stable: v${CLIPROXY_MAX_STABLE_VERSION}`;
return {
...result,
isStable,
maxStableVersion: CLIPROXY_MAX_STABLE_VERSION,
stabilityMessage,
};
}
// Re-export version pin functions
+17
View File
@@ -15,6 +15,8 @@ import {
} from '../../cliproxy';
import { getPortProcess, isCLIProxyProcess } from '../../utils/port-utils';
import type { HealthCheck } from './types';
import { CLIPROXY_MAX_STABLE_VERSION } from '../../cliproxy/platform-detector';
import { isNewerVersion } from '../../cliproxy/binary/version-checker';
/**
* Check CLIProxy binary installation
@@ -23,6 +25,21 @@ export function checkCliproxyBinary(): HealthCheck {
if (isCLIProxyInstalled()) {
const version = getInstalledCliproxyVersion();
const binaryPath = getCLIProxyPath();
// Check if version exceeds stable cap
const isUnstable = isNewerVersion(version, CLIPROXY_MAX_STABLE_VERSION);
if (isUnstable) {
return {
id: 'cliproxy-binary',
name: 'CLIProxy Binary',
status: 'warning',
message: `v${version} (unstable)`,
details: binaryPath,
fix: `Downgrade: ccs cliproxy install ${CLIPROXY_MAX_STABLE_VERSION.replace(/-\d+$/, '')}`,
};
}
return {
id: 'cliproxy-binary',
name: 'CLIProxy Binary',
+38 -1
View File
@@ -6,11 +6,17 @@
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { RefreshCw, Loader2 } from 'lucide-react';
import { RefreshCw, Loader2, AlertTriangle } from 'lucide-react';
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow';
import { cn } from '@/lib/utils';
interface VersionInfo {
currentVersion: string;
isStable: boolean;
stabilityMessage?: string;
}
interface LoginButtonProps {
provider: string;
displayName: string;
@@ -110,6 +116,22 @@ export function CliproxyHeader({
const { data: authData } = useCliproxyAuth();
const { provider: authProvider, isAuthenticating, startAuth } = useCliproxyAuthFlow();
const lastUpdatedText = useRelativeTime(lastUpdated);
const [versionInfo, setVersionInfo] = useState<VersionInfo | null>(null);
useEffect(() => {
fetch('/api/cliproxy/update-check')
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (data) {
setVersionInfo({
currentVersion: data.currentVersion,
isStable: data.isStable,
stabilityMessage: data.stabilityMessage,
});
}
})
.catch(() => {}); // Silently fail
}, []);
const providers = [
{ id: 'claude', displayName: 'Claude' },
@@ -170,6 +192,21 @@ export function CliproxyHeader({
{isRunning ? 'Running' : 'Offline'}
</Badge>
{versionInfo && (
<Badge
variant={versionInfo.isStable ? 'secondary' : 'destructive'}
className={cn(
'gap-1.5',
!versionInfo.isStable &&
'bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30'
)}
title={versionInfo.stabilityMessage}
>
{!versionInfo.isStable && <AlertTriangle className="w-3 h-3" />}v
{versionInfo.currentVersion}
</Badge>
)}
{lastUpdatedText && (
<span className="text-xs text-muted-foreground">{lastUpdatedText}</span>
)}