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:
96
frontend/src/features/web/PairingScreen.tsx
Normal file
96
frontend/src/features/web/PairingScreen.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Web pairing screen — ticket #13, lot F2.
|
||||
*
|
||||
* Shown by {@link WebApp} when the client is not paired. The user types the code
|
||||
* the server printed at first launch; on submit we `POST /api/pair {code}` via
|
||||
* the {@link WebSession}. On success the server sets the HttpOnly session cookie
|
||||
* and we advance to the workspace; a wrong code shows a clear message.
|
||||
*
|
||||
* Transport-neutral at the component seam: it talks to the injected
|
||||
* {@link WebSession}, never to `@tauri-apps/api` (the CI guard `no-direct-invoke`
|
||||
* stays green). Pure web-only UI — desktop never mounts it.
|
||||
*/
|
||||
|
||||
import { useState, type FormEvent } from "react";
|
||||
|
||||
import type { GatewayError } from "@/domain";
|
||||
import { Button, Field, Input, Panel } from "@/shared";
|
||||
import type { WebSession } from "@/adapters/http";
|
||||
|
||||
interface PairingScreenProps {
|
||||
/** The shared web session performing the `POST /api/pair` handshake. */
|
||||
session: WebSession;
|
||||
/** Called once pairing succeeds (cookie set) so the app can advance. */
|
||||
onPaired: () => void;
|
||||
}
|
||||
|
||||
function describe(e: unknown): string {
|
||||
if (e && typeof e === "object" && "message" in e) {
|
||||
return String((e as GatewayError).message);
|
||||
}
|
||||
return String(e);
|
||||
}
|
||||
|
||||
export function PairingScreen({ session, onPaired }: PairingScreenProps) {
|
||||
const [code, setCode] = useState("");
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function submit(e: FormEvent): Promise<void> {
|
||||
e.preventDefault();
|
||||
const trimmed = code.trim();
|
||||
if (!trimmed || busy) return;
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
await session.pair(trimmed);
|
||||
onPaired();
|
||||
} catch (err) {
|
||||
setError(describe(err));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center bg-canvas p-6">
|
||||
<Panel className="w-full max-w-sm">
|
||||
<form onSubmit={submit} className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h1 className="text-lg font-semibold tracking-tight">Appairer cet appareil</h1>
|
||||
<p className="text-sm text-muted">
|
||||
Saisissez le code affiché par le serveur IdeA pour connecter ce
|
||||
navigateur.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Field label="Code d'appairage">
|
||||
{({ id, describedBy }) => (
|
||||
<Input
|
||||
id={id}
|
||||
aria-describedby={describedBy}
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder="p. ex. 4821-93"
|
||||
autoFocus
|
||||
autoComplete="one-time-code"
|
||||
disabled={busy}
|
||||
invalid={!!error}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
{error && (
|
||||
<p role="alert" data-testid="pairing-error" className="text-sm text-danger">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<Button type="submit" disabled={busy || code.trim().length === 0}>
|
||||
{busy ? "Appairage…" : "Appairer"}
|
||||
</Button>
|
||||
</form>
|
||||
</Panel>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user