mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 22:17:14 +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 { loadOrCreateUnifiedConfig, saveUnifiedConfig } from '../../config/unified-config-loader';
|
||||||
import { testConnection } from '../../cliproxy/remote-proxy-client';
|
import { testConnection } from '../../cliproxy/remote-proxy-client';
|
||||||
import { isProxyRunning } from '../../cliproxy/services/proxy-lifecycle-service';
|
import { isProxyRunning } from '../../cliproxy/services/proxy-lifecycle-service';
|
||||||
|
import { DEFAULT_BACKEND } from '../../cliproxy/platform-detector';
|
||||||
import {
|
import {
|
||||||
DEFAULT_CLIPROXY_SERVER_CONFIG,
|
DEFAULT_CLIPROXY_SERVER_CONFIG,
|
||||||
CliproxyServerConfig,
|
CliproxyServerConfig,
|
||||||
@@ -73,7 +74,7 @@ router.put('/', async (req: Request, res: Response) => {
|
|||||||
router.get('/backend', async (_req: Request, res: Response) => {
|
router.get('/backend', async (_req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const config = await loadOrCreateUnifiedConfig();
|
const config = await loadOrCreateUnifiedConfig();
|
||||||
res.json({ backend: config.cliproxy?.backend ?? 'plus' });
|
res.json({ backend: config.cliproxy?.backend ?? DEFAULT_BACKEND });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[cliproxy-server-routes] Failed to load backend config:', error);
|
console.error('[cliproxy-server-routes] Failed to load backend config:', error);
|
||||||
res.status(500).json({ error: 'Failed to load backend config' });
|
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
|
// Check if proxy is running - warn about restart requirement
|
||||||
const config = await loadOrCreateUnifiedConfig();
|
const config = await loadOrCreateUnifiedConfig();
|
||||||
const currentBackend = config.cliproxy?.backend ?? 'plus';
|
const currentBackend = config.cliproxy?.backend ?? DEFAULT_BACKEND;
|
||||||
if (currentBackend !== backend && isProxyRunning() && !force) {
|
if (currentBackend !== backend && isProxyRunning() && !force) {
|
||||||
res.status(409).json({
|
res.status(409).json({
|
||||||
error: 'Proxy is running. Stop proxy first or use force=true to change backend.',
|
error: 'Proxy is running. Stop proxy first or use force=true to change backend.',
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface VersionInfo {
|
|||||||
currentVersion: string;
|
currentVersion: string;
|
||||||
isStable: boolean;
|
isStable: boolean;
|
||||||
stabilityMessage?: string;
|
stabilityMessage?: string;
|
||||||
|
backendLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoginButtonProps {
|
interface LoginButtonProps {
|
||||||
@@ -127,6 +128,7 @@ export function CliproxyHeader({
|
|||||||
currentVersion: data.currentVersion,
|
currentVersion: data.currentVersion,
|
||||||
isStable: data.isStable,
|
isStable: data.isStable,
|
||||||
stabilityMessage: data.stabilityMessage,
|
stabilityMessage: data.stabilityMessage,
|
||||||
|
backendLabel: data.backendLabel,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -157,7 +159,9 @@ export function CliproxyHeader({
|
|||||||
{/* Top row: Title and Login Buttons */}
|
{/* Top row: Title and Login Buttons */}
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||||
<div>
|
<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>
|
<p className="text-sm text-muted-foreground mt-1">CCS-level account management</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useCliproxyStats, useCliproxyStatus } from '@/hooks/use-cliproxy-stats';
|
import { useCliproxyStats, useCliproxyStatus } from '@/hooks/use-cliproxy-stats';
|
||||||
|
import { useCliproxyUpdateCheck } from '@/hooks/use-cliproxy';
|
||||||
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context';
|
||||||
|
|
||||||
interface CliproxyStatsOverviewProps {
|
interface CliproxyStatsOverviewProps {
|
||||||
@@ -37,6 +38,8 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
|
|||||||
const { privacyMode } = usePrivacy();
|
const { privacyMode } = usePrivacy();
|
||||||
const { data: status, isLoading: statusLoading } = useCliproxyStatus();
|
const { data: status, isLoading: statusLoading } = useCliproxyStatus();
|
||||||
const { data: stats, isLoading: statsLoading, error } = useCliproxyStats(status?.running);
|
const { data: stats, isLoading: statsLoading, error } = useCliproxyStats(status?.running);
|
||||||
|
const { data: updateCheck } = useCliproxyUpdateCheck();
|
||||||
|
const backendLabel = updateCheck?.backendLabel ?? 'CLIProxy';
|
||||||
|
|
||||||
const isLoading = statusLoading || (status?.running && statsLoading);
|
const isLoading = statusLoading || (status?.running && statsLoading);
|
||||||
|
|
||||||
@@ -71,7 +74,7 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
|
|||||||
Session Statistics
|
Session Statistics
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Real-time usage metrics from CLIProxy Plus
|
Real-time usage metrics from {backendLabel}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="secondary" className="w-fit gap-1.5">
|
<Badge variant="secondary" className="w-fit gap-1.5">
|
||||||
@@ -146,7 +149,9 @@ export function CliproxyStatsOverview({ className }: CliproxyStatsOverviewProps)
|
|||||||
<Activity className="h-5 w-5" />
|
<Activity className="h-5 w-5" />
|
||||||
Session Statistics
|
Session Statistics
|
||||||
</h2>
|
</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>
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/com
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Skeleton } from '@/components/ui/skeleton';
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
import { Cpu, AlertCircle } from 'lucide-react';
|
import { Cpu, AlertCircle } from 'lucide-react';
|
||||||
import { useCliproxyModels } from '@/hooks/use-cliproxy';
|
import { useCliproxyModels, useCliproxyUpdateCheck } from '@/hooks/use-cliproxy';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
/** Category display configuration */
|
/** Category display configuration */
|
||||||
@@ -70,6 +70,8 @@ function EmptyModelsState() {
|
|||||||
|
|
||||||
export function ModelPreferencesGrid() {
|
export function ModelPreferencesGrid() {
|
||||||
const { data: modelsData, isLoading, isError } = useCliproxyModels();
|
const { data: modelsData, isLoading, isError } = useCliproxyModels();
|
||||||
|
const { data: updateCheck } = useCliproxyUpdateCheck();
|
||||||
|
const backendLabel = updateCheck?.backendLabel ?? 'CLIProxy';
|
||||||
|
|
||||||
// Sort categories by model count
|
// Sort categories by model count
|
||||||
const sortedCategories = useMemo(() => {
|
const sortedCategories = useMemo(() => {
|
||||||
@@ -102,7 +104,7 @@ export function ModelPreferencesGrid() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Models available through CLIProxy Plus, grouped by provider
|
Models available through {backendLabel}, grouped by provider
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
} from '@/components/ui/sidebar';
|
} from '@/components/ui/sidebar';
|
||||||
import { CcsLogo } from '@/components/shared/ccs-logo';
|
import { CcsLogo } from '@/components/shared/ccs-logo';
|
||||||
import { useSidebar } from '@/hooks/use-sidebar';
|
import { useSidebar } from '@/hooks/use-sidebar';
|
||||||
|
import { useCliproxyUpdateCheck } from '@/hooks/use-cliproxy';
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||||
|
|
||||||
@@ -87,6 +88,18 @@ export function AppSidebar() {
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { state } = useSidebar();
|
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)
|
// Helper to check if a route is active (exact match)
|
||||||
const isRouteActive = (path: string) => location.pathname === path;
|
const isRouteActive = (path: string) => location.pathname === path;
|
||||||
@@ -122,13 +135,13 @@ export function AppSidebar() {
|
|||||||
{/* Click navigates to overview AND opens submenu */}
|
{/* Click navigates to overview AND opens submenu */}
|
||||||
<CollapsibleTrigger asChild>
|
<CollapsibleTrigger asChild>
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
tooltip={item.label}
|
tooltip={getItemLabel(item)}
|
||||||
isActive={isParentActive(item.children)}
|
isActive={isParentActive(item.children)}
|
||||||
onClick={() => navigate(item.path)}
|
onClick={() => navigate(item.path)}
|
||||||
>
|
>
|
||||||
{item.icon && <item.icon className="w-4 h-4" />}
|
{item.icon && <item.icon className="w-4 h-4" />}
|
||||||
<span className="group-data-[collapsible=icon]:hidden">
|
<span className="group-data-[collapsible=icon]:hidden">
|
||||||
{item.label}
|
{getItemLabel(item)}
|
||||||
</span>
|
</span>
|
||||||
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 group-data-[collapsible=icon]:hidden" />
|
<ChevronRight className="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 group-data-[collapsible=icon]:hidden" />
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
@@ -155,12 +168,12 @@ export function AppSidebar() {
|
|||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
asChild
|
asChild
|
||||||
isActive={isRouteActive(item.path)}
|
isActive={isRouteActive(item.path)}
|
||||||
tooltip={item.label}
|
tooltip={getItemLabel(item)}
|
||||||
>
|
>
|
||||||
<Link to={item.path}>
|
<Link to={item.path}>
|
||||||
{item.icon && <item.icon className="w-4 h-4" />}
|
{item.icon && <item.icon className="w-4 h-4" />}
|
||||||
<span className="group-data-[collapsible=icon]:hidden flex-1">
|
<span className="group-data-[collapsible=icon]:hidden flex-1">
|
||||||
{item.label}
|
{getItemLabel(item)}
|
||||||
</span>
|
</span>
|
||||||
{item.badge && (
|
{item.badge && (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|||||||
@@ -333,9 +333,40 @@ export function useCliproxyUpdateCheck() {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['cliproxy-update-check'],
|
queryKey: ['cliproxy-update-check'],
|
||||||
queryFn: () => api.cliproxy.updateCheck(),
|
queryFn: () => api.cliproxy.updateCheck(),
|
||||||
staleTime: 60 * 60 * 1000, // 1 hour (matches backend cache)
|
staleTime: 5 * 60 * 1000, // 5 minutes (reduced from 1 hour for faster backend switch response)
|
||||||
refetchInterval: 60 * 60 * 1000, // Refresh every hour
|
refetchInterval: 5 * 60 * 1000, // Refresh every 5 minutes
|
||||||
refetchOnWindowFocus: false, // Don't refresh on window focus (save API calls)
|
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,
|
Box,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { useProxyConfig, useRawConfig } from '../../hooks';
|
import { useProxyConfig, useRawConfig } from '../../hooks';
|
||||||
|
import { useUpdateBackend } from '@/hooks/use-cliproxy';
|
||||||
import { LocalProxyCard } from './local-proxy-card';
|
import { LocalProxyCard } from './local-proxy-card';
|
||||||
import { RemoteProxyCard } from './remote-proxy-card';
|
import { RemoteProxyCard } from './remote-proxy-card';
|
||||||
import { api } from '@/lib/api-client';
|
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 [backend, setBackend] = useState<'original' | 'plus'>('plus');
|
||||||
const [backendSaving, setBackendSaving] = useState(false);
|
|
||||||
const [hasKiroGhcpVariants, setHasKiroGhcpVariants] = useState(false);
|
const [hasKiroGhcpVariants, setHasKiroGhcpVariants] = useState(false);
|
||||||
|
const updateBackendMutation = useUpdateBackend();
|
||||||
|
|
||||||
// Fetch backend setting
|
// Fetch backend setting
|
||||||
const fetchBackend = useCallback(async () => {
|
const fetchBackend = useCallback(async () => {
|
||||||
@@ -100,24 +100,18 @@ export default function ProxySection() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Save backend setting
|
// Save backend setting using mutation hook (invalidates all related queries)
|
||||||
const handleBackendChange = async (value: 'original' | 'plus') => {
|
const handleBackendChange = (value: 'original' | 'plus') => {
|
||||||
const previousValue = backend;
|
const previousValue = backend;
|
||||||
setBackend(value);
|
setBackend(value); // Optimistic update
|
||||||
setBackendSaving(true);
|
updateBackendMutation.mutate(
|
||||||
try {
|
{ backend: value },
|
||||||
await api.cliproxyServer.updateBackend(value);
|
{
|
||||||
} catch (err) {
|
onError: () => {
|
||||||
const errorMessage = err instanceof Error ? err.message : 'Failed to save backend';
|
setBackend(previousValue); // Rollback on error
|
||||||
// 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');
|
|
||||||
}
|
}
|
||||||
console.error('[Proxy] Failed to save backend:', err);
|
);
|
||||||
setBackend(previousValue);
|
|
||||||
} finally {
|
|
||||||
setBackendSaving(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Log when debug mode changes (sanitize sensitive fields)
|
// Log when debug mode changes (sanitize sensitive fields)
|
||||||
@@ -140,8 +134,10 @@ export default function ProxySection() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
fetchRawConfig();
|
fetchRawConfig();
|
||||||
fetchBackend();
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- Async data fetching on mount is intended
|
||||||
checkPlusOnlyVariants();
|
void fetchBackend();
|
||||||
|
|
||||||
|
void checkPlusOnlyVariants();
|
||||||
}, [fetchConfig, fetchRawConfig, fetchBackend, checkPlusOnlyVariants]);
|
}, [fetchConfig, fetchRawConfig, fetchBackend, checkPlusOnlyVariants]);
|
||||||
|
|
||||||
if (loading || !config) {
|
if (loading || !config) {
|
||||||
@@ -253,7 +249,8 @@ export default function ProxySection() {
|
|||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1">
|
||||||
<div className="p-5 space-y-6">
|
<div className="p-5 space-y-6">
|
||||||
<p className="text-sm text-muted-foreground">
|
<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>
|
</p>
|
||||||
|
|
||||||
{/* Mode Toggle - Card based selection */}
|
{/* Mode Toggle - Card based selection */}
|
||||||
@@ -277,7 +274,7 @@ export default function ProxySection() {
|
|||||||
<span className="font-medium">Local</span>
|
<span className="font-medium">Local</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<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>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -298,7 +295,7 @@ export default function ProxySection() {
|
|||||||
<span className="font-medium">Remote</span>
|
<span className="font-medium">Remote</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-muted-foreground">
|
<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>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -314,7 +311,7 @@ export default function ProxySection() {
|
|||||||
{/* Plus Backend Card */}
|
{/* Plus Backend Card */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleBackendChange('plus')}
|
onClick={() => handleBackendChange('plus')}
|
||||||
disabled={backendSaving}
|
disabled={updateBackendMutation.isPending}
|
||||||
className={`p-4 rounded-lg border-2 text-left transition-all ${
|
className={`p-4 rounded-lg border-2 text-left transition-all ${
|
||||||
backend === 'plus'
|
backend === 'plus'
|
||||||
? 'border-primary bg-primary/5'
|
? 'border-primary bg-primary/5'
|
||||||
@@ -335,7 +332,7 @@ export default function ProxySection() {
|
|||||||
{/* Original Backend Card */}
|
{/* Original Backend Card */}
|
||||||
<button
|
<button
|
||||||
onClick={() => handleBackendChange('original')}
|
onClick={() => handleBackendChange('original')}
|
||||||
disabled={backendSaving}
|
disabled={updateBackendMutation.isPending}
|
||||||
className={`p-4 rounded-lg border-2 text-left transition-all ${
|
className={`p-4 rounded-lg border-2 text-left transition-all ${
|
||||||
backend === 'original'
|
backend === 'original'
|
||||||
? 'border-primary bg-primary/5'
|
? 'border-primary bg-primary/5'
|
||||||
|
|||||||
Reference in New Issue
Block a user