/** * Web client root — ticket #13, lot F2. Mounted (instead of the desktop `App`) * only when the HTTP transport is selected (`VITE_TRANSPORT="http"`). * * Routes on the {@link WebSession} paired flag: not paired ⇒ {@link PairingScreen}; * paired ⇒ the read-only {@link WebWorkspace}. It subscribes to the session's * `onUnauthorized` signal so a `401` from any `/api/invoke` (expired/missing * cookie) drops back to pairing automatically. "Se déconnecter" (F6) revokes the * server session (`POST /api/logout`), tears down the live WS singleton, and * returns to pairing. */ import { useEffect, useState } from "react"; import { Button } from "@/shared"; import { disconnectWebLive, getWebSession, type WebSession } from "@/adapters/http"; import { PairingScreen } from "./PairingScreen"; import { WebWorkspace } from "./WebWorkspace"; interface WebAppProps { /** Injectable session (tests); defaults to the shared singleton. */ session?: WebSession; } export function WebApp({ session }: WebAppProps = {}) { const webSession = session ?? getWebSession(); const [paired, setPaired] = useState(() => webSession.isPaired()); const [signingOut, setSigningOut] = useState(false); useEffect(() => { // A 401 anywhere clears the flag and fires this: return to pairing. The live // WS is torn down by the composition-root 401 handler (F6). return webSession.onUnauthorized(() => setPaired(false)); }, [webSession]); async function signOut(): Promise { if (signingOut) return; setSigningOut(true); try { // Revoke the server session (best-effort), then drop the live socket so it // stops reconnecting, and return to pairing regardless of the outcome. await webSession.logout(); } finally { disconnectWebLive(); setSigningOut(false); setPaired(false); } } return (

IdeA

web
{paired && ( )}
{paired ? ( ) : ( setPaired(true)} /> )}
); }