import { useEffect, useState } from 'react' import { Plus, Play, Square, Trash2, ArrowRightLeft, ArrowLeftRight, Shuffle } from 'lucide-react' import { api, type Tunnel, type Integration } from '../lib/api' const TEXT_PRIMARY = '#E8E6E0' const TEXT_SECONDARY = '#7A7D85' const GOLD = '#C8A434' const cardBase: React.CSSProperties = { backgroundColor: 'rgba(10, 10, 12, 0.92)', border: '1px solid rgba(200, 164, 52, 0.08)', borderRadius: '12px', padding: '20px', boxShadow: '0 0 20px rgba(200, 164, 52, 0.03)', } const MODE_LABEL: Record = { local: 'Local Forward', remote: 'Remote Forward', dynamic: 'Dynamic (SOCKS5)', } const MODE_ICON: Record> = { local: ArrowRightLeft, remote: ArrowLeftRight, dynamic: Shuffle, } const STATUS_COLOR: Record = { stopped: TEXT_SECONDARY, connecting: GOLD, retrying: '#E0A030', connected: '#2ECC71', error: '#E74C3C', } function inputStyle(): React.CSSProperties { return { backgroundColor: 'rgba(255,255,255,0.03)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '6px', padding: '6px 10px', color: TEXT_PRIMARY, fontSize: '13px', width: '100%', } } export default function Tunnels() { const [tunnels, setTunnels] = useState([]) const [hosts, setHosts] = useState([]) const [showForm, setShowForm] = useState(false) const [busyId, setBusyId] = useState(null) const [error, setError] = useState(null) const [name, setName] = useState('') const [integrationId, setIntegrationId] = useState('') const [mode, setMode] = useState('local') const [sourcePort, setSourcePort] = useState('') const [endpointHost, setEndpointHost] = useState('') const [endpointPort, setEndpointPort] = useState('') const [autoStart, setAutoStart] = useState(false) function refresh() { api.listTunnels().then(({ tunnels }) => setTunnels(tunnels)) } useEffect(() => { refresh() api.listIntegrations().then(({ integrations }) => setHosts(integrations.filter((i) => i.type === 'ssh'))) const interval = setInterval(refresh, 3000) return () => clearInterval(interval) }, []) async function handleCreate() { if (!name.trim() || !integrationId || !sourcePort) { setError('Name, SSH host, and source port are required') return } setError(null) try { await api.createTunnel({ name: name.trim(), integrationId: Number(integrationId), mode, sourcePort: Number(sourcePort), endpointHost: mode === 'dynamic' ? '' : endpointHost, endpointPort: mode === 'dynamic' ? 0 : Number(endpointPort) || 0, autoStart, }) setName('') setIntegrationId('') setSourcePort('') setEndpointHost('') setEndpointPort('') setAutoStart(false) setShowForm(false) refresh() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create tunnel') } } async function handleConnect(id: number) { setBusyId(id) try { await api.connectTunnel(id) refresh() } finally { setBusyId(null) } } async function handleDisconnect(id: number) { setBusyId(id) try { await api.disconnectTunnel(id) refresh() } finally { setBusyId(null) } } async function handleDelete(id: number) { setBusyId(id) try { await api.deleteTunnel(id) refresh() } finally { setBusyId(null) } } return (

SSH Tunnels

Local / remote / dynamic SOCKS5 port forwarding through your configured SSH hosts.

{showForm && (
{error &&
{error}
}
setName(e.target.value)} placeholder="my-tunnel" />
setSourcePort(e.target.value)} placeholder="8080" />
{mode !== 'dynamic' && ( <>
setEndpointHost(e.target.value)} placeholder="127.0.0.1" />
setEndpointPort(e.target.value)} placeholder="80" />
)}
)}
{tunnels.map((t) => { const Icon = MODE_ICON[t.mode] const host = hosts.find((h) => h.id === t.integrationId) return (
{t.name}
{t.status} {t.status === 'retrying' ? ` (${t.retryCount}/${t.maxRetries})` : ''}
{MODE_LABEL[t.mode]}
via {host?.name ?? `integration #${t.integrationId}`}
localhost:{t.sourcePort} {t.mode === 'dynamic' ? '(SOCKS5 proxy)' : `→ ${t.endpointHost}:${t.endpointPort}`}
{t.error &&
{t.error}
}
{t.status === 'connected' || t.status === 'connecting' || t.status === 'retrying' ? ( ) : ( )}
) })} {tunnels.length === 0 && !showForm && (
No tunnels configured yet.
)}
) }