2026-01-04 21:58:59 -05:00
"use client" ;
2026-04-10 07:16:14 -04:00
import { useState , useEffect , useRef } from "react" ;
2026-01-20 01:16:34 -05:00
import PropTypes from "prop-types" ;
2026-02-18 01:46:14 -05:00
import { Card , Button , Input , Modal , CardSkeleton , Toggle } from "@/shared/components" ;
2026-01-04 21:58:59 -05:00
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard" ;
2026-02-21 04:42:46 -05:00
const TUNNEL _BENEFITS = [
{ icon : "public" , title : "Access Anywhere" , desc : "Use your API from any network" } ,
{ icon : "group" , title : "Share Endpoint" , desc : "Share URL with team members" } ,
{ icon : "code" , title : "Use in Cursor/Cline" , desc : "Connect AI tools remotely" } ,
{ icon : "lock" , title : "Encrypted" , desc : "End-to-end TLS via Cloudflare" } ,
] ;
2026-04-10 07:16:14 -04:00
const TUNNEL _PING _INTERVAL _MS = 2000 ;
const TUNNEL _PING _MAX _MS = 300000 ;
2026-01-04 21:58:59 -05:00
export default function APIPageClient ( { machineId } ) {
const [ keys , setKeys ] = useState ( [ ] ) ;
const [ loading , setLoading ] = useState ( true ) ;
const [ showAddModal , setShowAddModal ] = useState ( false ) ;
const [ newKeyName , setNewKeyName ] = useState ( "" ) ;
const [ createdKey , setCreatedKey ] = useState ( null ) ;
2026-02-21 04:42:46 -05:00
const [ requireApiKey , setRequireApiKey ] = useState ( false ) ;
2026-04-11 03:56:35 -04:00
const [ requireLogin , setRequireLogin ] = useState ( true ) ;
const [ hasPassword , setHasPassword ] = useState ( true ) ;
const [ tunnelDashboardAccess , setTunnelDashboardAccess ] = useState ( false ) ;
2026-04-10 07:16:14 -04:00
// Cloudflare Tunnel state
const [ tunnelChecking , setTunnelChecking ] = useState ( true ) ;
2026-02-21 04:42:46 -05:00
const [ tunnelEnabled , setTunnelEnabled ] = useState ( false ) ;
const [ tunnelUrl , setTunnelUrl ] = useState ( "" ) ;
2026-03-19 12:47:13 -04:00
const [ tunnelPublicUrl , setTunnelPublicUrl ] = useState ( "" ) ;
2026-02-21 04:42:46 -05:00
const [ tunnelLoading , setTunnelLoading ] = useState ( false ) ;
const [ tunnelProgress , setTunnelProgress ] = useState ( "" ) ;
const [ tunnelStatus , setTunnelStatus ] = useState ( null ) ;
2026-04-10 07:16:14 -04:00
const [ showEnableTunnelModal , setShowEnableTunnelModal ] = useState ( false ) ;
const [ showDisableTunnelModal , setShowDisableTunnelModal ] = useState ( false ) ;
// Tailscale state
const [ tsEnabled , setTsEnabled ] = useState ( false ) ;
const [ tsUrl , setTsUrl ] = useState ( "" ) ;
const [ tsLoading , setTsLoading ] = useState ( false ) ;
const [ tsProgress , setTsProgress ] = useState ( "" ) ;
const [ tsStatus , setTsStatus ] = useState ( null ) ;
const [ tsInstalled , setTsInstalled ] = useState ( null ) ; // null=checking, true/false
const [ tsInstalling , setTsInstalling ] = useState ( false ) ;
const [ tsInstallLog , setTsInstallLog ] = useState ( [ ] ) ;
const [ tsSudoPassword , setTsSudoPassword ] = useState ( "" ) ;
const [ tsConnecting , setTsConnecting ] = useState ( false ) ;
const [ showTsModal , setShowTsModal ] = useState ( false ) ;
const [ showDisableTsModal , setShowDisableTsModal ] = useState ( false ) ;
const tsLogRef = useRef ( null ) ;
2026-02-27 23:07:51 -05:00
// API key visibility toggle state
const [ visibleKeys , setVisibleKeys ] = useState ( new Set ( ) ) ;
2026-01-04 21:58:59 -05:00
const { copied , copy } = useCopyToClipboard ( ) ;
2026-04-10 07:16:14 -04:00
// Auto-scroll install log
useEffect ( ( ) => {
if ( tsLogRef . current ) tsLogRef . current . scrollTop = tsLogRef . current . scrollHeight ;
} , [ tsInstallLog ] ) ;
2026-01-04 21:58:59 -05:00
useEffect ( ( ) => {
fetchData ( ) ;
2026-02-21 04:42:46 -05:00
loadSettings ( ) ;
2026-01-04 21:58:59 -05:00
} , [ ] ) ;
2026-02-21 04:42:46 -05:00
const loadSettings = async ( ) => {
2026-04-10 07:16:14 -04:00
setTunnelChecking ( true ) ;
2026-02-21 04:42:46 -05:00
try {
2026-04-10 07:16:14 -04:00
const [ settingsRes , statusRes ] = await Promise . all ( [
2026-02-21 04:42:46 -05:00
fetch ( "/api/settings" ) ,
fetch ( "/api/tunnel/status" )
] ) ;
if ( settingsRes . ok ) {
const data = await settingsRes . json ( ) ;
setRequireApiKey ( data . requireApiKey || false ) ;
2026-04-11 03:56:35 -04:00
setRequireLogin ( data . requireLogin !== false ) ;
setHasPassword ( data . hasPassword || false ) ;
setTunnelDashboardAccess ( data . tunnelDashboardAccess || false ) ;
2026-02-21 04:42:46 -05:00
}
2026-04-10 07:16:14 -04:00
if ( statusRes . ok ) {
const data = await statusRes . json ( ) ;
const tEnabled = data . tunnel ? . enabled || false ;
const tUrl = data . tunnel ? . tunnelUrl || "" ;
const tPublicUrl = data . tunnel ? . publicUrl || "" ;
setTunnelUrl ( tUrl ) ;
setTunnelPublicUrl ( tPublicUrl ) ;
const tsEn = data . tailscale ? . enabled || false ;
const tsUrlVal = data . tailscale ? . tunnelUrl || "" ;
setTsUrl ( tsUrlVal ) ;
if ( tsEn && tsUrlVal ) {
setTsLoading ( true ) ;
setTsProgress ( "Checking Tailscale..." ) ;
const tsHealthUrl = ` ${ tsUrlVal } /api/health ` ;
try {
const tsPing = await fetch ( tsHealthUrl , { mode : "no-cors" , cache : "no-store" } ) ;
if ( tsPing . ok || tsPing . type === "opaque" ) {
setTsEnabled ( true ) ;
} else {
const ok = await pingTsHealth ( tsUrlVal ) ;
setTsEnabled ( ok ) ;
if ( ! ok ) setTsStatus ( { type : "warning" , message : "Tailscale not reachable." } ) ;
}
} catch {
const ok = await pingTsHealth ( tsUrlVal ) ;
setTsEnabled ( ok ) ;
if ( ! ok ) setTsStatus ( { type : "warning" , message : "Tailscale not reachable." } ) ;
} finally {
setTsLoading ( false ) ;
setTsProgress ( "" ) ;
}
} else {
setTsEnabled ( tsEn ) ;
}
if ( tEnabled && ( tPublicUrl || tUrl ) ) {
// Ping once to verify reachable
const healthUrl = ` ${ tPublicUrl || tUrl } /api/health ` ;
try {
2026-04-13 06:38:09 -04:00
const ping = await fetch ( healthUrl , { cache : "no-store" } ) ;
if ( ping . ok ) {
2026-04-10 07:16:14 -04:00
setTunnelEnabled ( true ) ;
} else {
pingTunnelHealth ( tPublicUrl || tUrl ) ;
}
} catch {
pingTunnelHealth ( tPublicUrl || tUrl ) ;
}
} else {
setTunnelEnabled ( tEnabled ) ;
}
2026-02-21 04:42:46 -05:00
}
} catch ( error ) {
console . log ( "Error loading settings:" , error ) ;
2026-04-10 07:16:14 -04:00
} finally {
setTunnelChecking ( false ) ;
2026-02-21 04:42:46 -05:00
}
} ;
2026-04-11 03:56:35 -04:00
const handleTunnelDashboardAccess = async ( value ) => {
try {
const res = await fetch ( "/api/settings" , {
method : "PATCH" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { tunnelDashboardAccess : value } ) ,
} ) ;
if ( res . ok ) setTunnelDashboardAccess ( value ) ;
} catch ( error ) {
console . log ( "Error updating tunnelDashboardAccess:" , error ) ;
}
} ;
2026-02-21 04:42:46 -05:00
const handleRequireApiKey = async ( value ) => {
try {
const res = await fetch ( "/api/settings" , {
method : "PATCH" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { requireApiKey : value } ) ,
} ) ;
if ( res . ok ) setRequireApiKey ( value ) ;
} catch ( error ) {
console . log ( "Error updating requireApiKey:" , error ) ;
}
} ;
const fetchData = async ( ) => {
try {
const keysRes = await fetch ( "/api/keys" ) ;
const keysData = await keysRes . json ( ) ;
if ( keysRes . ok ) {
setKeys ( keysData . keys || [ ] ) ;
}
} catch ( error ) {
console . log ( "Error fetching data:" , error ) ;
} finally {
setLoading ( false ) ;
}
} ;
2026-04-10 07:16:14 -04:00
// u2500u2500u2500 Cloudflare Tunnel handlers
// Ping tunnel health until reachable, also check backend status to detect process die
const pingTunnelHealth = async ( url ) => {
setTunnelLoading ( true ) ;
setTunnelProgress ( "Waiting for tunnel ready..." ) ;
const healthUrl = ` ${ url } /api/health ` ;
const start = Date . now ( ) ;
while ( Date . now ( ) - start < TUNNEL _PING _MAX _MS ) {
await new Promise ( ( r ) => setTimeout ( r , TUNNEL _PING _INTERVAL _MS ) ) ;
try {
const ping = await fetch ( healthUrl , { mode : "no-cors" , cache : "no-store" } ) ;
if ( ping . ok || ping . type === "opaque" ) {
setTunnelEnabled ( true ) ;
setTunnelLoading ( false ) ;
setTunnelProgress ( "" ) ;
return true ;
}
} catch { /* not ready yet */ }
// Every 5 pings (~10s), check if backend process still alive
if ( ( Date . now ( ) - start ) % 10000 < TUNNEL _PING _INTERVAL _MS ) {
try {
const statusRes = await fetch ( "/api/tunnel/status" ) ;
if ( statusRes . ok ) {
const status = await statusRes . json ( ) ;
if ( ! status . tunnel ? . enabled ) {
setTunnelStatus ( { type : "error" , message : "Tunnel process stopped unexpectedly." } ) ;
setTunnelLoading ( false ) ;
setTunnelProgress ( "" ) ;
return false ;
}
}
} catch { /* ignore */ }
}
}
setTunnelStatus ( { type : "error" , message : "Tunnel created but not reachable. Please try again." } ) ;
setTunnelLoading ( false ) ;
setTunnelProgress ( "" ) ;
return false ;
} ;
2026-02-21 04:42:46 -05:00
const handleEnableTunnel = async ( ) => {
2026-04-10 07:16:14 -04:00
setShowEnableTunnelModal ( false ) ;
2026-02-21 04:42:46 -05:00
setTunnelLoading ( true ) ;
setTunnelStatus ( null ) ;
2026-04-10 07:16:14 -04:00
setTunnelProgress ( "Creating tunnel..." ) ;
2026-04-15 23:58:35 -04:00
// Poll download progress while enable request is pending
let polling = true ;
const pollProgress = async ( ) => {
while ( polling ) {
try {
const r = await fetch ( "/api/tunnel/status" ) ;
if ( r . ok ) {
const s = await r . json ( ) ;
if ( s . download ? . downloading ) {
setTunnelProgress ( ` Downloading cloudflared... ${ s . download . progress } % ` ) ;
} else if ( polling ) {
setTunnelProgress ( "Creating tunnel..." ) ;
}
}
} catch { /* ignore */ }
await new Promise ( ( r ) => setTimeout ( r , 1000 ) ) ;
}
} ;
pollProgress ( ) ;
2026-02-21 04:42:46 -05:00
try {
2026-04-10 07:16:14 -04:00
const res = await fetch ( "/api/tunnel/enable" , { method : "POST" } ) ;
2026-04-15 23:58:35 -04:00
polling = false ;
2026-02-21 04:42:46 -05:00
const data = await res . json ( ) ;
2026-04-10 07:16:14 -04:00
if ( ! res . ok ) {
2026-02-21 04:42:46 -05:00
setTunnelStatus ( { type : "error" , message : data . error || "Failed to enable tunnel" } ) ;
2026-04-10 07:16:14 -04:00
return ;
}
const url = data . publicUrl || data . tunnelUrl ;
if ( ! url ) {
setTunnelStatus ( { type : "error" , message : "No tunnel URL returned" } ) ;
return ;
2026-02-21 04:42:46 -05:00
}
2026-04-10 07:16:14 -04:00
setTunnelUrl ( data . tunnelUrl || "" ) ;
setTunnelPublicUrl ( data . publicUrl || "" ) ;
await pingTunnelHealth ( url ) ;
2026-02-21 04:42:46 -05:00
} catch ( error ) {
2026-04-10 07:16:14 -04:00
setTunnelStatus ( { type : "error" , message : error . message } ) ;
2026-02-21 04:42:46 -05:00
} finally {
2026-04-15 23:58:35 -04:00
polling = false ;
2026-02-21 04:42:46 -05:00
setTunnelLoading ( false ) ;
setTunnelProgress ( "" ) ;
}
} ;
const handleDisableTunnel = async ( ) => {
setTunnelLoading ( true ) ;
setTunnelStatus ( null ) ;
try {
const res = await fetch ( "/api/tunnel/disable" , { method : "POST" } ) ;
const data = await res . json ( ) ;
if ( res . ok ) {
setTunnelEnabled ( false ) ;
setTunnelUrl ( "" ) ;
2026-03-19 12:47:13 -04:00
setTunnelPublicUrl ( "" ) ;
2026-04-10 07:16:14 -04:00
setShowDisableTunnelModal ( false ) ;
2026-02-21 04:42:46 -05:00
setTunnelStatus ( { type : "success" , message : "Tunnel disabled" } ) ;
} else {
setTunnelStatus ( { type : "error" , message : data . error || "Failed to disable tunnel" } ) ;
}
} catch ( error ) {
setTunnelStatus ( { type : "error" , message : error . message } ) ;
} finally {
setTunnelLoading ( false ) ;
}
} ;
2026-02-08 23:27:41 -05:00
2026-04-10 07:16:14 -04:00
// u2500u2500u2500 Tailscale handlers
const checkTailscaleInstalled = async ( ) => {
setTsInstalled ( null ) ;
try {
const res = await fetch ( "/api/tunnel/tailscale-check" ) ;
if ( res . ok ) {
const data = await res . json ( ) ;
setTsInstalled ( data . installed ) ;
return data ;
}
} catch { /* ignore */ }
setTsInstalled ( false ) ;
return { installed : false } ;
} ;
const handleInstallTailscale = async ( ) => {
setTsInstalling ( true ) ;
setTsStatus ( null ) ;
setTsInstallLog ( [ ] ) ;
try {
const res = await fetch ( "/api/tunnel/tailscale-install" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { sudoPassword : tsSudoPassword } ) ,
} ) ;
setTsSudoPassword ( "" ) ;
const reader = res . body . getReader ( ) ;
const decoder = new TextDecoder ( ) ;
let buffer = "" ;
while ( true ) {
const { done , value } = await reader . read ( ) ;
if ( done ) break ;
buffer += decoder . decode ( value , { stream : true } ) ;
const parts = buffer . split ( "\n\n" ) ;
buffer = parts . pop ( ) || "" ;
for ( const part of parts ) {
const lines = part . split ( "\n" ) ;
let event = "progress" ;
let data = null ;
for ( const line of lines ) {
if ( line . startsWith ( "event: " ) ) event = line . slice ( 7 ) . trim ( ) ;
if ( line . startsWith ( "data: " ) ) {
try { data = JSON . parse ( line . slice ( 6 ) ) ; } catch { /* skip */ }
}
}
if ( ! data ) continue ;
if ( event === "progress" ) {
setTsInstallLog ( ( prev ) => [ ... prev . slice ( - 50 ) , data . message ] ) ;
} else if ( event === "done" ) {
setTsInstalled ( true ) ;
setTsInstalling ( false ) ;
return ;
} else if ( event === "error" ) {
setTsStatus ( { type : "error" , message : data . error || "Install failed" } ) ;
}
}
}
} catch ( e ) {
setTsStatus ( { type : "error" , message : e . message } ) ;
} finally {
setTsInstalling ( false ) ;
}
} ;
// Ping Tailscale health until reachable
const pingTsHealth = async ( url ) => {
setTsProgress ( "Waiting for Tailscale ready..." ) ;
const healthUrl = ` ${ url } /api/health ` ;
const start = Date . now ( ) ;
while ( Date . now ( ) - start < TUNNEL _PING _MAX _MS ) {
await new Promise ( ( r ) => setTimeout ( r , TUNNEL _PING _INTERVAL _MS ) ) ;
try {
const ping = await fetch ( healthUrl , { mode : "no-cors" , cache : "no-store" } ) ;
if ( ping . ok || ping . type === "opaque" ) return true ;
} catch { /* not ready yet */ }
}
return false ;
} ;
const handleConnectTailscale = async ( preOpenedTab ) => {
const tab = preOpenedTab || null ;
setShowTsModal ( false ) ;
setTsConnecting ( true ) ;
setTsLoading ( true ) ;
setTsStatus ( null ) ;
setTsProgress ( "Connecting..." ) ;
try {
const res = await fetch ( "/api/tunnel/tailscale-enable" , { method : "POST" } ) ;
const data = await res . json ( ) ;
if ( res . ok && data . success ) {
if ( tab ) tab . close ( ) ;
setTsUrl ( data . tunnelUrl || "" ) ;
const reachable = await pingTsHealth ( data . tunnelUrl ) ;
if ( reachable ) {
setTsEnabled ( true ) ;
setTsStatus ( null ) ;
} else {
setTsEnabled ( true ) ;
setTsStatus ( { type : "warning" , message : "Connected but not reachable yet." } ) ;
}
return ;
}
// Needs login: redirect pre-opened tab or open new
if ( data . needsLogin && data . authUrl ) {
if ( tab ) tab . location . href = data . authUrl ;
else window . open ( data . authUrl , "tailscale_auth" , "width=600,height=700" ) ;
setTsProgress ( "Waiting for login..." ) ;
for ( let i = 0 ; i < 40 ; i ++ ) {
await new Promise ( ( r ) => setTimeout ( r , 3000 ) ) ;
try {
const r2 = await fetch ( "/api/tunnel/tailscale-check" ) ;
if ( r2 . ok ) {
const check = await r2 . json ( ) ;
if ( check . loggedIn ) {
setTsProgress ( "Starting funnel..." ) ;
const res2 = await fetch ( "/api/tunnel/tailscale-enable" , { method : "POST" } ) ;
const data2 = await res2 . json ( ) ;
if ( res2 . ok && data2 . success ) {
if ( tab ) tab . close ( ) ;
setTsUrl ( data2 . tunnelUrl || "" ) ;
const ok2 = await pingTsHealth ( data2 . tunnelUrl ) ;
if ( ok2 ) {
setTsEnabled ( true ) ;
setTsStatus ( null ) ;
} else {
setTsEnabled ( true ) ;
setTsStatus ( { type : "warning" , message : "Connected but not reachable yet." } ) ;
}
} else if ( data2 . funnelNotEnabled && data2 . enableUrl ) {
await pollFunnelEnable ( data2 . enableUrl , tab ) ;
} else {
setTsStatus ( { type : "error" , message : data2 . error || "Failed to start funnel" } ) ;
}
return ;
}
}
} catch { /* retry */ }
}
setTsStatus ( { type : "error" , message : "Login timed out. Please try again." } ) ;
return ;
}
// Funnel not enabled: redirect pre-opened tab
if ( data . funnelNotEnabled && data . enableUrl ) {
await pollFunnelEnable ( data . enableUrl , tab ) ;
return ;
}
if ( tab ) tab . close ( ) ;
setTsStatus ( { type : "error" , message : data . error || "Failed to connect" } ) ;
} catch ( error ) {
if ( tab ) tab . close ( ) ;
setTsStatus ( { type : "error" , message : error . message } ) ;
} finally {
setTsLoading ( false ) ;
setTsConnecting ( false ) ;
setTsProgress ( "" ) ;
}
} ;
const pollFunnelEnable = async ( enableUrl , tab ) => {
if ( tab ) tab . location . href = enableUrl ;
else window . open ( enableUrl , "tailscale_auth" , "width=600,height=700" ) ;
setTsProgress ( "Enable Funnel in browser, waiting..." ) ;
for ( let i = 0 ; i < 40 ; i ++ ) {
await new Promise ( ( r ) => setTimeout ( r , 3000 ) ) ;
try {
const res = await fetch ( "/api/tunnel/tailscale-enable" , { method : "POST" } ) ;
const data = await res . json ( ) ;
if ( res . ok && data . success ) {
if ( tab ) tab . close ( ) ;
setTsUrl ( data . tunnelUrl || "" ) ;
const ok3 = await pingTsHealth ( data . tunnelUrl ) ;
if ( ok3 ) {
setTsEnabled ( true ) ;
setTsStatus ( null ) ;
} else {
setTsEnabled ( true ) ;
setTsStatus ( { type : "warning" , message : "Connected but not reachable yet." } ) ;
}
return ;
}
if ( data . funnelNotEnabled ) continue ;
if ( data . error ) {
setTsStatus ( { type : "error" , message : data . error } ) ;
return ;
}
} catch { /* retry */ }
}
setTsStatus ( { type : "error" , message : "Timed out waiting for Funnel to be enabled." } ) ;
} ;
const handleDisableTailscale = async ( ) => {
setTsLoading ( true ) ;
setTsStatus ( null ) ;
try {
const res = await fetch ( "/api/tunnel/tailscale-disable" , { method : "POST" } ) ;
const data = await res . json ( ) ;
if ( res . ok ) {
setTsEnabled ( false ) ;
setTsUrl ( "" ) ;
setShowDisableTsModal ( false ) ;
setTsStatus ( { type : "success" , message : "Tailscale disabled" } ) ;
} else {
setTsStatus ( { type : "error" , message : data . error || "Failed to disable Tailscale" } ) ;
}
} catch ( e ) {
setTsStatus ( { type : "error" , message : e . message } ) ;
} finally {
setTsLoading ( false ) ;
}
} ;
const handleOpenTsModal = async ( ) => {
setTsStatus ( null ) ;
setTsInstallLog ( [ ] ) ;
setShowTsModal ( true ) ;
await checkTailscaleInstalled ( ) ;
} ;
2026-01-04 21:58:59 -05:00
const handleCreateKey = async ( ) => {
if ( ! newKeyName . trim ( ) ) return ;
try {
const res = await fetch ( "/api/keys" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { name : newKeyName } ) ,
} ) ;
const data = await res . json ( ) ;
if ( res . ok ) {
setCreatedKey ( data . key ) ;
await fetchData ( ) ;
setNewKeyName ( "" ) ;
setShowAddModal ( false ) ;
}
} catch ( error ) {
console . log ( "Error creating key:" , error ) ;
}
} ;
const handleDeleteKey = async ( id ) => {
if ( ! confirm ( "Delete this API key?" ) ) return ;
try {
const res = await fetch ( ` /api/keys/ ${ id } ` , { method : "DELETE" } ) ;
if ( res . ok ) {
setKeys ( keys . filter ( ( k ) => k . id !== id ) ) ;
2026-02-27 23:07:51 -05:00
// Clean up visibility state
setVisibleKeys ( prev => {
const next = new Set ( prev ) ;
next . delete ( id ) ;
return next ;
} ) ;
2026-01-04 21:58:59 -05:00
}
} catch ( error ) {
console . log ( "Error deleting key:" , error ) ;
}
} ;
2026-02-20 03:07:12 -05:00
const handleToggleKey = async ( id , isActive ) => {
try {
const res = await fetch ( ` /api/keys/ ${ id } ` , {
method : "PUT" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { isActive } ) ,
} ) ;
if ( res . ok ) {
setKeys ( prev => prev . map ( k => k . id === id ? { ... k , isActive } : k ) ) ;
}
} catch ( error ) {
console . log ( "Error toggling key:" , error ) ;
}
} ;
2026-02-27 23:07:51 -05:00
const maskKey = ( fullKey ) => {
if ( ! fullKey ) return "" ;
return fullKey . length > 8 ? fullKey . slice ( 0 , 8 ) + "..." : fullKey ;
} ;
const toggleKeyVisibility = ( keyId ) => {
setVisibleKeys ( prev => {
const next = new Set ( prev ) ;
if ( next . has ( keyId ) ) next . delete ( keyId ) ;
else next . add ( keyId ) ;
return next ;
} ) ;
} ;
2026-01-20 01:16:34 -05:00
const [ baseUrl , setBaseUrl ] = useState ( "/v1" ) ;
2026-01-04 21:58:59 -05:00
2026-01-20 01:16:34 -05:00
// Hydration fix: Only access window on client side
useEffect ( ( ) => {
if ( typeof window !== "undefined" ) {
setBaseUrl ( ` ${ window . location . origin } /v1 ` ) ;
}
} , [ ] ) ;
2026-01-04 21:58:59 -05:00
if ( loading ) {
return (
< div className = "flex flex-col gap-8" >
< CardSkeleton / >
< CardSkeleton / >
< / d i v >
) ;
}
2026-04-10 07:16:14 -04:00
const currentEndpoint = baseUrl ;
2026-01-04 21:58:59 -05:00
return (
< div className = "flex flex-col gap-8" >
{ /* Endpoint Card */ }
2026-02-08 23:27:41 -05:00
< Card >
2026-04-10 07:16:14 -04:00
< h2 className = "text-lg font-semibold mb-4" > API Endpoint < / h 2 >
{ /* Endpoint rows */ }
< div className = "flex flex-col gap-2" >
{ /* Local */ }
< EndpointRow
label = "Local"
url = { currentEndpoint }
copyId = "local_url"
copied = { copied }
onCopy = { copy }
/ >
{ /* Cloudflare Tunnel */ }
2026-01-04 21:58:59 -05:00
< div className = "flex items-center gap-2" >
2026-04-10 07:16:14 -04:00
< span className = { ` text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[68px] text-center ${
tunnelEnabled ? "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400" : "bg-sidebar text-text-muted"
} ` }>Tunnel</span>
{ tunnelEnabled && ! tunnelLoading ? (
< >
< Input value = { ` ${ tunnelPublicUrl || tunnelUrl } /v1 ` } readOnly className = "flex-1 font-mono text-sm" / >
< button
onClick = { ( ) => copy ( ` ${ tunnelPublicUrl || tunnelUrl } /v1 ` , "tunnel_url" ) }
className = "p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors shrink-0"
>
< span className = "material-symbols-outlined text-[18px]" > { copied === "tunnel_url" ? "check" : "content_copy" } < / s p a n >
< / b u t t o n >
< button
onClick = { ( ) => setShowDisableTunnelModal ( true ) }
className = "p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
title = "Disable Tunnel"
>
< span className = "material-symbols-outlined text-[18px]" > power _settings _new < / s p a n >
< / b u t t o n >
< / >
) : tunnelLoading ? (
2026-04-13 04:52:01 -04:00
< >
< div className = "flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted" >
< span className = "material-symbols-outlined animate-spin text-sm" > progress _activity < / s p a n >
{ tunnelProgress || "Creating tunnel..." }
< / d i v >
< button
2026-04-13 05:49:34 -04:00
onClick = { ( ) => { setTunnelLoading ( false ) ; setTunnelProgress ( "" ) ; } }
2026-04-13 04:52:01 -04:00
className = "p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
2026-04-13 05:49:34 -04:00
title = "Stop"
2026-04-13 04:52:01 -04:00
>
< span className = "material-symbols-outlined text-[18px]" > power _settings _new < / s p a n >
< / b u t t o n >
< / >
2026-04-10 07:16:14 -04:00
) : tunnelStatus ? . type === "error" ? (
< >
< div className = "flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-red-300 dark:border-red-800 bg-red-500/5 text-sm text-red-600 dark:text-red-400" >
< span className = "material-symbols-outlined text-sm" > error < / s p a n >
{ tunnelStatus . message }
< / d i v >
< Button size = "sm" icon = "cloud_upload" onClick = { ( ) => setShowEnableTunnelModal ( true ) } > Enable < / B u t t o n >
< / >
) : tunnelChecking ? (
2026-04-13 04:52:01 -04:00
< >
< div className = "flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted" >
< span className = "material-symbols-outlined animate-spin text-sm" > progress _activity < / s p a n >
Checking ...
< / d i v >
< button
2026-04-13 05:49:34 -04:00
onClick = { ( ) => setTunnelChecking ( false ) }
2026-04-13 04:52:01 -04:00
className = "p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
2026-04-13 05:49:34 -04:00
title = "Stop"
2026-04-13 04:52:01 -04:00
>
< span className = "material-symbols-outlined text-[18px]" > power _settings _new < / s p a n >
< / b u t t o n >
< / >
2026-01-04 21:58:59 -05:00
) : (
< Button
2026-04-10 07:16:14 -04:00
size = "sm"
2026-01-04 21:58:59 -05:00
icon = "cloud_upload"
2026-04-04 12:24:24 -04:00
onClick = { ( ) => {
if ( ! requireApiKey ) {
setTunnelStatus ( { type : "error" , message : "Security required: Enable \"Require API key\" before activating the tunnel." } ) ;
return ;
}
2026-04-10 07:16:14 -04:00
setShowEnableTunnelModal ( true ) ;
2026-04-04 12:24:24 -04:00
} }
2026-04-10 07:16:14 -04:00
className = "bg-linear-to-r from-primary to-blue-500 hover:from-primary-hover hover:to-blue-600 text-white!"
2026-01-04 21:58:59 -05:00
>
2026-04-10 07:16:14 -04:00
Enable
2026-01-04 21:58:59 -05:00
< / B u t t o n >
) }
< / d i v >
2026-04-10 07:16:14 -04:00
{ /* Tailscale */ }
< div className = "flex items-center gap-2" >
< span className = { ` text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[68px] text-center ${
tsEnabled ? "bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400" : "bg-sidebar text-text-muted"
} ` }>Tailscale</span>
{ tsEnabled && ! tsLoading ? (
< >
< Input value = { ` ${ tsUrl } /v1 ` } readOnly className = "flex-1 font-mono text-sm" / >
< button
onClick = { ( ) => copy ( ` ${ tsUrl } /v1 ` , "ts_url" ) }
className = "p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors shrink-0"
>
< span className = "material-symbols-outlined text-[18px]" > { copied === "ts_url" ? "check" : "content_copy" } < / s p a n >
< / b u t t o n >
< button
onClick = { ( ) => setShowDisableTsModal ( true ) }
className = "p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
title = "Disable Tailscale"
>
< span className = "material-symbols-outlined text-[18px]" > power _settings _new < / s p a n >
< / b u t t o n >
< / >
) : ( tsLoading || tsConnecting ) ? (
2026-04-13 04:52:01 -04:00
< >
< div className = "flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-border bg-input text-sm text-text-muted" >
< span className = "material-symbols-outlined animate-spin text-sm" > progress _activity < / s p a n >
{ tsProgress || "Connecting..." }
< / d i v >
< button
2026-04-13 05:49:34 -04:00
onClick = { ( ) => { setTsLoading ( false ) ; setTsConnecting ( false ) ; setTsProgress ( "" ) ; } }
2026-04-13 04:52:01 -04:00
className = "p-2 hover:bg-red-500/10 rounded text-red-500 transition-colors shrink-0"
2026-04-13 05:49:34 -04:00
title = "Stop"
2026-04-13 04:52:01 -04:00
>
< span className = "material-symbols-outlined text-[18px]" > power _settings _new < / s p a n >
< / b u t t o n >
< / >
2026-04-10 07:16:14 -04:00
) : tsStatus ? . type === "error" ? (
< >
< div className = "flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-red-300 dark:border-red-800 bg-red-500/5 text-sm text-red-600 dark:text-red-400" >
< span className = "material-symbols-outlined text-sm" > error < / s p a n >
{ tsStatus . message }
< / d i v >
< Button size = "sm" icon = "vpn_lock" onClick = { handleOpenTsModal } > Enable < / B u t t o n >
< / >
) : (
< Button
size = "sm"
icon = "vpn_lock"
onClick = { handleOpenTsModal }
className = "bg-linear-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 text-white!"
>
Enable
< / B u t t o n >
) }
2026-02-08 23:27:41 -05:00
< / d i v >
2026-04-10 07:16:14 -04:00
< / d i v >
2026-04-11 03:56:35 -04:00
{ /* Security warnings when tunnel or tailscale is active */ }
{ ( tunnelEnabled || tsEnabled ) && (
< div className = "mt-4 flex flex-col gap-2" >
{ ! requireApiKey && (
< SecurityWarning
message = "Require API key is disabled — your endpoint is publicly accessible without authentication."
action = { { label : "Enable" , href : "#require-api-key" } }
/ >
) }
{ ( ! requireLogin || ! hasPassword ) && (
< SecurityWarning
message = {
! requireLogin
? "Require login is disabled — anyone can access your dashboard via tunnel."
: "Dashboard uses the default password — change it in Profile settings."
}
action = { {
label : ! requireLogin ? "Enable" : "Change password" ,
href : "/dashboard/profile" ,
} }
/ >
) }
< / d i v >
) }
{ /* Tunnel dashboard access option */ }
{ ( tunnelEnabled || tsEnabled ) && (
< div className = "mt-4 pt-4 border-t border-border flex items-center gap-3" >
< Toggle
checked = { tunnelDashboardAccess }
onChange = { ( ) => handleTunnelDashboardAccess ( ! tunnelDashboardAccess ) }
/ >
< div className = "flex items-center gap-1.5" >
< p className = "font-medium text-sm" > Allow dashboard access via tunnel < / p >
2026-04-13 06:38:09 -04:00
< Tooltip text = "When enabled, the dashboard can be accessed through your tunnel or Tailscale URL (login still required). When disabled, dashboard access via tunnel/Tailscale is completely blocked." / >
2026-04-11 03:56:35 -04:00
< / d i v >
< / d i v >
) }
2026-01-04 21:58:59 -05:00
< / C a r d >
{ /* API Keys */ }
2026-04-11 03:56:35 -04:00
< Card id = "require-api-key" >
2026-01-04 21:58:59 -05:00
< div className = "flex items-center justify-between mb-4" >
< h2 className = "text-lg font-semibold" > API Keys < / h 2 >
< Button icon = "add" onClick = { ( ) => setShowAddModal ( true ) } >
Create Key
< / B u t t o n >
< / d i v >
2026-02-18 01:46:14 -05:00
< div className = "flex items-center justify-between pb-4 mb-4 border-b border-border" >
< div >
< p className = "font-medium" > Require API key < / p >
< p className = "text-sm text-text-muted" >
Requests without a valid key will be rejected
< / p >
< / d i v >
< Toggle
checked = { requireApiKey }
onChange = { ( ) => handleRequireApiKey ( ! requireApiKey ) }
/ >
< / d i v >
2026-01-04 21:58:59 -05:00
{ keys . length === 0 ? (
2026-01-31 00:58:04 -05:00
< div className = "text-center py-12" >
< div className = "inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 text-primary mb-4" >
< span className = "material-symbols-outlined text-[32px]" > vpn _key < / s p a n >
< / d i v >
< p className = "text-text-main font-medium mb-1" > No API keys yet < / p >
< p className = "text-sm text-text-muted mb-4" > Create your first API key to get started < / p >
< Button icon = "add" onClick = { ( ) => setShowAddModal ( true ) } >
Create Key
< / B u t t o n >
2026-01-04 21:58:59 -05:00
< / d i v >
) : (
2026-01-31 00:58:04 -05:00
< div className = "flex flex-col" >
{ keys . map ( ( key ) => (
< div
key = { key . id }
2026-02-20 03:07:12 -05:00
className = { ` group flex items-center justify-between py-3 border-b border-black/[0.03] dark:border-white/[0.03] last:border-b-0 ${ key . isActive === false ? "opacity-60" : "" } ` }
2026-01-31 00:58:04 -05:00
>
< div className = "flex-1 min-w-0" >
< p className = "text-sm font-medium" > { key . name } < / p >
< div className = "flex items-center gap-2 mt-1" >
2026-02-27 23:07:51 -05:00
< code className = "text-xs text-text-muted font-mono" >
{ visibleKeys . has ( key . id ) ? key . key : maskKey ( key . key ) }
< / c o d e >
< button
onClick = { ( ) => toggleKeyVisibility ( key . id ) }
className = "p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-0 group-hover:opacity-100 transition-all"
title = { visibleKeys . has ( key . id ) ? "Hide key" : "Show key" }
>
< span className = "material-symbols-outlined text-[14px]" >
{ visibleKeys . has ( key . id ) ? "visibility_off" : "visibility" }
< / s p a n >
< / b u t t o n >
2026-01-31 00:58:04 -05:00
< button
onClick = { ( ) => copy ( key . key , key . id ) }
className = "p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-0 group-hover:opacity-100 transition-all"
>
< span className = "material-symbols-outlined text-[14px]" >
{ copied === key . id ? "check" : "content_copy" }
< / s p a n >
< / b u t t o n >
< / d i v >
< p className = "text-xs text-text-muted mt-1" >
Created { new Date ( key . createdAt ) . toLocaleDateString ( ) }
< / p >
2026-02-20 03:07:12 -05:00
{ key . isActive === false && (
< p className = "text-xs text-orange-500 mt-1" > Paused < / p >
) }
< / d i v >
< div className = "flex items-center gap-2" >
< Toggle
size = "sm"
checked = { key . isActive ? ? true }
onChange = { ( checked ) => {
if ( key . isActive && ! checked ) {
if ( confirm ( ` Pause API key " ${ key . name } "? \n \n This key will stop working immediately but can be resumed later. ` ) ) {
handleToggleKey ( key . id , checked ) ;
}
} else {
handleToggleKey ( key . id , checked ) ;
}
} }
title = { key . isActive ? "Pause key" : "Resume key" }
/ >
< button
onClick = { ( ) => handleDeleteKey ( key . id ) }
className = "p-2 hover:bg-red-500/10 rounded text-red-500 opacity-0 group-hover:opacity-100 transition-all"
>
< span className = "material-symbols-outlined text-[18px]" > delete < / s p a n >
< / b u t t o n >
2026-01-31 00:58:04 -05:00
< / d i v >
< / d i v >
) ) }
2026-01-04 21:58:59 -05:00
< / d i v >
) }
< / C a r d >
{ /* Add Key Modal */ }
< Modal
isOpen = { showAddModal }
title = "Create API Key"
onClose = { ( ) => {
setShowAddModal ( false ) ;
setNewKeyName ( "" ) ;
} }
>
< div className = "flex flex-col gap-4" >
< Input
label = "Key Name"
value = { newKeyName }
onChange = { ( e ) => setNewKeyName ( e . target . value ) }
placeholder = "Production Key"
/ >
< div className = "flex gap-2" >
< Button onClick = { handleCreateKey } fullWidth disabled = { ! newKeyName . trim ( ) } >
Create
< / B u t t o n >
< Button
onClick = { ( ) => {
setShowAddModal ( false ) ;
setNewKeyName ( "" ) ;
} }
variant = "ghost"
fullWidth
>
Cancel
< / B u t t o n >
< / d i v >
< / d i v >
< / M o d a l >
{ /* Created Key Modal */ }
< Modal
isOpen = { ! ! createdKey }
title = "API Key Created"
onClose = { ( ) => setCreatedKey ( null ) }
>
< div className = "flex flex-col gap-4" >
< div className = "bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 rounded-lg p-4" >
< p className = "text-sm text-yellow-800 dark:text-yellow-200 mb-2 font-medium" >
Save this key now !
< / p >
< p className = "text-sm text-yellow-700 dark:text-yellow-300" >
This is the only time you will see this key . Store it securely .
< / p >
< / d i v >
< div className = "flex gap-2" >
< Input
value = { createdKey || "" }
readOnly
className = "flex-1 font-mono text-sm"
/ >
< Button
variant = "secondary"
icon = { copied === "created_key" ? "check" : "content_copy" }
onClick = { ( ) => copy ( createdKey , "created_key" ) }
>
{ copied === "created_key" ? "Copied!" : "Copy" }
< / B u t t o n >
< / d i v >
< Button onClick = { ( ) => setCreatedKey ( null ) } fullWidth >
Done
< / B u t t o n >
< / d i v >
< / M o d a l >
2026-02-21 04:42:46 -05:00
{ /* Enable Tunnel Modal */ }
< Modal
2026-04-10 07:16:14 -04:00
isOpen = { showEnableTunnelModal }
2026-02-21 04:42:46 -05:00
title = "Enable Tunnel"
2026-04-10 07:16:14 -04:00
onClose = { ( ) => setShowEnableTunnelModal ( false ) }
2026-02-21 04:42:46 -05:00
>
< div className = "flex flex-col gap-4" >
< div className = "bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4" >
< div className = "flex items-start gap-3" >
< span className = "material-symbols-outlined text-blue-600 dark:text-blue-400" > cloud _upload < / s p a n >
< div >
< p className = "text-sm text-blue-800 dark:text-blue-200 font-medium mb-1" >
Cloudflare Tunnel
< / p >
< p className = "text-sm text-blue-700 dark:text-blue-300" >
Expose your local 9 Router to the internet . No port forwarding , no static IP needed . Share endpoint URL with your team or use it in Cursor , Cline , and other AI tools from anywhere .
< / p >
< / d i v >
< / d i v >
< / d i v >
< div className = "grid grid-cols-2 gap-3" >
{ TUNNEL _BENEFITS . map ( ( benefit ) => (
< div key = { benefit . title } className = "flex flex-col items-center text-center p-3 rounded-lg bg-sidebar/50" >
< span className = "material-symbols-outlined text-xl text-primary mb-1" > { benefit . icon } < / s p a n >
< p className = "text-xs font-semibold" > { benefit . title } < / p >
< p className = "text-xs text-text-muted" > { benefit . desc } < / p >
< / d i v >
) ) }
< / d i v >
< p className = "text-xs text-text-muted" >
Requires outbound port 7844 ( TCP / UDP ) . Connection may take 10 - 30 s .
< / p >
< div className = "flex gap-2" >
< Button
onClick = { handleEnableTunnel }
fullWidth
className = "bg-linear-to-r from-primary to-blue-500 hover:from-primary-hover hover:to-blue-600 text-white!"
>
Start Tunnel
< / B u t t o n >
2026-04-10 07:16:14 -04:00
< Button onClick = { ( ) => setShowEnableTunnelModal ( false ) } variant = "ghost" fullWidth > Cancel < / B u t t o n >
2026-02-21 04:42:46 -05:00
< / d i v >
< / d i v >
< / M o d a l >
2026-04-10 07:16:14 -04:00
{ /* Disable Cloudflare Tunnel Modal */ }
2026-01-04 21:58:59 -05:00
< Modal
2026-04-10 07:16:14 -04:00
isOpen = { showDisableTunnelModal }
2026-02-21 04:42:46 -05:00
title = "Disable Tunnel"
2026-04-10 07:16:14 -04:00
onClose = { ( ) => ! tunnelLoading && setShowDisableTunnelModal ( false ) }
2026-01-04 21:58:59 -05:00
>
< div className = "flex flex-col gap-4" >
2026-04-10 07:16:14 -04:00
< p className = "text-sm text-text-muted" > The Cloudflare tunnel will be disconnected . Remote access via tunnel URL will stop working . < / p >
< div className = "flex gap-2" >
< Button onClick = { handleDisableTunnel } fullWidth disabled = { tunnelLoading } className = "bg-red-500! hover:bg-red-600! text-white!" >
{ tunnelLoading ? "Disabling..." : "Disable" }
< / B u t t o n >
< Button onClick = { ( ) => setShowDisableTunnelModal ( false ) } variant = "ghost" fullWidth disabled = { tunnelLoading } > Cancel < / B u t t o n >
< / d i v >
< / d i v >
< / M o d a l >
{ /* Tailscale Modal */ }
< Modal
isOpen = { showTsModal }
title = "Tailscale Funnel"
onClose = { ( ) => { if ( ! tsInstalling ) { setShowTsModal ( false ) ; setTsSudoPassword ( "" ) ; setTsStatus ( null ) ; } } }
>
< div className = "flex flex-col gap-4" >
{ /* Checking state */ }
{ tsInstalled === null && (
< p className = "text-sm text-text-muted flex items-center gap-2" >
< span className = "material-symbols-outlined animate-spin text-sm" > progress _activity < / s p a n >
Checking ...
< / p >
) }
{ /* Not installed */ }
{ tsInstalled === false && ! tsInstalling && (
< div className = "flex flex-col gap-3" >
< p className = "text-sm text-text-muted" > Tailscale is not installed . Install it to enable Funnel . < / p >
< div className = "flex gap-2" >
< Button
onClick = { handleInstallTailscale }
fullWidth
className = "bg-linear-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 text-white!"
>
Install Tailscale
< / B u t t o n >
< Button onClick = { ( ) => setShowTsModal ( false ) } variant = "ghost" fullWidth > Cancel < / B u t t o n >
2026-01-04 21:58:59 -05:00
< / d i v >
< / d i v >
2026-04-10 07:16:14 -04:00
) }
{ /* Installing with progress log */ }
{ tsInstalling && (
< div className = "flex flex-col gap-2" >
< div className = "flex items-center gap-2 text-sm text-text-muted" >
< span className = "material-symbols-outlined animate-spin text-sm" > progress _activity < / s p a n >
Installing Tailscale ...
< / d i v >
{ tsInstallLog . length > 0 && (
< div ref = { tsLogRef } className = "bg-black/5 dark:bg-white/5 rounded p-2 max-h-40 overflow-y-auto font-mono text-xs text-text-muted" >
{ tsInstallLog . map ( ( line , i ) => (
< div key = { i } > { line } < / d i v >
) ) }
< / d i v >
) }
< / d i v >
) }
{ /* Installed: show Connect button */ }
{ tsInstalled === true && ! tsInstalling && (
< div className = "flex flex-col gap-3" >
< div className = "flex items-center gap-2 text-sm text-green-600 dark:text-green-400" >
< span className = "material-symbols-outlined text-[16px]" > check _circle < / s p a n >
Tailscale installed
< / d i v >
< div className = "flex gap-2" >
< Button
onClick = { ( ) => {
const tab = window . open ( "" , "tailscale_auth" , "width=600,height=700" ) ;
if ( tab ) tab . document . write ( "<p style='font-family:sans-serif;text-align:center;margin-top:40px'>Connecting to Tailscale...</p>" ) ;
handleConnectTailscale ( tab ) ;
} }
fullWidth
className = "bg-linear-to-r from-indigo-500 to-purple-500 hover:from-indigo-600 hover:to-purple-600 text-white!"
>
Connect
< / B u t t o n >
< Button onClick = { ( ) => setShowTsModal ( false ) } variant = "ghost" fullWidth > Cancel < / B u t t o n >
< / d i v >
< / d i v >
) }
2026-01-04 21:58:59 -05:00
2026-04-10 07:16:14 -04:00
{ tsStatus && < StatusAlert status = { tsStatus } / > }
< / d i v >
< / M o d a l >
2026-01-04 21:58:59 -05:00
2026-04-10 07:16:14 -04:00
{ /* Disable Tailscale Modal */ }
< Modal
isOpen = { showDisableTsModal }
title = "Disable Tailscale"
onClose = { ( ) => ! tsLoading && setShowDisableTsModal ( false ) }
>
< div className = "flex flex-col gap-4" >
< p className = "text-sm text-text-muted" > Tailscale Funnel will be stopped . Remote access via Tailscale URL will stop working . < / p >
2026-01-04 21:58:59 -05:00
< div className = "flex gap-2" >
2026-04-10 07:16:14 -04:00
< Button onClick = { handleDisableTailscale } fullWidth disabled = { tsLoading } className = "bg-red-500! hover:bg-red-600! text-white!" >
{ tsLoading ? "Disabling..." : "Disable" }
2026-01-04 21:58:59 -05:00
< / B u t t o n >
2026-04-10 07:16:14 -04:00
< Button onClick = { ( ) => setShowDisableTsModal ( false ) } variant = "ghost" fullWidth disabled = { tsLoading } > Cancel < / B u t t o n >
2026-01-04 21:58:59 -05:00
< / d i v >
< / d i v >
< / M o d a l >
< / d i v >
) ;
2026-01-19 05:15:11 -05:00
}
2026-04-10 07:16:14 -04:00
/** Reusable endpoint row component */
function EndpointRow ( { label , url , copyId , copied , onCopy , badge , actions } ) {
return (
< div className = "flex items-center gap-2" >
< span className = { ` text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[68px] text-center ${ badge === "CF" ? "bg-orange-100 dark:bg-orange-900/30 text-orange-600 dark:text-orange-400" :
badge === "TS" ? "bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400" :
"bg-sidebar text-text-muted"
} ` }>{label}</span>
< Input value = { url } readOnly className = "flex-1 font-mono text-sm" / >
< button
onClick = { ( ) => onCopy ( url , copyId ) }
className = "p-2 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary transition-colors shrink-0"
>
< span className = "material-symbols-outlined text-[18px]" > { copied === copyId ? "check" : "content_copy" } < / s p a n >
< / b u t t o n >
{ actions }
< / d i v >
) ;
}
/** Reusable status alert */
function StatusAlert ( { status , className = "" } ) {
// Render URLs in message as clickable links
const renderMessage = ( msg ) => {
const parts = msg . split ( /(https?:\/\/[^\s]+)/g ) ;
return parts . map ( ( part , i ) =>
/^https?:\/\// . test ( part )
? < a key = { i } href = { part } target = "_blank" rel = "noreferrer" className = "underline font-medium" > { part } < / a >
: part
) ;
} ;
return (
< div className = { ` p-2 rounded text-sm ${ className } ${ status . type === "success" ? "bg-green-500/10 text-green-600 dark:text-green-400" :
status . type === "warning" ? "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400" :
status . type === "info" ? "bg-blue-500/10 text-blue-600 dark:text-blue-400" :
"bg-red-500/10 text-red-600 dark:text-red-400"
} ` }>
{ renderMessage ( status . message ) }
< / d i v >
) ;
}
2026-04-11 03:56:35 -04:00
/** Inline tooltip, Claude Code CLI style */
function Tooltip ( { text } ) {
return (
< span className = "relative group inline-flex items-center" >
< span className = "material-symbols-outlined text-[14px] text-text-muted cursor-help" > help < / s p a n >
< span className = "pointer-events-none absolute left-5 top-1/2 -translate-y-1/2 z-50 w-64 rounded bg-gray-900 dark:bg-gray-800 text-white text-xs px-2.5 py-1.5 opacity-0 group-hover:opacity-100 transition-opacity shadow-lg" >
{ text }
< / s p a n >
< / s p a n >
) ;
}
/** Security warning banner with optional action link */
function SecurityWarning ( { message , action } ) {
return (
< div className = "flex items-center gap-2 px-3 py-2 rounded-lg bg-amber-500/10 border border-amber-500/20 text-amber-700 dark:text-amber-400" >
< span className = "material-symbols-outlined text-[16px] shrink-0 mt-0.5" > warning < / s p a n >
< p className = "text-xs flex-1" > { message } < / p >
{ action && (
< a
href = { action . href }
className = "text-xs font-medium underline shrink-0 hover:opacity-80"
onClick = { action . href . startsWith ( "#" ) ? ( e ) => {
e . preventDefault ( ) ;
document . getElementById ( action . href . slice ( 1 ) ) ? . scrollIntoView ( { behavior : "smooth" } ) ;
} : undefined }
>
{ action . label }
< / a >
) }
< / d i v >
) ;
}
2026-01-19 05:15:11 -05:00
APIPageClient . propTypes = {
2026-01-20 01:16:34 -05:00
machineId : PropTypes . string . isRequired ,
2026-02-08 04:45:31 -05:00
} ;