chore(wip): consolidation intermédiaire multi-tickets (sprints Statistiques, UI, Bug resolution, Serveur-client)
Regroupe l'état de travail en cours réalisé dans un même worktree sur plusieurs tickets/sprints (#85, #136, #145, #155-160, #162-164), mélangeant des tickets QA et inProgress. Ne constitue pas une feature terminée : commit de sauvegarde avant triage/split par ticket en branches feature/* dédiées. Exclut les dossiers d'environnement de build locaux et le heap dump parasite (.gitignore mis à jour). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
49
.ideai/tickets/130/carnet.md
Normal file
49
.ideai/tickets/130/carnet.md
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
issueRef: "#130"
|
||||
version: 6
|
||||
updatedBy: {"kind":"user"}
|
||||
updatedAt: 1785243026193
|
||||
---
|
||||
|
||||
## #130 — Cadrage technique Architect (2026-07-27)
|
||||
|
||||
**Root cause identifiée dans le code réel** — bug critique de non-idempotence, confirmé et reproductible par construction (pas besoin du détail score 20/25 pour l'expliquer, mais cohérent avec le scénario rapporté).
|
||||
|
||||
Erreur SQLite 2067 = violation de contrainte `UNIQUE`. La table `active_set_results` (`lib/infrastructure/local/tables.dart:473-513`) porte :
|
||||
```sql
|
||||
UNIQUE (active_workout_session_id, program_index, exercise_index, set_index)
|
||||
```
|
||||
|
||||
`ActiveWorkoutSessionUseCases.recordCurrentSetResult` (`lib/application/use_cases.dart:2451-2511`), appelée par le bouton "Terminer la série" (`workout_execution_screen.dart:1459`, via `_recordAndAdvance`), construit **toujours** un `ActiveSetResult` avec une **metadata/id neuve** (`_newMetadata(ids, originDeviceId, now)`, ligne 2485) puis appelle `sessionRepository.saveSetResult` → `insertOnConflictUpdate` (`drift_repositories.dart:1130-1140`). Drift résout ce conflit sur la **clé primaire** (`id`), pas sur la contrainte `UNIQUE` métier — donc si une ligne existe déjà pour `(session, program, exercise, set)` avec un autre `id`, `insertOnConflictUpdate` ne trouve pas de conflit sur `id`, tente un `INSERT`, et percute l'`UNIQUE` → 2067.
|
||||
|
||||
**Comparer avec le code correct existant** : `upsertSetResultAtPosition` (ligne 2513-2588, utilisée pour l'édition a posteriori d'une série passée) fait ça bien — elle **cherche d'abord** la ligne existante via `listSetResults` (ligne 2546-2555) et réutilise son `metadata`/`id` (`existing.metadata.touch(now)`) si trouvée, sinon en crée une neuve. C'est le pattern à répliquer dans `recordCurrentSetResult`.
|
||||
|
||||
**Deux points d'entrée indépendants écrivent sur la même clé sans coordination**, ce qui rend la collision non hypothétique :
|
||||
1. Le bouton téléphone (`_recordAndAdvance` → `recordCurrentSetResult`).
|
||||
2. Le handler de commande montre `_finishCurrentSet` (`use_cases.dart:3546`, déclenché par `WatchCommandType.finishCurrentSet`/`skipCurrentSet`, ligne 3472-3476) qui appelle **aussi** `recordCurrentSetResult` (ligne 3574) — un chemin d'écriture séparé pour la même position logique.
|
||||
|
||||
Toute situation où une ligne existe déjà pour cette position au moment de l'appel (retry après un premier appel resté en vol, double-tap sans garde métier au niveau du use case, ou commande montre + tap téléphone proches dans le temps) déclenche l'erreur — pas seulement le cas particulier score/reps décrit dans le ticket, mais c'est cohérent avec un scénario où l'utilisateur a interagi via les deux surfaces (téléphone + montre) pour la même série.
|
||||
|
||||
### Correctif (contrat clair pour DevBackend)
|
||||
Faire de `recordCurrentSetResult` un **véritable upsert sur la clé logique**, à l'identique du pattern déjà en place dans `upsertSetResultAtPosition` :
|
||||
```dart
|
||||
final existingResults = await sessionRepository.listSetResults(sessionId);
|
||||
final existing = existingResults.firstWhereOrNull((r) =>
|
||||
r.programIndex == programIndex &&
|
||||
r.exerciseIndex == exerciseIndex &&
|
||||
r.setIndex == setIndex);
|
||||
final result = ActiveSetResult(
|
||||
metadata: existing == null
|
||||
? _newMetadata(ids, originDeviceId, now)
|
||||
: existing.metadata.touch(now),
|
||||
// ... reste inchangé
|
||||
);
|
||||
```
|
||||
Ce correctif rend `recordCurrentSetResult` idempotent et sûr vis-à-vis des deux points d'entrée (téléphone/montre) et de tout retry, sans changer sa signature publique ni le contrat vu par l'UI/la montre.
|
||||
|
||||
### Tests
|
||||
- Use case : appeler `recordCurrentSetResult` deux fois de suite pour la même position (simulant un retry ou une double soumission téléphone/montre) → doit réussir les deux fois sans exception, avec la seconde valeur qui gagne (dernière écriture) et un seul enregistrement en base.
|
||||
- Cas croisé : `recordCurrentSetResult` puis vérifier `listSetResults` ne retourne qu'une ligne pour cette position.
|
||||
- Régression : vérifier que `upsertSetResultAtPosition` reste inchangée et continue de passer ses tests existants.
|
||||
|
||||
**Prêt pour implémentation directe, aucun arbitrage supplémentaire nécessaire. Priorité critique confirmée** (bloque la fin de série dans un cas déjà survenu en usage réel).
|
||||
16
.ideai/tickets/130/issue.md
Normal file
16
.ideai/tickets/130/issue.md
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
id: "b13bf705-ee2e-4c88-b8f4-894468987db8"
|
||||
number: 130
|
||||
title: "[Bug] Erreur sur le bouton terminer la série"
|
||||
status: "closed"
|
||||
priority: "critical"
|
||||
sprint: null
|
||||
links: []
|
||||
agentRefs: [{"agentId":"57695b92-24d0-4876-837c-76116e70a6ae","role":"assigned"}]
|
||||
createdBy: {"kind":"user"}
|
||||
updatedBy: {"kind":"user"}
|
||||
createdAt: 1785133011266
|
||||
updatedAt: 1785243026193
|
||||
version: 6
|
||||
---
|
||||
En voulant temriner une série via le bouton "terminer la série" j'ai une erreur. Je suis en série 1/3, avec un nombre de répétitions à entrer. Mon score n'est pas à nul, il n'y a pas de chrono. J'ai un score de 20 avec un objectif de 25 répétition dans la série. L'erreur dit quelque chose comme: Impossible de terminer la serie SqlLiteException (2067). Visiblement c'est en essayant d'insérer une valeur dans un table active_set_result, les valeurs sont toutes à ?
|
||||
Reference in New Issue
Block a user