From 120d0d1ac24daaddfbaeff7c7bd0b6b6f9c89e0e Mon Sep 17 00:00:00 2001 From: Blomios Date: Fri, 17 Jul 2026 10:40:26 +0200 Subject: [PATCH 1/2] fix(frontend): clavier texte et normalisation du code d'appairage (#75) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le code d'appairage est de l'hexadécimal majuscule (p. ex. 3F7A9C21), pas une suite de chiffres. Le champ appelait pourtant le pavé numérique des mobiles, rendant les lettres A-F non saisissables. Le champ passe donc en inputMode="text" avec autoCapitalize="characters", ce qui revient sur le choix inverse fait en #69 : ce ticket avait supposé un code purement numérique. Le placeholder et l'aide du champ décrivent désormais le format réel. La saisie est normalisée (majuscules, espaces retirés) avant pair() car le serveur compare le code strictement ; la dette correspondante côté backend est suivie en #76. Co-Authored-By: Claude Opus 4.8 --- .../src/features/web/PairingScreen.test.tsx | 77 +++++++++++++++++++ frontend/src/features/web/PairingScreen.tsx | 28 ++++--- 2 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 frontend/src/features/web/PairingScreen.test.tsx diff --git a/frontend/src/features/web/PairingScreen.test.tsx b/frontend/src/features/web/PairingScreen.test.tsx new file mode 100644 index 0000000..4aab76a --- /dev/null +++ b/frontend/src/features/web/PairingScreen.test.tsx @@ -0,0 +1,77 @@ +/** + * #75 — the pairing code is uppercase hex (`3F7A9C21`), not digits: + * - the field must summon the text keyboard, not the numeric keypad; + * - a lowercase entry must reach `session.pair()` uppercased and space-free, + * because the server compares the code strictly. + */ +import { describe, it, expect, vi } from "vitest"; +import { fireEvent, render, screen } from "@testing-library/react"; + +import { WebSession } from "@/adapters/http"; +import type { FetchLike } from "@/adapters/http/httpInvoker"; +import type { FlagStore } from "@/adapters/http/webSession"; +import { PairingScreen } from "./PairingScreen"; + +function memStore(): FlagStore { + const map = new Map(); + return { + getItem: (k) => map.get(k) ?? null, + setItem: (k, v) => void map.set(k, v), + removeItem: (k) => void map.delete(k), + }; +} + +const okFetch: FetchLike = async () => ({ + ok: true, + status: 200, + json: async () => ({ ok: true }), + text: async () => "{}", +}); + +function setup() { + const session = new WebSession({ baseUrl: "https://h", fetchImpl: okFetch, store: memStore() }); + const pair = vi.spyOn(session, "pair"); + const onPaired = vi.fn(); + render(); + return { pair, onPaired }; +} + +describe("PairingScreen code field", () => { + it("asks for the text keyboard, not the numeric keypad", () => { + setup(); + const field = screen.getByLabelText("Code d'appairage"); + + expect(field.getAttribute("inputmode")).toBe("text"); + expect(field.getAttribute("autocapitalize")).toBe("characters"); + expect(field.getAttribute("autocomplete")).toBe("one-time-code"); + }); + + it("describes the code without lying about its shape", () => { + setup(); + const field = screen.getByLabelText("Code d'appairage"); + + expect(field.getAttribute("placeholder")).toBe("p. ex. AB12CD34"); + expect(screen.getByText("8 caractères : chiffres et lettres A-F")).toBeTruthy(); + }); + + it("uppercases and strips spaces before pairing", async () => { + const { pair, onPaired } = setup(); + + fireEvent.change(screen.getByLabelText("Code d'appairage"), { + target: { value: " ab12cd34 " }, + }); + fireEvent.click(screen.getByRole("button", { name: "Appairer" })); + + await vi.waitFor(() => expect(onPaired).toHaveBeenCalled()); + expect(pair).toHaveBeenCalledWith("AB12CD34"); + }); + + it("keeps submit disabled for a blank code", () => { + setup(); + const submit = screen.getByRole("button", { name: "Appairer" }); + expect((submit as HTMLButtonElement).disabled).toBe(true); + + fireEvent.change(screen.getByLabelText("Code d'appairage"), { target: { value: " " } }); + expect((submit as HTMLButtonElement).disabled).toBe(true); + }); +}); diff --git a/frontend/src/features/web/PairingScreen.tsx b/frontend/src/features/web/PairingScreen.tsx index 32bb704..89edf46 100644 --- a/frontend/src/features/web/PairingScreen.tsx +++ b/frontend/src/features/web/PairingScreen.tsx @@ -31,6 +31,14 @@ function describe(e: unknown): string { return String(e); } +/** + * The server compares the code strictly against the uppercase hex it generated, + * so the UI uppercases (and drops stray spaces) before sending. + */ +function normalize(raw: string): string { + return raw.replace(/\s+/g, "").toUpperCase(); +} + export function PairingScreen({ session, onPaired }: PairingScreenProps) { const [code, setCode] = useState(""); const [error, setError] = useState(null); @@ -38,12 +46,12 @@ export function PairingScreen({ session, onPaired }: PairingScreenProps) { async function submit(e: FormEvent): Promise { e.preventDefault(); - const trimmed = code.trim(); - if (!trimmed || busy) return; + const normalized = normalize(code); + if (!normalized || busy) return; setBusy(true); setError(null); try { - await session.pair(trimmed); + await session.pair(normalized); onPaired(); } catch (err) { setError(describe(err)); @@ -64,20 +72,20 @@ export function PairingScreen({ session, onPaired }: PairingScreenProps) {

