fix(ui): sync backend state across all CLIProxy UI components

- Add useUpdateBackend mutation hook that invalidates related queries
- Reduce update-check stale time from 1hr to 5min + enable refetchOnWindowFocus
- Settings proxy page now uses mutation hook for proper query invalidation
- Make all CLIProxy labels dynamic based on backendLabel from API:
  - cliproxy-header.tsx: page title
  - app-sidebar.tsx: nav item label
  - cliproxy-stats-overview.tsx: both offline and running descriptions
  - model-preferences-grid.tsx: card description
  - settings proxy section: descriptions for local/remote modes
- Fix API route to use DEFAULT_BACKEND constant instead of hardcoded 'plus'

Ensures backend switching in Settings immediately reflects in all CLIProxy pages.
This commit is contained in:
kaitranntt
2026-01-23 13:46:47 -05:00
parent dad4349994
commit 88560c7119
7 changed files with 93 additions and 40 deletions
+3 -2
View File
@@ -11,6 +11,7 @@ import { Router, Request, Response } from 'express';
import { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader';
import { testConnection } from '../../cliproxy/remote-proxy-client';
import { isProxyRunning } from '../../cliproxy/services/proxy-lifecycle-service';
import { DEFAULT_BACKEND } from '../../cliproxy/platform-detector';
import {
DEFAULT_CLIPROXY_SERVER_CONFIG,
CliproxyServerConfig,
@@ -73,7 +74,7 @@ router.put('/', async (req: Request, res: Response) => {
router.get('/backend', async (_req: Request, res: Response) => {
try {
const config = await loadOrCreateUnifiedConfig();
res.json({ backend: config.cliproxy?.backend ?? 'plus' });
res.json({ backend: config.cliproxy?.backend ?? DEFAULT_BACKEND });
} catch (error) {
console.error('[cliproxy-server-routes] Failed to load backend config:', error);
res.status(500).json({ error: 'Failed to load backend config' });
@@ -99,7 +100,7 @@ router.put('/backend', async (req: Request, res: Response) => {
// Check if proxy is running - warn about restart requirement
const config = await loadOrCreateUnifiedConfig();
const currentBackend = config.cliproxy?.backend ?? 'plus';
const currentBackend = config.cliproxy?.backend ?? DEFAULT_BACKEND;
if (currentBackend !== backend && isProxyRunning() && !force) {
res.status(409).json({
error: 'Proxy is running. Stop proxy first or use force=true to change backend.',
@@ -15,6 +15,7 @@ interface VersionInfo {
currentVersion: string;
isStable: boolean;
stabilityMessage?: string;
backendLabel?: string;
}
interface LoginButtonProps {
@@ -127,6 +128,7 @@ export function CliproxyHeader({
currentVersion: data.currentVersion,
isStable: data.isStable,
stabilityMessage: data.stabilityMessage,
backendLabel: data.backendLabel,
});
}
})
@@ -157,7 +159,9 @@ export function CliproxyHeader({
{/* Top row: Title and Login Buttons */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold tracking-tight">CLIProxy Plus</h1>
<h1 className="text-2xl font-bold tracking-tight">
{versionInfo?.backendLabel ?? 'CLIProxy'}
</h1>
<p className="text-sm text-muted-foreground mt-1">CCS-level account management</p>
</div>
@@ -27,6 +27,7 @@ import {
} from 'lucide-react';
import { cn } from '@/lib/utils';
import { useCliproxyStats, useCliproxyStatus } from '@/hooks/use-cliproxy-stats';
import { useCliproxyUpdateCheck } from '@/hooks/use-cliproxy';
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
interface CliproxyStatsOverviewProps {
@@ -37,6 +38,8 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
const { privacyMode } = usePrivacy();
const { data: status, isLoading: statusLoading } = useCliproxyStatus();
const { data: stats, isLoading: statsLoading, error } = useCliproxyStats(status?.running);
const { data: updateCheck } = useCliproxyUpdateCheck();
const backendLabel = updateCheck?.backendLabel ?? 'CLIProxy';
const isLoading = statusLoading || (status?.running && statsLoading);
@@ -71,7 +74,7 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
Session Statistics
</h2>
<p className="text-sm text-muted-foreground">
Real-time usage metrics from CLIProxy Plus
Real-time usage metrics from {backendLabel}
</p>
</div>
<Badge variant="secondary" className="w-fit gap-1.5">
@@ -146,7 +149,9 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
<Activity className="h-5 w-5" />
Session Statistics
</h2>
<p className="text-sm text-muted-foreground">Real-time usage metrics from CLIProxyAPI</p>
<p className="text-sm text-muted-foreground">
Real-time usage metrics from {backendLabel}
</p>
</div>
<Badge
variant="outline"
@@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
import { Badge } from '@/components/ui/badge';
import { Skeleton } from '@/components/ui/skeleton';
import { Cpu, AlertCircle } from 'lucide-react';
import { useCliproxyModels } from '@/hooks/use-cliproxy';
import { useCliproxyModels, useCliproxyUpdateCheck } from '@/hooks/use-cliproxy';
import { cn } from '@/lib/utils';
/** Category display configuration */
@@ -70,6 +70,8 @@ function EmptyModelsState() {
export function ModelPreferencesGrid() {
const { data: modelsData, isLoading, isError } = useCliproxyModels();
const { data: updateCheck } = useCliproxyUpdateCheck();
const backendLabel = updateCheck?.backendLabel ?? 'CLIProxy';
// Sort categories by model count
const sortedCategories = useMemo(() => {
@@ -102,7 +104,7 @@ export function ModelPreferencesGrid() {
</Badge>
</CardTitle>
<CardDescription>
Models available through CLIProxy Plus, grouped by provider
Models available through {backendLabel}, grouped by provider
</CardDescription>
</CardHeader>
<CardContent>
+17 -4
View File
@@ -30,6 +30,7 @@ import {
} from '@/components/ui/sidebar';
import { CcsLogo } from '@/components/shared/ccs-logo';
import { useSidebar } from '@/hooks/use-sidebar';
import { useCliproxyUpdateCheck } from '@/hooks/use-cliproxy';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
@@ -87,6 +88,18 @@ export function AppSidebar() {
const location = useLocation();
const navigate = useNavigate();
const { state } = useSidebar();
const { data: updateCheck } = useCliproxyUpdateCheck();
// Dynamic label for CLIProxy based on backend
const cliproxyLabel = updateCheck?.backendLabel ?? 'CLIProxy';
// Helper to get dynamic label (for CLIProxy route)
const getItemLabel = (item: { path: string; label: string }) => {
if (item.path === '/cliproxy') {
return cliproxyLabel;
}
return item.label;
};
// Helper to check if a route is active (exact match)
const isRouteActive = (path: string) => location.pathname === path;
@@ -122,13 +135,13 @@ export function AppSidebar() {
{/* Click navigates to overview AND opens submenu */}
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.label}
tooltip={getItemLabel(item)}
isActive={isParentActive(item.children)}
onClick={() => navigate(item.path)}
>
{item.icon && <item.icon className="w-4 h-4" />}
<span className="group-data-[collapsible=icon]:hidden">
{item.label}
{getItemLabel(item)}
</span>
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 group-data-[collapsible=icon]:hidden" />
</SidebarMenuButton>
@@ -155,12 +168,12 @@ export function AppSidebar() {
<SidebarMenuButton
asChild
isActive={isRouteActive(item.path)}
tooltip={item.label}
tooltip={getItemLabel(item)}
>
<Link to={item.path}>
{item.icon && <item.icon className="w-4 h-4" />}
<span className="group-data-[collapsible=icon]:hidden flex-1">
{item.label}
{getItemLabel(item)}
</span>
{item.badge && (
<Tooltip>
+34 -3
View File
@@ -333,9 +333,40 @@ 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)
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
});
}
// ==================== Backend Management ====================
/**
* Hook for switching CLIProxy backend (original vs plus)
* Invalidates all backend-dependent queries to ensure UI consistency
*/
export function useUpdateBackend() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ backend, force = false }: { backend: 'original' | 'plus'; force?: boolean }) =>
api.cliproxyServer.updateBackend(backend, force),
onSuccess: () => {
// Invalidate all queries that depend on backend setting
queryClient.invalidateQueries({ queryKey: ['cliproxy-update-check'] });
queryClient.invalidateQueries({ queryKey: ['cliproxy-versions'] });
queryClient.invalidateQueries({ queryKey: ['proxy-status'] });
queryClient.invalidateQueries({ queryKey: ['cliproxy-stats'] });
toast.success('Backend updated');
},
onError: (error: Error) => {
// Handle 409 conflict (proxy running)
if (error.message.includes('Proxy is running')) {
toast.error('Stop the proxy first to change backend');
} else {
toast.error(error.message);
}
},
});
}
+23 -26
View File
@@ -18,8 +18,8 @@ import {
Box,
AlertTriangle,
} from 'lucide-react';
import { toast } from 'sonner';
import { useProxyConfig, useRawConfig } from '../../hooks';
import { useUpdateBackend } from '@/hooks/use-cliproxy';
import { LocalProxyCard } from './local-proxy-card';
import { RemoteProxyCard } from './remote-proxy-card';
import { api } from '@/lib/api-client';
@@ -74,10 +74,10 @@ export default function ProxySection() {
}
};
// Backend state (loaded from API)
// Backend state (loaded from API) + mutation hook for proper query invalidation
const [backend, setBackend] = useState<'original' | 'plus'>('plus');
const [backendSaving, setBackendSaving] = useState(false);
const [hasKiroGhcpVariants, setHasKiroGhcpVariants] = useState(false);
const updateBackendMutation = useUpdateBackend();
// Fetch backend setting
const fetchBackend = useCallback(async () => {
@@ -100,24 +100,18 @@ export default function ProxySection() {
}
}, []);
// Save backend setting
const handleBackendChange = async (value: 'original' | 'plus') => {
// Save backend setting using mutation hook (invalidates all related queries)
const handleBackendChange = (value: 'original' | 'plus') => {
const previousValue = backend;
setBackend(value);
setBackendSaving(true);
try {
await api.cliproxyServer.updateBackend(value);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to save backend';
// Check if error is due to proxy running (409 conflict)
if (errorMessage.includes('Proxy is running')) {
toast.error('Stop the proxy first to change backend');
setBackend(value); // Optimistic update
updateBackendMutation.mutate(
{ backend: value },
{
onError: () => {
setBackend(previousValue); // Rollback on error
},
}
console.error('[Proxy] Failed to save backend:', err);
setBackend(previousValue);
} finally {
setBackendSaving(false);
}
);
};
// Log when debug mode changes (sanitize sensitive fields)
@@ -140,8 +134,10 @@ export default function ProxySection() {
useEffect(() => {
fetchConfig();
fetchRawConfig();
fetchBackend();
checkPlusOnlyVariants();
// eslint-disable-next-line react-hooks/set-state-in-effect -- Async data fetching on mount is intended
void fetchBackend();
void checkPlusOnlyVariants();
}, [fetchConfig, fetchRawConfig, fetchBackend, checkPlusOnlyVariants]);
if (loading || !config) {
@@ -253,7 +249,8 @@ export default function ProxySection() {
<ScrollArea className="flex-1">
<div className="p-5 space-y-6">
<p className="text-sm text-muted-foreground">
Configure local or remote CLIProxy Plus connection for proxy-based profiles
Configure local or remote {backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'} connection
for proxy-based profiles
</p>
{/* Mode Toggle - Card based selection */}
@@ -277,7 +274,7 @@ export default function ProxySection() {
<span className="font-medium">Local</span>
</div>
<p className="text-xs text-muted-foreground">
Run CLIProxy Plus binary on this machine
Run {backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'} binary on this machine
</p>
</button>
@@ -298,7 +295,7 @@ export default function ProxySection() {
<span className="font-medium">Remote</span>
</div>
<p className="text-xs text-muted-foreground">
Connect to a remote CLIProxy Plus server
Connect to a remote {backend === 'plus' ? 'CLIProxy Plus' : 'CLIProxy'} server
</p>
</button>
</div>
@@ -314,7 +311,7 @@ export default function ProxySection() {
{/* Plus Backend Card */}
<button
onClick={() => handleBackendChange('plus')}
disabled={backendSaving}
disabled={updateBackendMutation.isPending}
className={`p-4 rounded-lg border-2 text-left transition-all ${
backend === 'plus'
? 'border-primary bg-primary/5'
@@ -335,7 +332,7 @@ export default function ProxySection() {
{/* Original Backend Card */}
<button
onClick={() => handleBackendChange('original')}
disabled={backendSaving}
disabled={updateBackendMutation.isPending}
className={`p-4 rounded-lg border-2 text-left transition-all ${
backend === 'original'
? 'border-primary bg-primary/5'