feat(frontend): polish web (logout, 401→pairing, reconnexion globale) (#13)

Flow logout : POST /api/logout puis retour à l'écran de pairing et
déconnexion du live. 401 renvoie au pairing sans boucle. Bannière de
reconnexion couvrant le cas live-only et le serveur indisponible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 08:23:11 +02:00
parent d538808a0d
commit 6117177957
10 changed files with 288 additions and 14 deletions

View File

@ -28,6 +28,7 @@ import { useProjectWorkState } from "@/features/workstate/useProjectWorkState";
import { Button, Panel, Spinner, cn } from "@/shared";
import { WebAgentCell } from "./WebAgentCell";
import { useLiveReconnect } from "./useLiveReconnect";
import { useLiveConnectionState } from "./useLiveConnectionState";
function describe(e: unknown): string {
if (e && typeof e === "object" && "message" in e) {
@ -75,6 +76,7 @@ export function WebWorkspace() {
return (
<div className="flex h-full flex-col gap-4 overflow-y-auto p-6">
<ReconnectBanner />
<div className="flex items-center justify-between">
<h2 className="text-base font-semibold tracking-tight">Projets</h2>
<Button variant="ghost" size="sm" onClick={() => void refresh()}>
@ -119,6 +121,28 @@ export function WebWorkspace() {
);
}
/**
* Global reconnection banner (F6). The F3 terminal path writes a "déconnecté"
* notice into xterm, but live-only surfaces (no terminal open) had no visible
* signal. This surfaces the shared live socket's `reconnecting` state so the user
* always knows the view may be momentarily stale; `useLiveReconnect` re-syncs the
* read-model on recovery. Inert on desktop (hook returns `null`).
*/
function ReconnectBanner() {
const connection = useLiveConnectionState();
if (connection !== "reconnecting") return null;
return (
<div
role="status"
data-testid="web-reconnect-banner"
className="flex items-center gap-2 rounded-md border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning"
>
<Spinner size={12} />
Connexion perdue reconnexion en cours
</div>
);
}
/** Live work-state + background + inbox for the opened project (F5). */
function LiveProjectPanel({ projectId, root }: { projectId: string; root: string | null }) {
const vm = useProjectWorkState(projectId);