fix(cliproxy): degrade gracefully on GitHub release 403s

- fall back to stale CLIProxy version caches instead of surfacing 500s

- remove duplicate dashboard update-check fetches and disable retry/focus refetch

- add backend and UI regressions for the degraded-path behavior
This commit is contained in:
Tam Nhu Tran committed 2026-04-13 22:19:16 -04:00
1 parent 4874c154cb
commit 4e15ec5387
8 files changed
+418 -86

No files matched your search

+10 -33
View File
@@ -3,22 +3,15 @@
* Fixed header with OAuth login buttons, status indicator, and refresh
*/
import { useState, useEffect } from 'react';
import { useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { RefreshCw, Loader2, AlertTriangle } from 'lucide-react';
import { useCliproxyAuth } from '@/hooks/use-cliproxy';
import { useCliproxyAuth, useCliproxyUpdateCheck } from '@/hooks/use-cliproxy';
import { useCliproxyAuthFlow } from '@/hooks/use-cliproxy-auth-flow';
import { cn } from '@/lib/utils';
import { CLIPROXY_PROVIDERS, getProviderDisplayName } from '@/lib/provider-config';
interface VersionInfo {
currentVersion: string;
isStable: boolean;
stabilityMessage?: string;
backendLabel?: string;
}
interface LoginButtonProps {
provider: string;
displayName: string;
@@ -116,25 +109,9 @@ export function CliproxyHeader({
isRunning = true,
}: CliproxyHeaderProps) {
const { data: authData } = useCliproxyAuth();
const { data: updateCheck } = useCliproxyUpdateCheck();
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,
backendLabel: data.backendLabel,
});
}
})
.catch(() => {}); // Silently fail
}, []);
const providers = CLIPROXY_PROVIDERS.map((id) => ({
id,
@@ -155,7 +132,7 @@ export function CliproxyHeader({
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold tracking-tight">
{versionInfo?.backendLabel ?? 'CLIProxy'}
{updateCheck?.backendLabel ?? 'CLIProxy'}
</h1>
<p className="text-sm text-muted-foreground mt-1">CCS-level account management</p>
</div>
@@ -191,18 +168,18 @@ export function CliproxyHeader({
{isRunning ? 'Running' : 'Offline'}
</Badge>
{versionInfo && (
{updateCheck && (
<Badge
variant={versionInfo.isStable ? 'secondary' : 'destructive'}
variant={updateCheck.isStable ? 'secondary' : 'destructive'}
className={cn(
'gap-1.5',
!versionInfo.isStable &&
!updateCheck.isStable &&
'bg-amber-500/20 text-amber-600 dark:text-amber-400 border-amber-500/30'
)}
title={versionInfo.stabilityMessage}
title={updateCheck.stabilityMessage}
>
{!versionInfo.isStable && <AlertTriangle className="w-3 h-3" />}v
{versionInfo.currentVersion}
{!updateCheck.isStable && <AlertTriangle className="w-3 h-3" />}v
{updateCheck.currentVersion}
</Badge>
)}
+4 -2
View File
@@ -447,7 +447,8 @@ export function useCliproxyUpdateCheck() {
queryFn: () => api.cliproxy.updateCheck(),
staleTime: 5 * 60 * 1000, // 5 minutes (reduced from 1 hour for faster backend switch response)
refetchInterval: 5 * 60 * 1000, // Refresh every 5 minutes
refetchOnWindowFocus: true, // Refetch on window focus to catch backend changes
refetchOnWindowFocus: false, // Avoid refetch bursts for non-critical release metadata
retry: false,
});
}
@@ -492,7 +493,8 @@ export function useCliproxyVersions() {
queryKey: ['cliproxy-versions'],
queryFn: () => api.cliproxy.versions(),
staleTime: 5 * 60 * 1000, // 5 minutes (reduced for faster backend switch response)
refetchOnWindowFocus: true, // Refetch on focus to catch backend changes
refetchOnWindowFocus: false, // Avoid repeated release lookups while browsing the dashboard
retry: false,
});
}
@@ -0,0 +1,56 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { render, screen } from '../../../setup/test-utils';
const hookMocks = vi.hoisted(() => ({
startAuth: vi.fn(),
}));
vi.mock('@/hooks/use-cliproxy', () => ({
useCliproxyAuth: () => ({
data: {
authStatus: [],
},
}),
useCliproxyUpdateCheck: () => ({
data: {
backendLabel: 'CLIProxy Plus',
currentVersion: '6.9.23-0',
isStable: true,
stabilityMessage: undefined,
},
}),
}));
vi.mock('@/hooks/use-cliproxy-auth-flow', () => ({
useCliproxyAuthFlow: () => ({
provider: null,
isAuthenticating: false,
startAuth: hookMocks.startAuth,
}),
}));
import { CliproxyHeader } from '@/components/cliproxy/cliproxy-header';
describe('CliproxyHeader', () => {
const fetchMock = vi.fn<typeof fetch>();
beforeEach(() => {
vi.clearAllMocks();
fetchMock.mockReset();
vi.stubGlobal('fetch', fetchMock);
});
afterEach(() => {
vi.unstubAllGlobals();
});
it('renders version data from the shared update query without issuing a direct fetch', () => {
render(
<CliproxyHeader onRefresh={vi.fn()} isRefreshing={false} lastUpdated={new Date()} isRunning />
);
expect(screen.getByText('CLIProxy Plus')).toBeInTheDocument();
expect(screen.getByText('v6.9.23-0')).toBeInTheDocument();
expect(fetchMock).not.toHaveBeenCalled();
});
});