import { useCallback, useEffect, useMemo, useState } from 'react'; import { api } from '../api'; import { toast } from './ui/sonner'; import { Button } from './ui/button'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from './ui/sheet'; import type { Contact, PaneState, RepeaterAclResponse, RepeaterLppTelemetryResponse, RepeaterStatusResponse, } from '../types'; import { TelemetryPane } from './repeater/RepeaterTelemetryPane'; import { AclPane } from './repeater/RepeaterAclPane'; import { LppTelemetryPane } from './repeater/RepeaterLppTelemetryPane'; import { ConsolePane } from './repeater/RepeaterConsolePane'; import { RepeaterLogin } from './RepeaterLogin'; import { ServerLoginStatusBanner } from './ServerLoginStatusBanner'; import { useRememberedServerPassword } from '../hooks/useRememberedServerPassword'; import { buildServerLoginAttemptFromError, buildServerLoginAttemptFromResponse, type ServerLoginAttemptState, } from '../utils/serverLoginState'; interface RoomServerPanelProps { contact: Contact; onAuthenticatedChange?: (authenticated: boolean) => void; } type RoomPaneKey = 'status' | 'acl' | 'lppTelemetry'; type RoomPaneData = { status: RepeaterStatusResponse | null; acl: RepeaterAclResponse | null; lppTelemetry: RepeaterLppTelemetryResponse | null; }; type RoomPaneStates = Record; type ConsoleEntry = { command: string; response: string; timestamp: number; outgoing: boolean; }; const INITIAL_PANE_STATE: PaneState = { loading: false, attempt: 0, error: null, fetched_at: null, }; function createInitialPaneStates(): RoomPaneStates { return { status: { ...INITIAL_PANE_STATE }, acl: { ...INITIAL_PANE_STATE }, lppTelemetry: { ...INITIAL_PANE_STATE }, }; } export function RoomServerPanel({ contact, onAuthenticatedChange }: RoomServerPanelProps) { const { password, setPassword, rememberPassword, setRememberPassword, persistAfterLogin } = useRememberedServerPassword('room', contact.public_key); const [loginLoading, setLoginLoading] = useState(false); const [loginError, setLoginError] = useState(null); const [authenticated, setAuthenticated] = useState(false); const [lastLoginAttempt, setLastLoginAttempt] = useState(null); const [advancedOpen, setAdvancedOpen] = useState(false); const [paneData, setPaneData] = useState({ status: null, acl: null, lppTelemetry: null, }); const [paneStates, setPaneStates] = useState(createInitialPaneStates); const [consoleHistory, setConsoleHistory] = useState([]); const [consoleLoading, setConsoleLoading] = useState(false); useEffect(() => { setLoginLoading(false); setLoginError(null); setAuthenticated(false); setLastLoginAttempt(null); setAdvancedOpen(false); setPaneData({ status: null, acl: null, lppTelemetry: null, }); setPaneStates(createInitialPaneStates()); setConsoleHistory([]); setConsoleLoading(false); }, [contact.public_key]); useEffect(() => { onAuthenticatedChange?.(authenticated); }, [authenticated, onAuthenticatedChange]); const refreshPane = useCallback( async (pane: K, loader: () => Promise) => { setPaneStates((prev) => ({ ...prev, [pane]: { ...prev[pane], loading: true, attempt: prev[pane].attempt + 1, error: null, }, })); try { const data = await loader(); setPaneData((prev) => ({ ...prev, [pane]: data })); setPaneStates((prev) => ({ ...prev, [pane]: { loading: false, attempt: prev[pane].attempt, error: null, fetched_at: Date.now(), }, })); } catch (err) { setPaneStates((prev) => ({ ...prev, [pane]: { ...prev[pane], loading: false, error: err instanceof Error ? err.message : 'Unknown error', }, })); } }, [] ); const performLogin = useCallback( async (nextPassword: string, method: 'password' | 'blank') => { if (loginLoading) return; setLoginLoading(true); setLoginError(null); try { const result = await api.roomLogin(contact.public_key, nextPassword); setLastLoginAttempt(buildServerLoginAttemptFromResponse(method, result, 'room server')); setAuthenticated(true); if (result.authenticated) { toast.success('Login confirmed by the room server.'); } else { toast.warning("Couldn't confirm room login", { description: result.message ?? 'No confirmation came back from the room server. You can still open tools and try again.', }); } } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; setLastLoginAttempt(buildServerLoginAttemptFromError(method, message, 'room server')); setAuthenticated(true); setLoginError(message); toast.error('Room login request failed', { description: `${message}. You can still open tools and retry the login from here.`, }); } finally { setLoginLoading(false); } }, [contact.public_key, loginLoading] ); const handleLogin = useCallback( async (nextPassword: string) => { await performLogin(nextPassword, 'password'); persistAfterLogin(nextPassword); }, [performLogin, persistAfterLogin] ); const handleLoginAsGuest = useCallback(async () => { await performLogin('', 'blank'); persistAfterLogin(''); }, [performLogin, persistAfterLogin]); const handleConsoleCommand = useCallback( async (command: string) => { setConsoleLoading(true); const timestamp = Date.now(); setConsoleHistory((prev) => [ ...prev, { command, response: command, timestamp, outgoing: true }, ]); try { const response = await api.sendRepeaterCommand(contact.public_key, command); setConsoleHistory((prev) => [ ...prev, { command, response: response.response, timestamp: Date.now(), outgoing: false, }, ]); } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error'; setConsoleHistory((prev) => [ ...prev, { command, response: `(error) ${message}`, timestamp: Date.now(), outgoing: false, }, ]); } finally { setConsoleLoading(false); } }, [contact.public_key] ); const panelTitle = useMemo(() => contact.name || contact.public_key.slice(0, 12), [contact]); const showLoginFailureState = lastLoginAttempt !== null && lastLoginAttempt.outcome !== 'confirmed'; if (!authenticated) { return (
Room server access is experimental and in public alpha. Please report any issues on{' '} GitHub .
); } return (
{showLoginFailureState ? ( 0} onRetryPassword={() => handleLogin(password)} onRetryBlank={handleLoginAsGuest} blankRetryLabel="Retry Existing-Access Login" showRetryActions={false} /> ) : null}
{showLoginFailureState ? (
) : (
)}
Room Server Tools Room server telemetry, ACL tools, sensor data, and CLI console

Room Server Tools

{panelTitle}

refreshPane('status', () => api.roomStatus(contact.public_key))} /> refreshPane('acl', () => api.roomAcl(contact.public_key))} /> refreshPane('lppTelemetry', () => api.roomLppTelemetry(contact.public_key)) } />
); }