feat(frontend): client web read-only pairing + snapshot état (#13)
Lot F2 du chantier server/client mode : client web read-only complétant le premier incrément livrable — pairing, liste des projets, ouverture et snapshot de l'état, sans PTY. - frontend/src/adapters/http/webSession.ts : session web (pairing/cookie). - frontend/src/features/web : PairingScreen, WebWorkspace, WebApp, index. - Câblage main.tsx et adaptations httpInvoker.ts / index.ts (cas 401). - Tests : webSession.test.ts, WebApp.test.tsx, cas 401 dans httpInvoker.test.ts. Validé : frontend 736 tests verts, build vert, garde no-direct-invoke verte, contrat B4↔F2 aligné, desktop non régressé. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
63
frontend/src/features/web/WebApp.tsx
Normal file
63
frontend/src/features/web/WebApp.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
/**
|
||||
* 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. A best-effort "Se déconnecter"
|
||||
* clears the local flag (server-side cookie revocation is B8).
|
||||
*/
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/shared";
|
||||
import { 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());
|
||||
|
||||
useEffect(() => {
|
||||
// A 401 anywhere clears the flag and fires this: return to pairing.
|
||||
return webSession.onUnauthorized(() => setPaired(false));
|
||||
}, [webSession]);
|
||||
|
||||
function signOut(): void {
|
||||
webSession.forget();
|
||||
setPaired(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col bg-canvas text-content">
|
||||
<header className="flex shrink-0 items-center justify-between border-b border-border px-6 py-3">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<h1 className="text-lg font-semibold tracking-tight">IdeA</h1>
|
||||
<span className="rounded-md bg-raised px-1.5 py-0.5 text-[0.65rem] font-medium uppercase text-muted">
|
||||
web
|
||||
</span>
|
||||
</div>
|
||||
{paired && (
|
||||
<Button variant="ghost" size="sm" onClick={signOut}>
|
||||
Se déconnecter
|
||||
</Button>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
{paired ? (
|
||||
<WebWorkspace />
|
||||
) : (
|
||||
<PairingScreen session={webSession} onPaired={() => setPaired(true)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user