---
.../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