- + {({ id, describedBy }) => ( setCode(e.target.value)} - placeholder="p. ex. 4821-93" + placeholder="p. ex. AB12CD34" autoFocus autoComplete="one-time-code" - // #69 — phones: summon the numeric-ish keypad for a pairing code - // and keep the OS from "helpfully" capitalising/correcting it. - inputMode="numeric" - autoCapitalize="off" + // #75 — the code mixes digits and letters, so phones need the + // text keyboard; uppercase it on the way in, never autocorrect. + inputMode="text" + autoCapitalize="characters" autoCorrect="off" spellCheck={false} disabled={busy} @@ -92,7 +100,7 @@ export function PairingScreen({ session, onPaired }: PairingScreenProps) {

)} - From 481a7e2498d7464585c29a81221d63ee3e45a604 Mon Sep 17 00:00:00 2001 From: Blomios Date: Fri, 17 Jul 2026 10:40:26 +0200 Subject: [PATCH 2/2] =?UTF-8?q?chore(ideai):=20cl=C3=B4ture=20de=20#74=20e?= =?UTF-8?q?t=20#68,=20ouverture=20de=20#75=20et=20#76?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Enregistre l'état du registre de tickets : #74 et #68 passent en closed après la validation live de l'intégration web, #75 (ce fix) entre en QA et #76 ouvre la dette de casse du code d'appairage côté serveur. État runtime uniquement : aucun code de feature n'est touché. Co-Authored-By: Claude Opus 4.8 --- .../97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json | 82 ++++++++++++++++++- .ideai/tickets/68/carnet.md | 4 +- .ideai/tickets/68/issue.md | 6 +- .ideai/tickets/74/carnet.md | 4 +- .ideai/tickets/74/issue.md | 6 +- .ideai/tickets/75/carnet.md | 6 ++ .ideai/tickets/75/issue.md | 48 +++++++++++ .ideai/tickets/76/carnet.md | 6 ++ .ideai/tickets/76/issue.md | 40 +++++++++ .ideai/tickets/counter.json | 2 +- .ideai/tickets/index.json | 28 ++++++- 11 files changed, 215 insertions(+), 17 deletions(-) create mode 100644 .ideai/tickets/75/carnet.md create mode 100644 .ideai/tickets/75/issue.md create mode 100644 .ideai/tickets/76/carnet.md create mode 100644 .ideai/tickets/76/issue.md diff --git a/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json b/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json index aef1dd3..7fad2e8 100644 --- a/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json +++ b/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json @@ -8177,10 +8177,88 @@ "ticket_id": "8bf414e0-60f1-4fb8-87ff-76d618150fb0", "conversation_id": "6bc594e8-a37c-0dbd-1de6-6e3b73002cb4" }, - "state": "running", + "state": "completed", "wakePolicy": "recordOnly", "createdAtMs": 1784277230713, - "updatedAtMs": 1784277230713, + "updatedAtMs": 1784277293596, + "deadlineMs": null, + "result": { + "outcome": "success", + "finished_at_ms": 1784277293596, + "exit_code": null, + "summary": "Headless rendezvous completed with Final", + "stdout_tail": "**#74 est intégré dans `develop`. Working tree propre, rien en attente, aucun push effectué.**\n\n## Ce que j'ai commité\n\nUn seul commit restait à faire : `4de25d2 — chore(ideai): état runtime du ticket #74 (passage en QA)`. Il porte les 4 fichiers non commités (issue, carnet, index des tickets, accusés de complétion des rendez-vous headless). C'est de l'état runtime pur, donc isolé du code de feature selon ma convention. J'ai vérifié avec `--untracked-files=all` qu'aucun artefact de build ne traînait (`dist/`, `dist-web/`, `target/`, AppImage) — rien.\n\n## Ce que j'ai mergé\n\n`aec4293` — merge `--no-ff` de `feature/ticket74-web-transport-bundle` vers `develop`. Aucun conflit, `develop` n'avait pas bougé depuis la création de la branche (0 commit de retard), donc pas de rebase préalable nécessaire.\n\n**Un point mérite ton attention** : ce merge intègre **deux tickets**, pas un. La branche #74 était empilée sur `feature/ticket68-appimage-web-assets` et contenait `166adc3` (#68, packaging des assets web en ressource Tauri), qui n'était pas encore dans `develop`. C'était voulu et je l'assume : le packaging #68 servait précisément le bundle cassé que #74 corrige, il ne pouvait pas être déclaré vert tout seul. Les deux ferment ensemble. Si tu considérais #68 comme ayant sa propre porte QA à franchir séparément, dis-le moi — mais techniquement, le séparer était impossible sans conflit frontal sur les mêmes lignes de `tauri.conf.json`.\n\nJ'ai aussi supprimé `feature/ticket68-appimage-web-assets` : elle était un ancêtre strict de la branche #74, sans aucun commit propre. La garder n'apportait rien.\n\n## État final\n\n`develop` est sur `aec4293`, en avance de **34 commits sur `origin/develop`**, arbre propre. Les deux branches de feature (`ticket74-web-transport-bundle`, `ticket68-appimage-web-assets`) sont supprimées, leur travail vivant dans `develop`.\n\n## Ce qui reste en suspens\n\nLe **statut des tickets #68 et #74 est resté à `qa`** dans les métadonnées. Le code est mergé et validé live, mais la clôture formelle d'un ticket n'est pas ma décision — c'est Main qui arbitre. À faire passer en `done` de son côté.\n\nCôté remote, **les 34 commits d'avance sur `origin/develop` ne sont pas poussés**, conformément à mon périmètre strictement local. Un `git push origin develop` les publierait ; dis-moi si tu le veux et je m'en occupe. Enfin, `develop → main` (release) n'a pas été touché, ça reste une décision explicite de ta part.", + "stderr_tail": null + }, + "completionDelivered": false + }, + { + "id": "078855de-999f-40a8-97fc-0b5c1be2fe5c", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "c3e0d9d8-df36-4b00-b271-62d8aa78892c", + "ticket_id": "1a3657bd-1067-4a0c-96ab-28bb326a3e6c", + "conversation_id": "652e01c1-67a5-0913-0272-fc456de31f6d" + }, + "state": "completed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784277366856, + "updatedAtMs": 1784277379813, + "deadlineMs": null, + "result": { + "outcome": "success", + "finished_at_ms": 1784277379813, + "exit_code": null, + "summary": "Headless rendezvous completed with Final", + "stdout_tail": "Décision UX pour Ticket #75 :\n\n1. `inputMode`\nUtiliser `inputMode=\"text\"`.\n\nRaison : le code contient chiffres + lettres `A-F`; un pavé numérique rend la saisie partiellement impossible. Ne pas utiliser `numeric`, `decimal` ni `tel`.\n\n2. Placeholder / aide visuelle\nRemplacer le placeholder actuel par un exemple fidèle :\n\n`AB12-CD34`\n\nAjouter ou conserver une aide courte sous le champ si l’écran en a déjà une :\n\n`8 caractères : chiffres et lettres A-F`\n\nÉviter le mot “hexadécimal”. Il est exact techniquement, mais inutile côté utilisateur.\n\n3. Casse\nL’UI doit normaliser la saisie en majuscules avant affichage et avant envoi.\n\nComportement attendu :\n- si l’utilisateur tape `ab12cd34`, le champ affiche `AB12-CD34` ou `AB12CD34` selon le formatage existant retenu ;\n- la valeur envoyée au serveur est toujours `AB12CD34`, sans tiret ni espace ;\n- ne pas imposer à l’utilisateur de changer manuellement de casse.\n\nPérimètre frontend pur suffisant : normalisation côté UI avant submit. \nÀ noter comme robustesse future, mais hors périmètre demandé : le backend devrait idéalement comparer en normalisant aussi, car la tolérance ne doit pas dépendre uniquement du client.\n\n4. `autoCapitalize` / `autoComplete`\nMettre :\n\n- `autoCapitalize=\"characters\"`\n- `autoCorrect=\"off\"`\n- `autoComplete=\"one-time-code\"`\n- `spellCheck={false}` si le composant le permet\n\nCritères d’acceptation visuels/fonctionnels :\n- sur mobile, le clavier permet lettres et chiffres sans bascule manuelle obligatoire ;\n- le placeholder ne suggère plus un code numérique ;\n- une saisie minuscule aboutit à un code accepté par le serveur via normalisation frontend ;\n- l’aide reste courte, non technique, et ne prend pas plus d’une ligne sur mobile.", + "stderr_tail": null + }, + "completionDelivered": false + }, + { + "id": "fecf3304-b299-45f0-9ec6-0dff06d4a990", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "af7f86da-76bc-48e1-9900-71f45a624800", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "af7f86da-76bc-48e1-9900-71f45a624800", + "ticket_id": "e315464e-656f-4484-b444-18245987e85f", + "conversation_id": "09b15ec3-ce2f-0af2-2903-ef699df9de41" + }, + "state": "completed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784277402194, + "updatedAtMs": 1784277521434, + "deadlineMs": null, + "result": { + "outcome": "success", + "finished_at_ms": 1784277521434, + "exit_code": null, + "summary": "Headless rendezvous completed with Final", + "stdout_tail": "Ticket #75 est corrigé et vert. Le champ demande maintenant le clavier texte et la valeur envoyée au serveur est normalisée en majuscules.\n\n## Ce que j'ai changé\n\n**`frontend/src/features/web/PairingScreen.tsx`** — les 5 points de la spec, tels quels :\n\n- `inputMode=\"numeric\"` → `inputMode=\"text\"`, et `autoCapitalize=\"off\"` → `\"characters\"`. Les autres attributs (`autoCorrect=\"off\"`, `autoComplete=\"one-time-code\"`, `spellCheck={false}`) sont conservés. J'ai remplacé le commentaire `#69` qui justifiait le pavé numérique par un commentaire `#75` expliquant pourquoi le clavier texte est requis.\n- Placeholder `p. ex. 4821-93` → `p. ex. AB12CD34`, sans séparateur comme tu l'as tranché.\n- L'aide passe par le `hint` de `Field`, qui la rend dans le `

