2026-01-04 21:58:59 -05:00
"use client" ;
2026-05-09 06:48:20 -04:00
import { useState , useEffect , useRef , useCallback } from "react" ;
2026-01-20 01:16:34 -05:00
import PropTypes from "prop-types" ;
2026-05-13 04:32:17 -04:00
import { Card , Button , Input , Modal , CardSkeleton , Toggle , ConfirmModal } 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-05-16 01:38:06 -04:00
const STATUS _POLL _FAST _MS = 5000 ;
const STATUS _POLL _SLOW _MS = 30000 ;
2026-05-09 11:48:07 -04:00
const REACHABLE _MISS _THRESHOLD = 5 ;
2026-05-16 01:38:06 -04:00
const CLIENT _PING _FAST _MS = 10000 ;
const CLIENT _PING _SLOW _MS = 60000 ;
2026-05-09 11:48:07 -04:00
const CLIENT _PING _TIMEOUT _MS = 5000 ;
2026-05-25 05:15:34 -04:00
// Browser-side health probe: must reach origin (not just CF/TS edge).
// cors mode → res.ok=false for 5xx (e.g. Cloudflare 530 when origin dead).
// /api/health route sets Access-Control-Allow-Origin: * → CORS works through tunnel.
2026-05-09 11:48:07 -04:00
async function clientPingUrl ( url ) {
if ( ! url ) return false ;
try {
2026-05-21 03:30:59 -04:00
const res = await fetch ( ` ${ url } /api/health ` , {
mode : "cors" ,
2026-05-09 11:48:07 -04:00
cache : "no-store" ,
signal : AbortSignal . timeout ( CLIENT _PING _TIMEOUT _MS ) ,
} ) ;
2026-05-21 03:30:59 -04:00
return res . ok ;
2026-05-09 11:48:07 -04:00
} catch { return false ; }
}
2026-04-30 07:00:38 -04:00
2026-05-21 00:28:49 -04:00
// Race multiple URLs: resolve true as soon as any one passes ping.
async function clientPingAny ( ... urls ) {
const checks = urls . filter ( Boolean ) . map ( clientPingUrl ) ;
if ( ! checks . length ) return false ;
return new Promise ( ( resolve ) => {
let pending = checks . length ;
checks . forEach ( ( p ) => p . then ( ( ok ) => {
if ( ok ) resolve ( true ) ;
else if ( -- pending === 0 ) resolve ( false ) ;
} ) ) ;
} ) ;
}
2026-04-30 07:00:38 -04:00
const CAVEMAN _LEVELS = [
{ id : "lite" , label : "Lite" , desc : "Drop filler, keep grammar" } ,
{ id : "full" , label : "Full" , desc : "Drop articles, fragments OK" } ,
{ id : "ultra" , label : "Ultra" , desc : "Telegraphic, max compression" } ,
2026-06-06 01:45:32 -04:00
{ id : "wenyan-lite" , label : "Zip Lite" , desc : "Compact classical style, light compression" } ,
{ id : "wenyan" , label : "Zip Full" , desc : "Classical Chinese, 80-90% token reduction" } ,
{ id : "wenyan-ultra" , label : "Zip Max" , desc : "Extreme classical compression" } ,
2026-04-30 07:00:38 -04:00
] ;
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-05-13 04:32:17 -04:00
const [ confirmState , setConfirmState ] = useState ( null ) ;
2026-01-04 21:58:59 -05:00
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-28 00:07:39 -04:00
const [ rtkEnabled , setRtkEnabledState ] = useState ( true ) ;
2026-04-30 07:00:38 -04:00
const [ cavemanEnabled , setCavemanEnabled ] = useState ( false ) ;
const [ cavemanLevel , setCavemanLevel ] = useState ( "full" ) ;
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 ) ;
2026-05-09 06:48:20 -04:00
const [ tunnelReachable , setTunnelReachable ] = useState ( false ) ;
2026-02-21 04:42:46 -05:00
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 ) ;
2026-05-09 06:48:20 -04:00
const [ tsReachable , setTsReachable ] = useState ( false ) ;
2026-04-10 07:16:14 -04:00
const [ tsUrl , setTsUrl ] = useState ( "" ) ;
const [ tsLoading , setTsLoading ] = useState ( false ) ;
const [ tsProgress , setTsProgress ] = useState ( "" ) ;
const [ tsStatus , setTsStatus ] = useState ( null ) ;
2026-05-09 21:44:14 -04:00
const [ tsAuthUrl , setTsAuthUrl ] = useState ( "" ) ;
const [ tsAuthLabel , setTsAuthLabel ] = useState ( "" ) ;
2026-04-10 07:16:14 -04:00
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-05-09 06:48:20 -04:00
// Debounce reachable=false: server may briefly return false during background refresh.
// Only flip UI to "reconnecting" after N consecutive misses to avoid spinner flicker.
const tunnelMissRef = useRef ( 0 ) ;
const tsMissRef = useRef ( 0 ) ;
2026-05-09 11:48:07 -04:00
// Browser-side reachable cache (independent of backend DNS quirks)
const tunnelClientReachableRef = useRef ( false ) ;
const tsClientReachableRef = useRef ( false ) ;
2026-05-09 06:48:20 -04:00
// Track whether reachable=true was ever observed in this session.
// Distinguishes "Checking..." (initial cold cache) from "Reconnecting..." (lost connection).
const tunnelEverReachableRef = useRef ( false ) ;
const tsEverReachableRef = useRef ( false ) ;
const [ tunnelEverReachable , setTunnelEverReachable ] = useState ( false ) ;
const [ tsEverReachable , setTsEverReachable ] = useState ( false ) ;
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
2026-06-06 01:35:08 -04:00
// Client-side local/remote detection (UI hint only, not a security gate)
const [ isRemoteHost , setIsRemoteHost ] = useState ( false ) ;
useEffect ( ( ) => {
if ( typeof window !== "undefined" )
setIsRemoteHost ( ! [ "localhost" , "127.0.0.1" , "::1" ] . includes ( window . location . hostname ) ) ;
} , [ ] ) ;
2026-01-04 21:58:59 -05:00
const { copied , copy } = useCopyToClipboard ( ) ;
2026-05-19 23:05:22 -04:00
// Security gate: block remote exposure while dashboard uses default password or login is off.
const isLoginUnsafe = ! requireLogin || ! hasPassword ;
const unsafeReason = ! requireLogin
? "Enable \"Require login\" and set a custom password before activating the tunnel."
: "Change the default dashboard password before activating the tunnel." ;
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-05-16 01:38:06 -04:00
} , [ ] ) ;
2026-05-21 00:28:49 -04:00
// Status poll: only while degraded (not yet reachable). Stop once healthy to avoid spam.
// Visibility re-check: refresh once when tab becomes visible.
2026-05-16 01:38:06 -04:00
useEffect ( ( ) => {
const anyEnabled = tunnelEnabled || tsEnabled ;
if ( ! anyEnabled ) return ;
const tunnelHealthy = ! tunnelEnabled || tunnelReachable ;
const tsHealthy = ! tsEnabled || tsReachable ;
const allHealthy = tunnelHealthy && tsHealthy ;
2026-05-03 11:10:03 -04:00
const onVisible = ( ) => { if ( ! document . hidden ) syncTunnelStatus ( ) ; } ;
document . addEventListener ( "visibilitychange" , onVisible ) ;
2026-05-21 00:28:49 -04:00
if ( allHealthy ) return ( ) => document . removeEventListener ( "visibilitychange" , onVisible ) ;
const timer = setInterval ( ( ) => { if ( ! document . hidden ) syncTunnelStatus ( ) ; } , STATUS _POLL _FAST _MS ) ;
2026-05-03 11:10:03 -04:00
return ( ) => {
2026-05-21 00:28:49 -04:00
clearInterval ( timer ) ;
2026-05-03 11:10:03 -04:00
document . removeEventListener ( "visibilitychange" , onVisible ) ;
} ;
2026-05-16 01:38:06 -04:00
} , [ tunnelEnabled , tsEnabled , tunnelReachable , tsReachable ] ) ;
2026-01-04 21:58:59 -05:00
2026-05-09 11:48:07 -04:00
// Browser-side periodic ping: probes tunnel/tailscale URLs directly so UI stays
// "reachable" even when backend DNS (1.1.1.1) hiccups on *.ts.net or *.trycloudflare.com.
2026-05-16 01:38:06 -04:00
// Adaptive: slow when healthy, fast when degraded; pause when tab hidden.
2026-05-09 11:48:07 -04:00
useEffect ( ( ) => {
const probeBoth = async ( ) => {
2026-05-16 01:38:06 -04:00
if ( document . hidden ) return ;
2026-05-21 00:28:49 -04:00
if ( tunnelEnabled && ( tunnelUrl || tunnelPublicUrl ) ) {
const ok = await clientPingAny ( tunnelPublicUrl , tunnelUrl ) ;
2026-05-09 11:48:07 -04:00
tunnelClientReachableRef . current = ok ;
if ( ok ) { tunnelMissRef . current = 0 ; setTunnelReachable ( true ) ; if ( ! tunnelEverReachableRef . current ) { tunnelEverReachableRef . current = true ; setTunnelEverReachable ( true ) ; } }
2026-05-21 03:30:59 -04:00
else { tunnelMissRef . current += 1 ; if ( tunnelMissRef . current >= REACHABLE _MISS _THRESHOLD ) setTunnelReachable ( false ) ; }
2026-05-09 11:48:07 -04:00
} else {
tunnelClientReachableRef . current = false ;
}
if ( tsEnabled && tsUrl ) {
const ok = await clientPingUrl ( tsUrl ) ;
tsClientReachableRef . current = ok ;
if ( ok ) { tsMissRef . current = 0 ; setTsReachable ( true ) ; if ( ! tsEverReachableRef . current ) { tsEverReachableRef . current = true ; setTsEverReachable ( true ) ; } }
2026-05-21 03:30:59 -04:00
else { tsMissRef . current += 1 ; if ( tsMissRef . current >= REACHABLE _MISS _THRESHOLD ) setTsReachable ( false ) ; }
2026-05-09 11:48:07 -04:00
} else {
tsClientReachableRef . current = false ;
}
} ;
2026-05-21 00:28:49 -04:00
const anyEnabled = ( tunnelEnabled && ( tunnelUrl || tunnelPublicUrl ) ) || ( tsEnabled && tsUrl ) ;
2026-05-16 01:38:06 -04:00
if ( ! anyEnabled ) return ;
2026-05-09 11:48:07 -04:00
probeBoth ( ) ;
2026-05-16 01:38:06 -04:00
const tunnelHealthy = ! tunnelEnabled || tunnelReachable ;
const tsHealthy = ! tsEnabled || tsReachable ;
2026-05-21 00:28:49 -04:00
if ( tunnelHealthy && tsHealthy ) return ;
const id = setInterval ( probeBoth , CLIENT _PING _FAST _MS ) ;
2026-05-09 11:48:07 -04:00
return ( ) => clearInterval ( id ) ;
2026-05-21 00:28:49 -04:00
} , [ tunnelEnabled , tunnelUrl , tunnelPublicUrl , tsEnabled , tsUrl , tunnelReachable , tsReachable ] ) ;
2026-05-09 11:48:07 -04:00
2026-05-21 00:28:49 -04:00
// Client-side reachable only (server no longer probes; watchdog handles backend health).
// Miss-debounce: only flip to false after N consecutive misses.
const updateReachable = useCallback ( ( _unused , clientRef , missRef , setter , everRef , everSetter ) => {
const reachable = clientRef . current ;
2026-05-09 06:48:20 -04:00
if ( reachable ) {
missRef . current = 0 ;
setter ( true ) ;
if ( ! everRef . current ) {
everRef . current = true ;
everSetter ( true ) ;
}
} else {
missRef . current += 1 ;
if ( missRef . current >= REACHABLE _MISS _THRESHOLD ) setter ( false ) ;
}
} , [ ] ) ;
2026-05-03 11:10:03 -04:00
// Trust user intent (settingsEnabled): UI stays "enabled" while watchdog restarts process
const syncTunnelStatus = async ( ) => {
try {
const statusRes = await fetch ( "/api/tunnel/status" , { cache : "no-store" } ) ;
if ( ! statusRes . ok ) return ;
const data = await statusRes . json ( ) ;
const tEnabled = data . tunnel ? . settingsEnabled ? ? data . tunnel ? . enabled ? ? false ;
const tUrl = data . tunnel ? . tunnelUrl || "" ;
setTunnelUrl ( tUrl ) ;
2026-05-15 07:22:10 -04:00
setTunnelPublicUrl ( data . tunnel ? . publicUrl || "" ) ;
2026-05-03 11:10:03 -04:00
setTunnelEnabled ( tEnabled ) ;
2026-05-21 00:28:49 -04:00
updateReachable ( null , tunnelClientReachableRef , tunnelMissRef , setTunnelReachable , tunnelEverReachableRef , setTunnelEverReachable ) ;
2026-05-03 11:10:03 -04:00
const tsEn = data . tailscale ? . settingsEnabled ? ? data . tailscale ? . enabled ? ? false ;
const tsUrlVal = data . tailscale ? . tunnelUrl || "" ;
setTsUrl ( tsUrlVal ) ;
setTsEnabled ( tsEn ) ;
2026-05-21 00:28:49 -04:00
updateReachable ( null , tsClientReachableRef , tsMissRef , setTsReachable , tsEverReachableRef , setTsEverReachable ) ;
2026-05-03 11:10:03 -04:00
} catch { /* ignore poll errors */ }
} ;
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" ) ,
2026-05-03 11:10:03 -04:00
fetch ( "/api/tunnel/status" , { cache : "no-store" } )
2026-02-21 04:42:46 -05:00
] ) ;
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-04-28 00:07:39 -04:00
setRtkEnabledState ( data . rtkEnabled !== false ) ;
2026-04-30 07:00:38 -04:00
setCavemanEnabled ( ! ! data . cavemanEnabled ) ;
setCavemanLevel ( data . cavemanLevel || "full" ) ;
2026-02-21 04:42:46 -05:00
}
2026-04-10 07:16:14 -04:00
if ( statusRes . ok ) {
const data = await statusRes . json ( ) ;
2026-05-03 11:10:03 -04:00
const tEnabled = data . tunnel ? . settingsEnabled ? ? data . tunnel ? . enabled ? ? false ;
2026-04-10 07:16:14 -04:00
const tUrl = data . tunnel ? . tunnelUrl || "" ;
setTunnelUrl ( tUrl ) ;
2026-05-15 07:22:10 -04:00
setTunnelPublicUrl ( data . tunnel ? . publicUrl || "" ) ;
2026-05-03 11:10:03 -04:00
setTunnelEnabled ( tEnabled ) ;
2026-05-21 00:28:49 -04:00
updateReachable ( null , tunnelClientReachableRef , tunnelMissRef , setTunnelReachable , tunnelEverReachableRef , setTunnelEverReachable ) ;
2026-05-03 11:10:03 -04:00
const tsEn = data . tailscale ? . settingsEnabled ? ? data . tailscale ? . enabled ? ? false ;
2026-04-10 07:16:14 -04:00
const tsUrlVal = data . tailscale ? . tunnelUrl || "" ;
setTsUrl ( tsUrlVal ) ;
2026-05-03 11:10:03 -04:00
setTsEnabled ( tsEn ) ;
2026-05-21 00:28:49 -04:00
updateReachable ( null , tsClientReachableRef , tsMissRef , setTsReachable , tsEverReachableRef , setTsEverReachable ) ;
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 ) ;
}
} ;
2026-04-22 04:36:51 -04:00
const handleRtkEnabled = async ( value ) => {
try {
const res = await fetch ( "/api/settings" , {
method : "PATCH" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( { rtkEnabled : value } ) ,
} ) ;
if ( res . ok ) setRtkEnabledState ( value ) ;
} catch ( error ) {
console . log ( "Error updating rtkEnabled:" , error ) ;
}
} ;
2026-04-30 07:00:38 -04:00
const patchSetting = async ( patch ) => {
try {
await fetch ( "/api/settings" , {
method : "PATCH" ,
headers : { "Content-Type" : "application/json" } ,
body : JSON . stringify ( patch ) ,
} ) ;
} catch ( error ) {
console . log ( "Error updating setting:" , error ) ;
}
} ;
const handleCavemanEnabled = ( value ) => {
setCavemanEnabled ( value ) ;
patchSetting ( { cavemanEnabled : value } ) ;
} ;
const handleCavemanLevel = ( level ) => {
setCavemanLevel ( level ) ;
patchSetting ( { cavemanLevel : level } ) ;
} ;
2026-02-21 04:42:46 -05:00
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
2026-05-21 00:28:49 -04:00
// Ping tunnel health until reachable. Race multiple URLs (shortlink + direct) — 1 OK is enough.
const pingTunnelHealth = async ( ... urls ) => {
2026-04-10 07:16:14 -04:00
setTunnelLoading ( true ) ;
setTunnelProgress ( "Waiting for tunnel ready..." ) ;
2026-05-21 00:28:49 -04:00
const targets = urls . filter ( Boolean ) . map ( ( u ) => ` ${ u } /api/health ` ) ;
2026-04-10 07:16:14 -04:00
const start = Date . now ( ) ;
while ( Date . now ( ) - start < TUNNEL _PING _MAX _MS ) {
await new Promise ( ( r ) => setTimeout ( r , TUNNEL _PING _INTERVAL _MS ) ) ;
2026-05-21 00:28:49 -04:00
const ok = await Promise . any ( targets . map ( async ( h ) => {
2026-05-21 03:30:59 -04:00
const p = await fetch ( h , { mode : "cors" , cache : "no-store" } ) ;
if ( p . ok ) return true ;
2026-05-21 00:28:49 -04:00
throw new Error ( "not ready" ) ;
} ) ) . catch ( ( ) => false ) ;
if ( ok ) {
setTunnelEnabled ( true ) ;
setTunnelLoading ( false ) ;
setTunnelProgress ( "" ) ;
return true ;
}
2026-04-10 07:16:14 -04:00
// 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 ;
}
2026-05-15 07:22:10 -04:00
const url = data . tunnelUrl ;
2026-04-10 07:16:14 -04:00
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
2026-05-15 07:22:10 -04:00
setTunnelUrl ( url ) ;
2026-04-10 07:16:14 -04:00
setTunnelPublicUrl ( data . publicUrl || "" ) ;
2026-05-21 00:28:49 -04:00
await pingTunnelHealth ( data . publicUrl , 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-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 ) ;
2026-05-10 10:54:54 -04:00
setShowTsModal ( false ) ;
handleConnectTailscale ( ) ;
2026-04-10 07:16:14 -04:00
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 ;
} ;
2026-05-09 21:44:14 -04:00
// Show inline login button instead of auto-opening popup (browsers block popups
// opened after async work because the user gesture is lost).
const requestUserAuth = ( url , label ) => {
setTsAuthUrl ( url ) ;
setTsAuthLabel ( label ) ;
} ;
const clearUserAuth = ( ) => {
setTsAuthUrl ( "" ) ;
setTsAuthLabel ( "" ) ;
2026-05-09 06:48:20 -04:00
} ;
const handleConnectTailscale = async ( ) => {
2026-04-10 07:16:14 -04:00
setShowTsModal ( false ) ;
setTsConnecting ( true ) ;
setTsLoading ( true ) ;
setTsStatus ( null ) ;
setTsProgress ( "Connecting..." ) ;
2026-05-09 21:44:14 -04:00
clearUserAuth ( ) ;
2026-04-10 07:16:14 -04:00
try {
const res = await fetch ( "/api/tunnel/tailscale-enable" , { method : "POST" } ) ;
const data = await res . json ( ) ;
if ( res . ok && data . success ) {
setTsUrl ( data . tunnelUrl || "" ) ;
const reachable = await pingTsHealth ( data . tunnelUrl ) ;
2026-05-09 06:48:20 -04:00
setTsEnabled ( true ) ;
setTsStatus ( reachable ? null : { type : "warning" , message : "Connected but not reachable yet." } ) ;
2026-04-10 07:16:14 -04:00
return ;
}
if ( data . needsLogin && data . authUrl ) {
2026-05-09 21:44:14 -04:00
requestUserAuth ( data . authUrl , "Open Login Page" ) ;
setTsProgress ( "Login required — click \"Open Login Page\" to continue" ) ;
2026-04-10 07:16:14 -04:00
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 ) {
2026-05-09 21:44:14 -04:00
clearUserAuth ( ) ;
2026-04-10 07:16:14 -04:00
setTsProgress ( "Starting funnel..." ) ;
const res2 = await fetch ( "/api/tunnel/tailscale-enable" , { method : "POST" } ) ;
const data2 = await res2 . json ( ) ;
if ( res2 . ok && data2 . success ) {
setTsUrl ( data2 . tunnelUrl || "" ) ;
const ok2 = await pingTsHealth ( data2 . tunnelUrl ) ;
2026-05-09 06:48:20 -04:00
setTsEnabled ( true ) ;
setTsStatus ( ok2 ? null : { type : "warning" , message : "Connected but not reachable yet." } ) ;
2026-04-10 07:16:14 -04:00
} else if ( data2 . funnelNotEnabled && data2 . enableUrl ) {
2026-05-09 06:48:20 -04:00
await pollFunnelEnable ( data2 . enableUrl ) ;
2026-04-10 07:16:14 -04:00
} else {
setTsStatus ( { type : "error" , message : data2 . error || "Failed to start funnel" } ) ;
}
return ;
}
}
} catch { /* retry */ }
}
2026-05-09 21:44:14 -04:00
clearUserAuth ( ) ;
2026-04-10 07:16:14 -04:00
setTsStatus ( { type : "error" , message : "Login timed out. Please try again." } ) ;
return ;
}
if ( data . funnelNotEnabled && data . enableUrl ) {
2026-05-09 06:48:20 -04:00
await pollFunnelEnable ( data . enableUrl ) ;
2026-04-10 07:16:14 -04:00
return ;
}
setTsStatus ( { type : "error" , message : data . error || "Failed to connect" } ) ;
} catch ( error ) {
setTsStatus ( { type : "error" , message : error . message } ) ;
} finally {
setTsLoading ( false ) ;
setTsConnecting ( false ) ;
setTsProgress ( "" ) ;
2026-05-09 21:44:14 -04:00
clearUserAuth ( ) ;
2026-04-10 07:16:14 -04:00
}
} ;
2026-05-09 06:48:20 -04:00
const pollFunnelEnable = async ( enableUrl ) => {
2026-05-09 21:44:14 -04:00
requestUserAuth ( enableUrl , "Open Funnel Settings" ) ;
setTsProgress ( "Click \"Open Funnel Settings\" to enable Funnel..." ) ;
2026-04-10 07:16:14 -04:00
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 ) {
2026-05-09 21:44:14 -04:00
clearUserAuth ( ) ;
2026-04-10 07:16:14 -04:00
setTsUrl ( data . tunnelUrl || "" ) ;
const ok3 = await pingTsHealth ( data . tunnelUrl ) ;
2026-05-09 06:48:20 -04:00
setTsEnabled ( true ) ;
setTsStatus ( ok3 ? null : { type : "warning" , message : "Connected but not reachable yet." } ) ;
2026-04-10 07:16:14 -04:00
return ;
}
if ( data . funnelNotEnabled ) continue ;
if ( data . error ) {
2026-05-09 21:44:14 -04:00
clearUserAuth ( ) ;
2026-04-10 07:16:14 -04:00
setTsStatus ( { type : "error" , message : data . error } ) ;
return ;
}
} catch { /* retry */ }
}
2026-05-09 21:44:14 -04:00
clearUserAuth ( ) ;
2026-04-10 07:16:14 -04:00
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 ( [ ] ) ;
2026-05-09 21:44:14 -04:00
const data = await checkTailscaleInstalled ( ) ;
2026-05-10 10:54:54 -04:00
if ( data ? . installed && data ? . hasCachedPassword ) {
2026-05-09 21:44:14 -04:00
handleConnectTailscale ( ) ;
} else {
setShowTsModal ( true ) ;
}
2026-04-10 07:16:14 -04:00
} ;
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 ) => {
2026-05-13 04:32:17 -04:00
setConfirmState ( {
title : "Delete API Key" ,
message : "Delete this API key?" ,
onConfirm : async ( ) => {
setConfirmState ( null ) ;
try {
const res = await fetch ( ` /api/keys/ ${ id } ` , { method : "DELETE" } ) ;
if ( res . ok ) {
setKeys ( keys . filter ( ( k ) => k . id !== id ) ) ;
setVisibleKeys ( prev => {
const next = new Set ( prev ) ;
next . delete ( id ) ;
return next ;
} ) ;
}
} catch ( error ) {
console . log ( "Error deleting key:" , error ) ;
}
2026-01-04 21:58:59 -05:00
}
2026-05-13 04:32:17 -04:00
} ) ;
2026-01-04 21:58:59 -05:00
} ;
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-05-04 00:29:02 -04:00
< h2 className = "text-lg font-semibold mb-4 flex items-center gap-2" >
< span className = "material-symbols-outlined text-primary" > api < / s p a n >
API Endpoint
< / h 2 >
2026-04-10 07:16:14 -04:00
{ /* 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-05-03 07:00:35 -04:00
< span className = { ` text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${
tunnelEnabled ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"
2026-04-10 07:16:14 -04:00
} ` }>Tunnel</span>
2026-05-09 06:48:20 -04:00
{ tunnelEnabled && ! tunnelLoading && tunnelReachable ? (
2026-04-10 07:16:14 -04:00
< >
< 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 >
< / >
2026-05-09 06:48:20 -04:00
) : tunnelEnabled && ! tunnelLoading && ! tunnelReachable ? (
< >
< div className = "flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-amber-300 dark:border-amber-800 bg-amber-500/5 text-sm text-amber-600 dark:text-amber-400" >
< span className = "material-symbols-outlined animate-spin text-sm" > progress _activity < / s p a n >
{ tunnelEverReachable ? "Tunnel reconnecting..." : "Tunnel checking..." }
< / d i v >
< 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 >
< / >
2026-04-10 07:16:14 -04:00
) : 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 = { ( ) => {
2026-05-19 23:05:22 -04:00
if ( isLoginUnsafe ) {
setTunnelStatus ( { type : "error" , message : ` Security required: ${ unsafeReason } ` } ) ;
return ;
}
2026-04-04 12:24:24 -04:00
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-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" >
2026-05-03 07:00:35 -04:00
< span className = { ` text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${
tsEnabled ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"
2026-04-10 07:16:14 -04:00
} ` }>Tailscale</span>
2026-05-09 06:48:20 -04:00
{ tsEnabled && ! tsLoading && tsReachable ? (
2026-04-10 07:16:14 -04:00
< >
< 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 >
< / >
2026-05-09 06:48:20 -04:00
) : tsEnabled && ! tsLoading && ! tsReachable ? (
< >
< div className = "flex-1 flex items-center gap-2 px-3 py-1.5 rounded border border-amber-300 dark:border-amber-800 bg-amber-500/5 text-sm text-amber-600 dark:text-amber-400" >
< span className = "material-symbols-outlined animate-spin text-sm" > progress _activity < / s p a n >
{ tsEverReachable ? "Tailscale reconnecting..." : "Tailscale checking..." }
< / d i v >
< 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 >
< / >
2026-04-10 07:16:14 -04:00
) : ( 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 >
2026-05-09 21:44:14 -04:00
{ tsAuthUrl && (
< Button
size = "sm"
icon = "open_in_new"
onClick = { ( ) => window . open ( tsAuthUrl , "tailscale_auth" , "width=600,height=700,noopener,noreferrer" ) }
>
{ tsAuthLabel || "Open" }
< / B u t t o n >
) }
2026-04-13 04:52:01 -04:00
< button
2026-05-09 21:44:14 -04:00
onClick = { ( ) => { setTsLoading ( false ) ; setTsConnecting ( false ) ; setTsProgress ( "" ) ; clearUserAuth ( ) ; } }
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"
2026-05-19 23:05:22 -04:00
onClick = { ( ) => {
if ( isLoginUnsafe ) {
setTsStatus ( { type : "error" , message : ` Security required: ${ unsafeReason } ` } ) ;
return ;
}
handleOpenTsModal ( ) ;
} }
2026-04-10 07:16:14 -04:00
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
2026-05-19 23:05:22 -04:00
{ /* Pre-enable security gate banner */ }
{ isLoginUnsafe && ! tunnelEnabled && ! tsEnabled && (
< div className = "mt-4" >
< SecurityWarning
message = { unsafeReason }
action = { { label : "Open settings" , href : "/dashboard/profile" } }
/ >
< / 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 >
2026-04-30 07:00:38 -04:00
{ /* Token Saver (RTK + Caveman) */ }
2026-04-22 04:36:51 -04:00
< Card id = "rtk" >
< div className = "flex items-center justify-between mb-2" >
2026-05-04 00:29:02 -04:00
< h2 className = "text-lg font-semibold flex items-center gap-2" >
< span className = "material-symbols-outlined text-primary" > bolt < / s p a n >
Token Saver
< / h 2 >
2026-04-22 04:36:51 -04:00
< / d i v >
2026-05-03 07:00:35 -04:00
< div className = "flex items-center justify-between pt-2 pb-4 border-b border-border gap-4" >
< div className = "min-w-0 flex-1" >
2026-04-30 07:00:38 -04:00
< p className = "font-medium" >
Compress tool output { " " }
< a
href = "https://github.com/rtk-ai/rtk"
target = "_blank"
rel = "noreferrer"
className = "text-xs font-normal text-primary underline hover:opacity-80"
>
( RTK )
< / a >
< / p >
2026-04-22 04:36:51 -04:00
< p className = "text-sm text-text-muted" >
2026-05-03 07:00:35 -04:00
git / grep / ls / tree / logs → 60 - 90 % fewer input tokens
2026-04-22 04:36:51 -04:00
< / p >
< / d i v >
< Toggle
checked = { rtkEnabled }
onChange = { ( ) => handleRtkEnabled ( ! rtkEnabled ) }
/ >
< / d i v >
2026-05-03 07:00:35 -04:00
< div className = "flex items-center justify-between pt-4 gap-4 flex-wrap" >
< div className = "min-w-0 flex-1" >
2026-04-30 07:00:38 -04:00
< p className = "font-medium" >
Compress LLM output { " " }
< a
href = "https://github.com/JuliusBrussee/caveman"
target = "_blank"
rel = "noreferrer"
className = "text-xs font-normal text-primary underline hover:opacity-80"
>
( Caveman )
< / a >
< / p >
< p className = "text-sm text-text-muted" >
2026-05-03 07:00:35 -04:00
Terse - style system prompt → ~ 65 % fewer output tokens ( up to 87 % )
2026-04-30 07:00:38 -04:00
< / p >
< / d i v >
2026-05-03 07:00:35 -04:00
< div className = "flex items-center gap-3 shrink-0" >
{ cavemanEnabled && (
2026-06-06 01:45:32 -04:00
< div className = "flex flex-col items-end gap-1" >
< div className = "flex items-center gap-1.5" >
{ CAVEMAN _LEVELS . map ( ( lvl ) => (
< button
key = { lvl . id }
onClick = { ( ) => handleCavemanLevel ( lvl . id ) }
className = { ` px-3 py-1.5 rounded text-xs font-medium border transition-colors ${
cavemanLevel === lvl . id
? "bg-primary text-white border-primary"
: "bg-transparent border-border text-text-muted hover:bg-surface-2"
} ` }
title = { lvl . desc }
>
{ lvl . label }
< / b u t t o n >
) ) }
< / d i v >
< p className = "text-xs text-primary" >
{ CAVEMAN _LEVELS . find ( ( lvl ) => lvl . id === cavemanLevel ) ? . desc }
< / p >
2026-05-03 07:00:35 -04:00
< / d i v >
) }
< Toggle
checked = { cavemanEnabled }
onChange = { ( ) => handleCavemanEnabled ( ! cavemanEnabled ) }
/ >
2026-04-30 07:00:38 -04:00
< / d i v >
2026-05-03 07:00:35 -04:00
< / d i v >
2026-04-22 04:36:51 -04:00
< / C a r d >
2026-01-04 21:58:59 -05:00
{ /* 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" >
2026-05-04 00:29:02 -04:00
< h2 className = "text-lg font-semibold flex items-center gap-2" >
< span className = "material-symbols-outlined text-primary" > vpn _key < / s p a n >
API Keys
< / h 2 >
2026-01-04 21:58:59 -05:00
< 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-06-06 01:35:08 -04:00
{ isRemoteHost && ! requireApiKey && (
< div className = "mb-4 -mt-2" >
< SecurityWarning message = "Endpoint is exposed without an API key." / >
< / 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 ) }
2026-05-14 00:06:22 -04:00
className = "p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all"
2026-02-27 23:07:51 -05:00
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 ) }
2026-05-14 00:06:22 -04:00
className = "p-1 hover:bg-black/5 dark:hover:bg-white/5 rounded text-text-muted hover:text-primary opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all"
2026-01-31 00:58:04 -05:00
>
< 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 ) {
2026-05-13 04:32:17 -04:00
setConfirmState ( {
title : "Pause API Key" ,
message : ` Pause API key " ${ key . name } "? \n \n This key will stop working immediately but can be resumed later. ` ,
onConfirm : async ( ) => {
setConfirmState ( null ) ;
handleToggleKey ( key . id , checked ) ;
}
} ) ;
2026-02-20 03:07:12 -05:00
} else {
handleToggleKey ( key . id , checked ) ;
}
} }
title = { key . isActive ? "Pause key" : "Resume key" }
/ >
< button
onClick = { ( ) => handleDeleteKey ( key . id ) }
2026-05-14 00:06:22 -04:00
className = "p-2 hover:bg-red-500/10 rounded text-red-500 opacity-100 sm:opacity-0 sm:group-hover:opacity-100 transition-all"
2026-02-20 03:07:12 -05:00
>
< 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" >
2026-05-03 07:00:35 -04:00
< div className = "bg-surface-2 border border-border-subtle rounded-lg p-4" >
2026-02-21 04:42:46 -05:00
< div className = "flex items-start gap-3" >
2026-05-03 07:00:35 -04:00
< span className = "material-symbols-outlined text-primary" > cloud _upload < / s p a n >
2026-02-21 04:42:46 -05:00
< div >
2026-05-03 07:00:35 -04:00
< p className = "text-sm text-text-main font-medium mb-1" >
2026-02-21 04:42:46 -05:00
Cloudflare Tunnel
< / p >
2026-05-03 07:00:35 -04:00
< p className = "text-sm text-text-muted" >
2026-02-21 04:42:46 -05:00
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" >
2026-05-03 07:00:35 -04:00
< Button onClick = { handleEnableTunnel } fullWidth >
2026-02-21 04:42:46 -05:00
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" >
2026-05-03 07:00:35 -04:00
< Button onClick = { handleDisableTunnel } fullWidth disabled = { tunnelLoading } variant = "danger" >
2026-04-10 07:16:14 -04:00
{ 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" >
2026-05-03 07:00:35 -04:00
< Button onClick = { handleInstallTailscale } fullWidth >
2026-04-10 07:16:14 -04:00
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
2026-05-09 06:48:20 -04:00
onClick = { ( ) => handleConnectTailscale ( ) }
2026-04-10 07:16:14 -04:00
fullWidth
>
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-05-03 07:00:35 -04:00
< Button onClick = { handleDisableTailscale } fullWidth disabled = { tsLoading } variant = "danger" >
2026-04-10 07:16:14 -04:00
{ 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 >
2026-05-13 04:32:17 -04:00
{ /* Confirm Modal */ }
< ConfirmModal
isOpen = { ! ! confirmState }
onClose = { ( ) => setConfirmState ( null ) }
onConfirm = { confirmState ? . onConfirm }
title = { confirmState ? . title || "Confirm" }
message = { confirmState ? . message }
variant = "danger"
/ >
2026-01-04 21:58:59 -05:00
< / 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" >
2026-05-03 07:00:35 -04:00
< span className = { ` text-xs font-mono px-1.5 py-0.5 rounded shrink-0 min-w-[88px] text-center ${
( badge === "CF" || badge === "TS" ) ? "bg-primary/10 text-primary" : "bg-surface-2 text-text-muted"
2026-04-10 07:16:14 -04:00
} ` }>{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
} ;