Merge feature/ticket75-pairing-code-keyboard into develop
Intègre le ticket #75 : l'écran d'appairage web appelait le pavé numérique des mobiles alors que le code d'appairage est de l'hexadécimal majuscule, rendant les lettres A-F non saisissables. Le champ passe en clavier texte, annonce le format réel et normalise la saisie avant envoi. Le correctif revient sur le choix de #69, qui avait supposé un code purement numérique. Suite verte rejouée avant merge : 88 fichiers, 801 tests, tsc exit 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -8177,10 +8177,88 @@
|
|||||||
"ticket_id": "8bf414e0-60f1-4fb8-87ff-76d618150fb0",
|
"ticket_id": "8bf414e0-60f1-4fb8-87ff-76d618150fb0",
|
||||||
"conversation_id": "6bc594e8-a37c-0dbd-1de6-6e3b73002cb4"
|
"conversation_id": "6bc594e8-a37c-0dbd-1de6-6e3b73002cb4"
|
||||||
},
|
},
|
||||||
"state": "running",
|
"state": "completed",
|
||||||
"wakePolicy": "recordOnly",
|
"wakePolicy": "recordOnly",
|
||||||
"createdAtMs": 1784277230713,
|
"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 `<p id={...}-msg>` déjà câblé à `aria-describedby` — donc pas de `<p>` 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,
|
"deadlineMs": null,
|
||||||
"result": null,
|
"result": null,
|
||||||
"completionDelivered": false
|
"completionDelivered": false
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#68"
|
issueRef: "#68"
|
||||||
version: 7
|
version: 8
|
||||||
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
|
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
|
# Ticket #68 — Serveur embarqué activable depuis le desktop : carnet de chantier
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: "13821b24-f566-402e-bc10-02ac6e8a5f7c"
|
id: "13821b24-f566-402e-bc10-02ac6e8a5f7c"
|
||||||
number: 68
|
number: 68
|
||||||
title: "Ajouter dans l'app desktop, la possibilité d'activer le serveur"
|
title: "Ajouter dans l'app desktop, la possibilité d'activer le serveur"
|
||||||
status: "qa"
|
status: "closed"
|
||||||
priority: "medium"
|
priority: "medium"
|
||||||
sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6"
|
sprint: "028179b1-eaf4-41e9-9c1f-7c37125117e6"
|
||||||
links: [{"target":"#65","kind":"dependsOn"}]
|
links: [{"target":"#65","kind":"dependsOn"}]
|
||||||
@ -10,7 +10,7 @@ agentRefs: [{"agentId":"a6ced819-b893-4213-b003-9e9dc79b9641","role":"assigned"}
|
|||||||
createdBy: {"kind":"user"}
|
createdBy: {"kind":"user"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
|
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
|
||||||
createdAt: 1784193318419
|
createdAt: 1784193318419
|
||||||
updatedAt: 1784238387151
|
updatedAt: 1784277313706
|
||||||
version: 7
|
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
|
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
|
||||||
@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
issueRef: "#74"
|
issueRef: "#74"
|
||||||
version: 2
|
version: 3
|
||||||
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
|
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
|
||||||
updatedAt: 1784272247901
|
updatedAt: 1784277308704
|
||||||
---
|
---
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
id: "249bb0de-118f-4d6f-b0c4-1d4e2aadd83f"
|
id: "249bb0de-118f-4d6f-b0c4-1d4e2aadd83f"
|
||||||
number: 74
|
number: 74
|
||||||
title: "Le serveur embarqué sert le bundle desktop (Tauri) au navigateur — __TAURI_INTERNALS__ undefined"
|
title: "Le serveur embarqué sert le bundle desktop (Tauri) au navigateur — __TAURI_INTERNALS__ undefined"
|
||||||
status: "qa"
|
status: "closed"
|
||||||
priority: "high"
|
priority: "high"
|
||||||
sprint: null
|
sprint: null
|
||||||
links: [{"target":"#68","kind":"relatesTo"},{"target":"#66","kind":"relatesTo"},{"target":"#13","kind":"relatesTo"}]
|
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"}
|
createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
|
||||||
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
|
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
|
||||||
createdAt: 1784271086298
|
createdAt: 1784271086298
|
||||||
updatedAt: 1784272247901
|
updatedAt: 1784277308704
|
||||||
version: 2
|
version: 3
|
||||||
---
|
---
|
||||||
## Symptôme (constaté live, exposition derrière reverse proxy)
|
## Symptôme (constaté live, exposition derrière reverse proxy)
|
||||||
|
|
||||||
|
|||||||
6
.ideai/tickets/75/carnet.md
Normal file
6
.ideai/tickets/75/carnet.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#75"
|
||||||
|
version: 2
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
|
||||||
|
updatedAt: 1784277530792
|
||||||
|
---
|
||||||
48
.ideai/tickets/75/issue.md
Normal file
48
.ideai/tickets/75/issue.md
Normal file
@ -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::<String>().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.
|
||||||
6
.ideai/tickets/76/carnet.md
Normal file
6
.ideai/tickets/76/carnet.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
issueRef: "#76"
|
||||||
|
version: 1
|
||||||
|
updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"}
|
||||||
|
updatedAt: 1784277540034
|
||||||
|
---
|
||||||
40
.ideai/tickets/76/issue.md
Normal file
40
.ideai/tickets/76/issue.md
Normal file
@ -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`.
|
||||||
@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"nextNumber": 75
|
"nextNumber": 77
|
||||||
}
|
}
|
||||||
@ -707,13 +707,13 @@
|
|||||||
"issueRef": "#68",
|
"issueRef": "#68",
|
||||||
"path": "68",
|
"path": "68",
|
||||||
"title": "Ajouter dans l'app desktop, la possibilité d'activer le serveur",
|
"title": "Ajouter dans l'app desktop, la possibilité d'activer le serveur",
|
||||||
"status": "qa",
|
"status": "closed",
|
||||||
"priority": "medium",
|
"priority": "medium",
|
||||||
"sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6",
|
"sprint": "028179b1-eaf4-41e9-9c1f-7c37125117e6",
|
||||||
"assignedAgentIds": [
|
"assignedAgentIds": [
|
||||||
"a6ced819-b893-4213-b003-9e9dc79b9641"
|
"a6ced819-b893-4213-b003-9e9dc79b9641"
|
||||||
],
|
],
|
||||||
"updatedAt": 1784238387151
|
"updatedAt": 1784277313706
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"issueRef": "#69",
|
"issueRef": "#69",
|
||||||
@ -773,11 +773,31 @@
|
|||||||
"issueRef": "#74",
|
"issueRef": "#74",
|
||||||
"path": "74",
|
"path": "74",
|
||||||
"title": "Le serveur embarqué sert le bundle desktop (Tauri) au navigateur — __TAURI_INTERNALS__ undefined",
|
"title": "Le serveur embarqué sert le bundle desktop (Tauri) au navigateur — __TAURI_INTERNALS__ undefined",
|
||||||
"status": "qa",
|
"status": "closed",
|
||||||
"priority": "high",
|
"priority": "high",
|
||||||
"sprint": null,
|
"sprint": null,
|
||||||
"assignedAgentIds": [],
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
77
frontend/src/features/web/PairingScreen.test.tsx
Normal file
77
frontend/src/features/web/PairingScreen.test.tsx
Normal file
@ -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<string, string>();
|
||||||
|
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(<PairingScreen session={session} onPaired={onPaired} />);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -31,6 +31,14 @@ function describe(e: unknown): string {
|
|||||||
return String(e);
|
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) {
|
export function PairingScreen({ session, onPaired }: PairingScreenProps) {
|
||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@ -38,12 +46,12 @@ export function PairingScreen({ session, onPaired }: PairingScreenProps) {
|
|||||||
|
|
||||||
async function submit(e: FormEvent): Promise<void> {
|
async function submit(e: FormEvent): Promise<void> {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const trimmed = code.trim();
|
const normalized = normalize(code);
|
||||||
if (!trimmed || busy) return;
|
if (!normalized || busy) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await session.pair(trimmed);
|
await session.pair(normalized);
|
||||||
onPaired();
|
onPaired();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(describe(err));
|
setError(describe(err));
|
||||||
@ -64,20 +72,20 @@ export function PairingScreen({ session, onPaired }: PairingScreenProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Field label="Code d'appairage">
|
<Field label="Code d'appairage" hint="8 caractères : chiffres et lettres A-F">
|
||||||
{({ id, describedBy }) => (
|
{({ id, describedBy }) => (
|
||||||
<Input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
aria-describedby={describedBy}
|
aria-describedby={describedBy}
|
||||||
value={code}
|
value={code}
|
||||||
onChange={(e) => setCode(e.target.value)}
|
onChange={(e) => setCode(e.target.value)}
|
||||||
placeholder="p. ex. 4821-93"
|
placeholder="p. ex. AB12CD34"
|
||||||
autoFocus
|
autoFocus
|
||||||
autoComplete="one-time-code"
|
autoComplete="one-time-code"
|
||||||
// #69 — phones: summon the numeric-ish keypad for a pairing code
|
// #75 — the code mixes digits and letters, so phones need the
|
||||||
// and keep the OS from "helpfully" capitalising/correcting it.
|
// text keyboard; uppercase it on the way in, never autocorrect.
|
||||||
inputMode="numeric"
|
inputMode="text"
|
||||||
autoCapitalize="off"
|
autoCapitalize="characters"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
@ -92,7 +100,7 @@ export function PairingScreen({ session, onPaired }: PairingScreenProps) {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button type="submit" disabled={busy || code.trim().length === 0}>
|
<Button type="submit" disabled={busy || normalize(code).length === 0}>
|
||||||
{busy ? "Appairage…" : "Appairer"}
|
{busy ? "Appairage…" : "Appairer"}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
Reference in New Issue
Block a user