diff --git a/client/src/lib/ports.js b/client/src/lib/ports.js index f6d95aa4f9..1f6f685198 100644 --- a/client/src/lib/ports.js +++ b/client/src/lib/ports.js @@ -8,4 +8,9 @@ * top of that. Use these instead of re-hardcoding a port literal in a form * default, a copy-paste help string, or a cross-machine URL. */ -export { PORTS, DEFAULT_PEER_PORT } from '../../../server/lib/ports.js'; +export { + PORTS, + DEFAULT_PEER_PORT, + DEFAULT_TAILCAT_LOCAL_PORT, + DEFAULT_TAILCAT_REMOTE_PORT, +} from '../../../server/lib/ports.js'; diff --git a/client/src/pages/Instances.jsx b/client/src/pages/Instances.jsx index 0ba6751a9f..6a966b343c 100644 --- a/client/src/pages/Instances.jsx +++ b/client/src/pages/Instances.jsx @@ -14,7 +14,7 @@ import Pill from '../components/ui/Pill'; import EmptyState from '../components/EmptyState'; import socket from '../services/socket'; import { - getInstances, updateSelfInstance, addPeer, updatePeer, + getInstances, updateSelfInstance, addPeer, addTailcatPeer, updatePeer, removePeer, connectPeer, reciprocatePeer, probePeer, syncPeer, getTailnetInfo, getNetworkExposure, listPeerSubscriptions, @@ -24,7 +24,7 @@ import { import PeerAppsList from '../components/instances/PeerAppsList'; import PeerAgentsSection from '../components/instances/PeerAgentsSection'; import { SchemaGapBadge } from '../components/instances/SchemaGapBadge'; -import { DEFAULT_PEER_PORT } from '../lib/ports.js'; +import { DEFAULT_PEER_PORT, DEFAULT_TAILCAT_LOCAL_PORT } from '../lib/ports.js'; import PeerMediaProviderPanel from '../components/instances/PeerMediaProviderPanel'; import UnattendedRenderRouting from '../components/instances/UnattendedRenderRouting'; import BrainParityPanel from '../components/instances/BrainParityPanel'; @@ -207,7 +207,9 @@ function SelfCard({ self, onUpdate, syncStatus, tailnetInfo }) { // Exported for focused tests (the port input's placeholder must advertise the // same default the form actually submits — see Instances.test.jsx). export function AddPeerForm({ onAdd, addressRef }) { + const [mode, setMode] = useState('classic'); // 'classic' | 'tailcat' const [address, setAddress] = useState(''); + const [tcAddress, setTcAddress] = useState(''); const [port, setPort] = useState(String(DEFAULT_PEER_PORT)); const [name, setName] = useState(''); const [showAuth, setShowAuth] = useState(false); @@ -217,6 +219,24 @@ export function AddPeerForm({ onAdd, addressRef }) { const handleSubmit = async (e) => { e.preventDefault(); + if (mode === 'tailcat') { + if (!tcAddress.trim()) return; + setAdding(true); + const data = { tcAddress: tcAddress.trim() }; + if (name.trim()) data.name = name.trim(); + if (password) data.auth = { username: username.trim(), password }; + const result = await addTailcatPeer(data).catch(() => null); + setAdding(false); + if (!result) return; + setTcAddress(''); + setName(''); + setUsername(''); + setPassword(''); + setShowAuth(false); + onAdd(); + toast.success(`Peer added via tailcat (local :${result.port || DEFAULT_TAILCAT_LOCAL_PORT})`); + return; + } if (!address.trim()) return; setAdding(true); const data = { address: address.trim(), port: parseInt(port, 10) || DEFAULT_PEER_PORT }; @@ -237,11 +257,58 @@ export function AddPeerForm({ onAdd, addressRef }) { toast.success('Peer added'); }; + const canSubmit = mode === 'tailcat' ? !!tcAddress.trim() : !!address.trim(); + return (