diff --git a/ui/src/components/cliproxy/control-panel-embed.tsx b/ui/src/components/cliproxy/control-panel-embed.tsx index 393a9e64..90636e7a 100644 --- a/ui/src/components/cliproxy/control-panel-embed.tsx +++ b/ui/src/components/cliproxy/control-panel-embed.tsx @@ -3,10 +3,14 @@ * * Embeds the CLIProxy management.html with auto-authentication. * Uses postMessage to inject credentials into the iframe. + * Supports both local and remote CLIProxy server connections. */ -import { useState, useEffect, useRef, useCallback } from 'react'; -import { RefreshCw, AlertCircle, Key, X, Gauge } from 'lucide-react'; +import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; +import { RefreshCw, AlertCircle, Key, X, Gauge, Globe } from 'lucide-react'; +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/lib/api-client'; +import type { CliproxyServerConfig } from '@/lib/api-client'; /** CLIProxyAPI default port */ const CLIPROXY_DEFAULT_PORT = 8317; @@ -25,13 +29,52 @@ export function ControlPanelEmbed({ port = CLIPROXY_DEFAULT_PORT }: ControlPanel const [isConnected, setIsConnected] = useState(false); const [showLoginHint, setShowLoginHint] = useState(true); - const managementUrl = `http://localhost:${port}/management.html`; + // Fetch cliproxy_server config for remote/local mode detection + const { data: cliproxyConfig } = useQuery({ + queryKey: ['cliproxy-server-config'], + queryFn: () => api.cliproxyServer.get(), + staleTime: 30000, // 30 seconds + }); + + // Calculate URLs and settings based on remote or local mode + const { managementUrl, checkUrl, authToken, isRemote, displayHost } = useMemo(() => { + const remote = cliproxyConfig?.remote; + + if (remote?.enabled && remote?.host) { + const protocol = remote.protocol || 'http'; + // Use port from config, or default based on protocol (443 for https, 80 for http) + const remotePort = remote.port || (protocol === 'https' ? 443 : 80); + // Only include port in URL if it's non-standard + const portSuffix = + (protocol === 'https' && remotePort === 443) || (protocol === 'http' && remotePort === 80) + ? '' + : `:${remotePort}`; + const baseUrl = `${protocol}://${remote.host}${portSuffix}`; + + return { + managementUrl: `${baseUrl}/management.html`, + checkUrl: `${baseUrl}/`, + authToken: remote.auth_token || undefined, + isRemote: true, + displayHost: `${remote.host}${portSuffix}`, + }; + } + + // Local mode + return { + managementUrl: `http://localhost:${port}/management.html`, + checkUrl: `http://localhost:${port}/`, + authToken: CCS_CONTROL_PANEL_SECRET, + isRemote: false, + displayHost: `localhost:${port}`, + }; + }, [cliproxyConfig, port]); // Check if CLIProxy is running useEffect(() => { const checkConnection = async () => { try { - const response = await fetch(`http://localhost:${port}/`, { + const response = await fetch(checkUrl, { signal: AbortSignal.timeout(2000), }); if (response.ok) { @@ -39,16 +82,24 @@ export function ControlPanelEmbed({ port = CLIPROXY_DEFAULT_PORT }: ControlPanel setError(null); } else { setIsConnected(false); - setError('CLIProxy returned an error'); + setError( + isRemote + ? `Remote CLIProxy at ${displayHost} returned an error` + : 'CLIProxy returned an error' + ); } } catch { setIsConnected(false); - setError('CLIProxy is not running'); + setError( + isRemote + ? `Remote CLIProxy at ${displayHost} is not reachable` + : 'CLIProxy is not running' + ); } }; checkConnection(); - }, [port]); + }, [checkUrl, isRemote, displayHost]); // Handle iframe load - attempt to auto-login via postMessage const handleIframeLoad = useCallback(() => { @@ -57,23 +108,25 @@ export function ControlPanelEmbed({ port = CLIPROXY_DEFAULT_PORT }: ControlPanel // Try to inject credentials via postMessage // The management.html needs to listen for this message // If it doesn't support it, user will see the login page - if (iframeRef.current?.contentWindow) { + if (iframeRef.current?.contentWindow && authToken) { try { + // Derive apiBase from checkUrl (remove trailing slash) + const apiBase = checkUrl.replace(/\/$/, ''); // Send credentials to iframe iframeRef.current.contentWindow.postMessage( { type: 'ccs-auto-login', - apiBase: `http://localhost:${port}`, - managementKey: CCS_CONTROL_PANEL_SECRET, + apiBase, + managementKey: authToken, }, - `http://localhost:${port}` + apiBase ); } catch { // Cross-origin restriction - expected if not same origin console.debug('[ControlPanelEmbed] postMessage failed - cross-origin'); } } - }, [port]); + }, [checkUrl, authToken]); const handleRefresh = () => { setIsLoading(true); @@ -119,15 +172,22 @@ export function ControlPanelEmbed({ port = CLIPROXY_DEFAULT_PORT }: ControlPanel return (
- {/* Login hint banner */} + {/* Remote indicator and login hint banner */} {showLoginHint && !isLoading && (
+ {isRemote && ( + <> + + Remote + | + + )} Key:{' '} - ccs + {authToken || 'ccs'}
)}