` déjà câblé à `aria-describedby` — donc pas de `

` libre, et le lien accessibilité est automatique. Texte : « 8 caractères : chiffres et lettres A-F ».\n- Une fonction `normalize()` locale remplace le `trim()` : elle enlève tous les espaces (pas seulement en bord) et met en majuscules. C'est elle qui alimente `session.pair()`.\n- Le `disabled` du bouton submit passe de `code.trim().length === 0` à `normalize(code).length === 0`, donc une saisie de seuls espaces reste refusée, cohérente avec ce qui serait envoyé.\n\nPas de masque de saisie ni de formatage à la frappe, et l'écran ne parle toujours qu'à la `WebSession` injectée — aucun import `@tauri-apps` ajouté, le garde-fou `no-direct-invoke` n'est pas touché.\n\n**`frontend/src/features/web/PairingScreen.test.tsx`** (nouveau) — j'ai préféré un fichier dédié plutôt que d'alourdir `WebApp.test.tsx`, qui teste le routage et non le champ. Quatre tests, avec un `vi.spyOn(session, \"pair\")` sur une vraie `WebSession` à fetch injecté : le `inputMode` est bien `text`, le placeholder et l'aide sont fidèles, ` ab12cd34 ` part en `AB12CD34`, et le submit reste désactivé sur une saisie vide ou blanche.\n\n## Sortie de test réelle\n\n`cd frontend && npx vitest run` (suite complète) :\n\n```\n Test Files 88 passed (88)\n Tests 801 passed (801)\n Duration 16.78s\n```\n\n`npx tsc --noEmit` sort en 0. Les tests existants de `WebApp.test.tsx` passent sans modification : leur code `4821-93` traverse `normalize()` inchangé.\n\n## Dette à ouvrir, hors périmètre\n\nLa comparaison serveur (`crates/web-server/src/lib.rs:1612`) reste stricte sur la casse. Le fix d'aujourd'hui rend l'écran web tolérant, mais la tolérance vit côté client : tout autre client de l'API (curl, un futur client mobile natif, un test manuel) échouera encore sur `3f7a9c21`. Le backend devrait normaliser en majuscules à la comparaison, ce qui rendrait la normalisation UI redondante mais inoffensive. À toi de décider si ça mérite un ticket.", + "stderr_tail": null + }, + "completionDelivered": false + }, + { + "id": "cef21a7e-3eab-4bc5-a4d8-46af0cab4496", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5", + "ticket_id": "690948d6-4063-4d28-ba3c-a6b6545f03c0", + "conversation_id": "6bc594e8-a37c-0dbd-1de6-6e3b73002cb4" + }, + "state": "running", + "wakePolicy": "recordOnly", + "createdAtMs": 1784277549807, + "updatedAtMs": 1784277549807, "deadlineMs": null, "result": null, "completionDelivered": false diff --git a/.ideai/tickets/68/carnet.md b/.ideai/tickets/68/carnet.md index 9796242..6170a99 100644 --- a/.ideai/tickets/68/carnet.md +++ b/.ideai/tickets/68/carnet.md @@ -1,8 +1,8 @@ --- issueRef: "#68" -version: 7 +version: 8 updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedAt: 1784238387151 +updatedAt: 1784277313706 --- # Ticket #68 — Serveur embarqué activable depuis le desktop : carnet de chantier diff --git a/.ideai/tickets/68/issue.md b/.ideai/tickets/68/issue.md index 821a523..6b45f1d 100644 --- a/.ideai/tickets/68/issue.md +++ b/.ideai/tickets/68/issue.md @@ -2,7 +2,7 @@ id: "13821b24-f566-402e-bc10-02ac6e8a5f7c" number: 68 title: "Ajouter dans l'app desktop, la possibilité d'activer le serveur" -status: "qa" +status: "closed" priority: "medium" sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6" links: [{"target":"#65","kind":"dependsOn"}] @@ -10,7 +10,7 @@ agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"} createdBy: {"kind":"user"} updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} createdAt: 1784193318419 -updatedAt: 1784238387151 -version: 7 +updatedAt: 1784277313706 +version: 8 --- J'iamerais que depuis l'app desktop, on puisse quand même embarquer le serveur. Comme ça un utilisateur pourrait continuer son travail en cours en remote \ No newline at end of file diff --git a/.ideai/tickets/74/carnet.md b/.ideai/tickets/74/carnet.md index 1594ad1..f66e578 100644 --- a/.ideai/tickets/74/carnet.md +++ b/.ideai/tickets/74/carnet.md @@ -1,6 +1,6 @@ --- issueRef: "#74" -version: 2 +version: 3 updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedAt: 1784272247901 +updatedAt: 1784277308704 --- diff --git a/.ideai/tickets/74/issue.md b/.ideai/tickets/74/issue.md index cdf02ce..ad2f84c 100644 --- a/.ideai/tickets/74/issue.md +++ b/.ideai/tickets/74/issue.md @@ -2,7 +2,7 @@ id: "249bb0de-118f-4d6f-b0c4-1d4e2aadd83f" number: 74 title: "Le serveur embarqué sert le bundle desktop (Tauri) au navigateur — __TAURI_INTERNALS__ undefined" -status: "qa" +status: "closed" priority: "high" sprint: null links: [{"target":"#68","kind":"relatesTo"},{"target":"#66","kind":"relatesTo"},{"target":"#13","kind":"relatesTo"}] @@ -10,8 +10,8 @@ agentRefs: [] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} createdAt: 1784271086298 -updatedAt: 1784272247901 -version: 2 +updatedAt: 1784277308704 +version: 3 --- ## Symptôme (constaté live, exposition derrière reverse proxy) diff --git a/.ideai/tickets/75/carnet.md b/.ideai/tickets/75/carnet.md new file mode 100644 index 0000000..43bf53a --- /dev/null +++ b/.ideai/tickets/75/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#75" +version: 2 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784277530792 +--- diff --git a/.ideai/tickets/75/issue.md b/.ideai/tickets/75/issue.md new file mode 100644 index 0000000..f57fd4e --- /dev/null +++ b/.ideai/tickets/75/issue.md @@ -0,0 +1,48 @@ +--- +id: "d54a1f20-03e6-4925-982b-923a37331749" +number: 75 +title: "Écran d'appairage web : clavier numérique sur mobile alors que le code contient des lettres" +status: "qa" +priority: "medium" +sprint: null +links: [{"target":"#69","kind":"relatesTo"},{"target":"#74","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784277353320 +updatedAt: 1784277530792 +version: 2 +--- +## Symptôme (constaté live, téléphone) + +Sur l'écran d'appairage web (`PairingScreen`), le clavier virtuel qui s'ouvre est un pavé **numérique uniquement**, alors que le code d'appairage à saisir contient des **lettres**. Impossible de saisir le code sans changer de clavier à la main. + +## Cause racine (confirmée) + +Le code généré côté serveur est **hexadécimal, 8 caractères, majuscules** (`0-9A-F`) : + +```rust +// crates/web-server/src/lib.rs:2220 +fn new_pairing_code() -> String { + Uuid::new_v4().simple().to_string().chars().take(8) + .collect::().to_ascii_uppercase() +} +``` + +Or `frontend/src/features/web/PairingScreen.tsx:79` force `inputMode="numeric"` (introduit par #69 en supposant un code purement chiffré), et le placeholder `p. ex. 4821-93` (ligne 74) décrit un format qui n'existe pas. + +## Attendu + +- Le clavier mobile permet de saisir lettres **et** chiffres. +- Le placeholder reflète le format réel (hex 8 caractères majuscules). +- La saisie reste sans autocorrection ni capitalisation surprise ; le code étant en majuscules, la casse ne doit pas être un piège pour l'utilisateur. + +## Périmètre + +Frontend pur. Le format du code côté serveur n'est pas remis en cause par ce ticket. + +## DoD + +- Saisie possible au clavier mobile standard, lettres incluses. +- Placeholder cohérent avec `new_pairing_code()`. +- Test de non-régression sur le champ. \ No newline at end of file diff --git a/.ideai/tickets/76/carnet.md b/.ideai/tickets/76/carnet.md new file mode 100644 index 0000000..c70ae90 --- /dev/null +++ b/.ideai/tickets/76/carnet.md @@ -0,0 +1,6 @@ +--- +issueRef: "#76" +version: 1 +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedAt: 1784277540034 +--- diff --git a/.ideai/tickets/76/issue.md b/.ideai/tickets/76/issue.md new file mode 100644 index 0000000..01be580 --- /dev/null +++ b/.ideai/tickets/76/issue.md @@ -0,0 +1,40 @@ +--- +id: "c91f547f-96f0-4c9c-9fde-dbe7d2772dbc" +number: 76 +title: "Appairage : la comparaison du code est sensible à la casse côté serveur" +status: "open" +priority: "low" +sprint: null +links: [{"target":"#75","kind":"relatesTo"}] +agentRefs: [] +createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} +createdAt: 1784277540034 +updatedAt: 1784277540034 +version: 1 +--- +## Constat + +`crates/web-server/src/lib.rs:1612` compare le code d'appairage reçu au code attendu par égalité stricte : + +```rust +if request.code != state.pairing_code() { … } +``` + +Or `new_pairing_code()` (ligne 2220) génère le code en majuscules. Un client qui envoie `3f7a9c21` au lieu de `3F7A9C21` est donc rejeté. + +## Pourquoi c'est une dette et pas un bug bloquant + +Le fix #75 rend l'écran web tolérant en normalisant la saisie en majuscules avant `POST /api/pair`. La tolérance vit donc **côté client** : tout autre client de l'API (curl, un futur client mobile natif, un test manuel) échouera encore sur une saisie minuscule, avec un message d'erreur qui n'explique pas pourquoi. + +## Attendu + +La comparaison serveur normalise la casse (et les espaces) avant égalité. La normalisation côté UI de #75 devient alors redondante mais inoffensive — pas besoin de la retirer. + +## Vigilance + +La comparaison doit rester en temps constant si elle l'est aujourd'hui : normaliser ne doit pas introduire de court-circuit exploitable en timing. À vérifier lors de l'implémentation. + +## Périmètre + +Backend pur, `crates/web-server`. \ No newline at end of file diff --git a/.ideai/tickets/counter.json b/.ideai/tickets/counter.json index 1ca5d8b..4fa6919 100644 --- a/.ideai/tickets/counter.json +++ b/.ideai/tickets/counter.json @@ -1,3 +1,3 @@ { - "nextNumber": 75 + "nextNumber": 77 } \ No newline at end of file diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index 10b261a..1a94f21 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -707,13 +707,13 @@ "issueRef": "#68", "path": "68", "title": "Ajouter dans l'app desktop, la possibilité d'activer le serveur", - "status": "qa", + "status": "closed", "priority": "medium", "sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6", "assignedAgentIds": [ "a6ced819-b893-4213-b003-9e9dc79b9641" ], - "updatedAt": 1784238387151 + "updatedAt": 1784277313706 }, { "issueRef": "#69", @@ -773,11 +773,31 @@ "issueRef": "#74", "path": "74", "title": "Le serveur embarqué sert le bundle desktop (Tauri) au navigateur — __TAURI_INTERNALS__ undefined", - "status": "qa", + "status": "closed", "priority": "high", "sprint": null, "assignedAgentIds": [], - "updatedAt": 1784272247901 + "updatedAt": 1784277308704 + }, + { + "issueRef": "#75", + "path": "75", + "title": "Écran d'appairage web : clavier numérique sur mobile alors que le code contient des lettres", + "status": "qa", + "priority": "medium", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784277530792 + }, + { + "issueRef": "#76", + "path": "76", + "title": "Appairage : la comparaison du code est sensible à la casse côté serveur", + "status": "open", + "priority": "low", + "sprint": null, + "assignedAgentIds": [], + "updatedAt": 1784277540034 } ] } \ No newline at end of file