mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user