feat(frontend): écran Appareils et parcours d'appairage nommé (#77 F1/F2)

Expose la gestion des appareils appairés introduite en B1-B4, sur une
surface unique partagée par le web et le desktop.

- Écran Appareils : liste, renommage, révocation unitaire ou globale,
  activité formatée, panneau de code éphémère.
- Le parcours d'appairage demande un nom d'appareil, pour qu'une
  révocation porte sur quelque chose d'identifiable par l'utilisateur.
- Gateways DeviceGateway en trois adapters (Tauri, HTTP, Mock), le port
  restant le seul contrat connu de la feature.

Les erreurs sont mappées localement et le message du serveur n'est jamais
affiché tel quel : un échec d'appairage ne doit pas devenir un oracle pour
qui teste des codes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 13:27:06 +02:00
parent 8fe93d1652
commit 3f1e132e88
27 changed files with 2013 additions and 135 deletions

View File

@ -190,9 +190,11 @@ export type EmbeddedServerState =
| "failed";
/**
* Embedded-server status (mirror of `EmbeddedServerStatusDto`). `pairingCode`
* is a runtime-only secret: it exists while the server runs, is never
* persisted, and must never be written into a settings field.
* Embedded-server status (mirror of `EmbeddedServerStatusDto`).
*
* Carries **no pairing code** (#77): a code no longer exists while the server
* runs, only when someone asks for one. Generating is a device-management action
* ({@link PairingCode}), not a property of the server's status.
*/
export interface EmbeddedServerStatus {
state: EmbeddedServerState;
@ -202,8 +204,6 @@ export interface EmbeddedServerStatus {
publicUrl?: string;
/** Upstream URL to hand to the reverse proxy. */
upstreamUrl?: string;
/** Runtime pairing code — present only while running. */
pairingCode?: string;
/** Last failure, when `state` is `failed`. */
error?: GatewayError;
}
@ -1321,3 +1321,52 @@ export type ReplyChunk =
| { kind: "toolActivity"; label: string }
| { kind: "final"; content: string }
| { kind: "error"; message: string };
// ---------------------------------------------------------------------------
// Paired devices + pairing code (ticket #77)
// ---------------------------------------------------------------------------
/**
* One device paired with this IdeA instance (mirror of the backend device DTO).
*
* Deliberately carries **no IP and no User-Agent**: the design is mono-user and
* the list is an access-management surface, not a forensics log. `name` is the
* human label typed on the device itself at pairing time.
*/
export interface PairedDevice {
deviceId: string;
name: string;
/** Epoch milliseconds the device was paired. */
pairedAtMs: number;
/** Epoch milliseconds of the device's last authenticated request. */
lastSeenAtMs: number;
/** True for the device rendering this list (never true on desktop). */
isCurrentDevice: boolean;
}
/**
* A freshly generated, single-use pairing code (mirror of the backend DTO).
*
* `code` is the **canonical** value — uppercase hex, no separator. Any grouping
* shown to the user is presentation only; see {@link normalizePairingCode}.
*/
export interface PairingCode {
code: string;
/** Epoch milliseconds the code stops being accepted. */
expiresAtMs: number;
/** Lifetime granted at generation (600 s today). */
ttlSeconds: number;
}
/**
* Canonical form of a pairing code as the server compares it: uppercase, with
* spaces **and dashes** removed.
*
* Both separators matter. The UI groups the code visually (`AB12 CD34`) and a
* user may retype it with a dash out of habit, so a code copied by eye must
* still pair. The server normalises too (#76) — this keeps the client honest
* rather than being the only line of defence.
*/
export function normalizePairingCode(raw: string): string {
return raw.replace(/[\s-]+/g, "").toUpperCase();
}