Sur demande explicite de Sylvain (« pour le reste vas-y »), après le § 1.2 buildé et vérifié le matin. Les cinq constats de l'audit sont désormais traités. ⚠️ § 2.3 et § 2.4 touchent `WorkoutManager.swift` — la fin de séance du live workout, que la consigne du 22/09 demandait de ne pas approcher. Fait en ne changeant AUCUNE logique : les `try?` deviennent des `do/catch` qui journalisent, et le chemin d'échec gagne la clôture qui lui manquait. La récupération par `recentlySavedWorkout()` est intacte. § 1.3 — `CoachSleep` teste `authorizationStatus` : `.notDetermined` ne se confond plus avec une nuit sans sommeil, et le serveur cessera d'enregistrer des nuits vides sur lesquelles `sleep_score.py` calculait une régularité. Un refus EXPLICITE reste indiscernable d'une absence de données — HealthKit ne le révèle pas en lecture, par conception. L'en-tête du fichier dit maintenant ce que la promesse couvre au lieu de promettre plus. § 2.3 — le `else` final distinguait deux situations opposées qui y tombaient ensemble : montre verrouillée (la séance EST écrite) et `endCollection` qui a levé (rien n'est écrit). Seule la seconde est une perte, et elle seule renseigne `statusMessage` — annoncer une perte sur une montre verrouillée serait une fausse alerte. § 2.4 — `stopTracking()` d'abord : c'est le seul geste qui réussit à coup sûr et le seul dont l'omission se paie en batterie pendant des heures. Puis une tentative de sauvegarde en best-effort, avec `recentlySavedWorkout()` en second recours. `builder = nil` n'intervient plus qu'après les tentatives — il détruisait jusqu'ici la seule référence, rendant la perte définitive dans l'instruction qui l'ignorait. § 2.6 — `NSLock` sur le tampon GPS, comme aux quatre autres endroits du même fichier. ⚠️ Invérifiable sans son déclencheur : un workout à plusieurs HKWorkoutRoute. Sa correction ne se constatera pas plus que le défaut ne se manifestait. ⛔ NON COMPILÉS. Le test qui comptera pour § 2.3 et § 2.4 n'est pas un log : c'est une séance réelle menée jusqu'au bout, puis retrouvée dans Santé et dans coach. Tant qu'elle n'a pas eu lieu, ce sont des intentions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
405 lines
19 KiB
Markdown
405 lines
19 KiB
Markdown
# Diagnostic natif — les cinq constats ouverts de l'audit, vérifiés
|
||
|
||
> ## ⚠️ § 1.2 A ÉTÉ CORRIGÉ le 24/09, après ce diagnostic
|
||
>
|
||
> Sylvain a levé le gel sur ce point précis (« tu attends quoi ? »), puis sur
|
||
> **les quatre autres** (« pour le reste vas-y »). Les cinq sont donc corrigés :
|
||
> § 1.2 buildé et vérifié sur l'appareil, § 1.3 / 2.3 / 2.4 / 2.6 écrits et
|
||
> vérifiés en syntaxe, **pas encore compilés**.
|
||
>
|
||
> ⚠️ **§ 2.3 et § 2.4 touchent `WorkoutManager.swift`**, c'est-à-dire le chemin
|
||
> de fin de séance du live workout — ce que la consigne du 22/09 demandait de ne
|
||
> pas approcher. Fait sur demande explicite, en ne changeant aucune logique :
|
||
> les `try?` deviennent des `do/catch` qui journalisent, et le chemin d'échec
|
||
> gagne la clôture qui lui manquait. **À éprouver en séance réelle avant de
|
||
> considérer que ça tient.**
|
||
>
|
||
> ✅ **BUILD FAIT ET VÉRIFIÉ SUR L'APPAREIL, le 24/09.** Le diagnostic de
|
||
> `/settings` (« Diagnostiquer et pousser ») affiche sur l'iPhone de Sylvain :
|
||
>
|
||
> ```
|
||
> Montre : snapshot reçu
|
||
> Zones envoyées : version frozen du 2026-08-12
|
||
> ```
|
||
>
|
||
> Les quatre causes d'échec silencieux sont donc écartées : session activée,
|
||
> montre appairée, app Watch installée, `updateApplicationContext` sans erreur.
|
||
> Et la date de version arrive au poignet — elle se perdait au transport.
|
||
>
|
||
> ⚠️ **Ce que cela ne prouve pas** : `updateApplicationContext` est asynchrone.
|
||
> iOS a *accepté* le contexte, il le livre quand il peut. Ce qui est établi,
|
||
> c'est que l'iPhone a réussi à le transmettre au système — pas que la montre
|
||
> affiche les bonnes bornes. Seul le poignet le dira, pendant une séance.
|
||
|
||
**24/09/2026. Diagnostic mené en lecture seule** — aucune ligne de Swift n'avait
|
||
été modifiée à sa rédaction.
|
||
Demandé par Sylvain après l'audit du 22/09, dont cinq constats étaient marqués
|
||
**[rapporté]** — issus de revues automatisées, jamais reconfrontés au code.
|
||
L'audit lui-même le disait : « les confirmer avant d'agir ».
|
||
|
||
Ce document dit ce que le code fait **aujourd'hui**, ce que l'audit décrivait
|
||
mal, et ce qu'il faudra corriger le jour où le gel sur le natif sera levé. Il ne
|
||
propose aucun geste : la correction demande un build Mac, et rien n'est réel
|
||
tant que ce build n'a pas tourné.
|
||
|
||
⚠️ **Les numéros de ligne de l'audit ne valent plus.** Les correctifs du 22/09
|
||
au soir ont décalé `WorkoutManager.swift` et `CoachHealthRoute.swift`. Les
|
||
références ci-dessous sont celles du dépôt à `6031be8`.
|
||
|
||
---
|
||
|
||
## Verdict d'ensemble
|
||
|
||
**Les cinq sont confirmés.** Aucun faux positif. Deux sont plus graves que
|
||
l'audit ne le disait, un est plus nuancé.
|
||
|
||
| # | Constat | Vérifié | Écart avec l'audit |
|
||
|---|---|---|---|
|
||
| 1.2 | Zones cardiaques muettes vers la montre | ✅ | **Aggravé** : 4 sorties muettes, pas 1 |
|
||
| 1.3 | « Refusé » ≡ « aucune donnée » (sommeil) | ✅ | Exact |
|
||
| 2.3 | Séance perdue sans log | ✅ | **Nuancé** : `nil` n'est pas toujours une perte |
|
||
| 2.4 | `didFailWithError` laisse le GPS allumé | ✅ | **Aggravé** : `builder = nil` rend la récupération impossible |
|
||
| 2.6 | Tampon GPS sans verrou | ✅ | Exact |
|
||
|
||
---
|
||
|
||
## 1.2 — Les zones cardiaques peuvent ne jamais atteindre la montre
|
||
|
||
**`ios/App/App/CoachWidgetBridge.swift:175-183`** (`pushToWatch`)
|
||
|
||
L'audit ne signalait que le `try?`. Il y a **quatre** sorties silencieuses sur
|
||
sept lignes :
|
||
|
||
```swift
|
||
guard WCSession.isSupported() else { return } // 1. muet
|
||
let session = WCSession.default
|
||
guard session.activationState == .activated, // 2. muet
|
||
let data = try? JSONEncoder().encode(snapshot) else { return } // 3. muet
|
||
try? session.updateApplicationContext(["widgetSnapshot": data]) // 4. avalé
|
||
```
|
||
|
||
Aucun log, aucune remontée au JS. Le snapshot porte `zones: HeartRateZones?`
|
||
(`CoachWidgetSnapshot.swift:96`) — les bornes Z1…Z5 et `clinicalTargetBpm`, la
|
||
cible sous bêtabloquant.
|
||
|
||
**Ce que fait le chemin voisin, dans le même dépôt**
|
||
(`CoachRoutineBridge.swift:87-101`) pour la routine de gainage : il vérifie
|
||
`isPaired` **et** `isWatchAppInstalled` avec une raison explicite, entoure
|
||
l'appel d'un `do/catch`, journalise l'échec et appelle `call.reject`.
|
||
|
||
Le chemin instrumenté porte des exercices de mobilité. Le chemin muet porte les
|
||
bornes cardiaques d'un patient post-infarctus.
|
||
|
||
### L'aggravant est réel, et il se compose avec le premier
|
||
|
||
`HeartRateZones` (`ios/App/App/HeartRateZones.swift`) ne porte **aucune date** :
|
||
ni `version`, ni `validFrom`, ni `computedAt`. Or le backend sert une version
|
||
**figée et datée** (`data/hr_zones_history.json`, via `app.current_zones()`) —
|
||
c'est même une règle non négociable du projet web (règle 9 du CLAUDE.md).
|
||
|
||
**La date existe côté serveur et se perd au transport.**
|
||
|
||
Et `CoachWidgetSnapshot.asOf(_:)` périme l'eau, le café, les kcal, la routine,
|
||
l'activité du jour et le score de forme — mais **pas `zones`**, délibérément
|
||
(« version figée, valable des semaines »). Le raisonnement est bon pour une
|
||
péremption journalière ; il laisse ouvert le cas d'une **révision** des bornes.
|
||
|
||
Conséquence composée : après un `POST /api/cardiac-zones/confirm`, si le push
|
||
échoue silencieusement, la montre affiche les anciennes bornes **sans limite de
|
||
temps et sans que rien ne le signale**, ni au poignet ni à l'iPhone.
|
||
|
||
**À corriger, le jour venu** : aligner `pushToWatch` sur `CoachRoutineBridge`
|
||
(do/catch, log, `isPaired`/`isWatchAppInstalled`), et faire voyager la date de
|
||
version des zones pour que la montre puisse dire « bornes du 12/08 » plutôt que
|
||
d'afficher un chiffre sans âge.
|
||
|
||
---
|
||
|
||
## 1.3 — « Accès refusé » et « aucune donnée » sont rendus identiques
|
||
|
||
**`ios/App/App/CoachSleep.swift`** — en-tête ligne 30 :
|
||
|
||
> `// r.available = false si HealthKit est indisponible ou non autorisé`
|
||
|
||
Le code teste `HKHealthStore.isHealthDataAvailable()` (l. 59, 63) et la
|
||
disponibilité du type (l. 68). **`authorizationStatus(for:)` n'apparaît nulle
|
||
part dans le fichier.** La requête aboutit et rend :
|
||
|
||
```swift
|
||
call.resolve(["available": true, "samples": out, ...]) // l. 92-96
|
||
```
|
||
|
||
La documentation du fichier promet donc quelque chose que le code ne fait pas.
|
||
|
||
**La nuance de l'audit est juste** : HealthKit ne distingue pas volontairement
|
||
« refusé » de « vide » **en lecture** — c'est une protection de vie privée
|
||
documentée par Apple. Mais `authorizationStatus(for:)` renvoie `.notDetermined`
|
||
de façon fiable, et c'est le cas fréquent : réinstallation de l'app,
|
||
autorisation jamais accordée. C'est celui-là qu'on peut distinguer.
|
||
|
||
**Conséquence côté serveur** : `sleep_score.py` calcule une régularité de
|
||
coucher sur des nuits absentes, sans savoir qu'elles sont absentes parce qu'on
|
||
ne nous laisse plus regarder. Une valeur fausse, pas une valeur manquante.
|
||
|
||
Même famille dans `CoachHealthRoute.swift` (l. 173 et 277 selon l'audit — à
|
||
revérifier, les lignes ont bougé).
|
||
|
||
---
|
||
|
||
## 2.3 — Une séance peut disparaître sans un seul log
|
||
|
||
**`ios/App/CoachWatch/WorkoutManager.swift:383-398`**
|
||
|
||
```swift
|
||
LocationTracker.shared.stopTracking()
|
||
try? await builder.endCollection(at: date)
|
||
let workout = try? await builder.finishWorkout()
|
||
if let workout { … } else if let recovered = await self.recentlySavedWorkout() { … }
|
||
else { LocationTracker.shared.discardRoute() }
|
||
```
|
||
|
||
**Ce que l'audit ne disait pas, et qui change la lecture** : le commentaire du
|
||
code explique que `finishWorkout()` rend `nil` **sans erreur** quand la montre
|
||
est verrouillée, et que dans ce cas la séance **est** écrite dans HealthKit —
|
||
seul l'objet manque. C'est pour ça que `recentlySavedWorkout()` existe. Un `nil`
|
||
n'est donc pas une perte en soi, et le code le sait.
|
||
|
||
**Le défaut est plus précis que « la séance est perdue » :** le double `try?`
|
||
efface la différence entre deux situations opposées —
|
||
|
||
| Ce qui s'est passé | `workout` | La séance est-elle sauvée ? |
|
||
|---|---|---|
|
||
| Montre verrouillée à l'arrêt | `nil` | **Oui**, `recentlySavedWorkout()` la retrouve |
|
||
| `endCollection` a levé | `nil` | **Non**, il n'y a rien à retrouver |
|
||
|
||
Les deux chemins convergent vers le même `else`, et `discardRoute()` ne
|
||
journalise que la perte de la **trace**. `statusMessage` n'est pas touché :
|
||
après deux heures de sortie, l'utilisateur voit l'écran de fin habituel.
|
||
|
||
**À corriger** : remplacer les deux `try?` par un `do/catch` qui journalise
|
||
l'erreur — sans changer la logique de récupération, qui est bonne.
|
||
|
||
---
|
||
|
||
## 2.4 — `didFailWithError` ne clôt rien et laisse le GPS allumé
|
||
|
||
**`ios/App/CoachWatch/WorkoutManager.swift:451-464`**
|
||
|
||
```swift
|
||
workoutLog.error("session didFailWithError domain=… code=…: \(message)")
|
||
Task { @MainActor in
|
||
self.statusMessage = "Erreur séance : \(message)"
|
||
self.isRunning = false
|
||
self.session = nil
|
||
self.builder = nil
|
||
ConnectivityManager.shared.sendEnded(reason: "session-failed")
|
||
}
|
||
```
|
||
|
||
Comparé au chemin `.ended` (l. 383-398), il manque **tout** : `stopTracking()`,
|
||
`endCollection`, `finishWorkout`, `finishRoute`/`discardRoute`.
|
||
|
||
**Plus grave que l'audit ne le dit** : `self.builder = nil` détruit la seule
|
||
référence au builder. Après cette ligne, plus aucun code ne peut tenter de
|
||
sauver ce qu'il avait collecté — la perte devient définitive dans la même
|
||
instruction qui l'ignore.
|
||
|
||
Deux conséquences distinctes :
|
||
1. **Le GPS continue de tourner.** `LocationTracker` n'est jamais arrêté, alors
|
||
que la séance est finie du point de vue de l'interface. Sur une montre, ça se
|
||
paie en batterie, et rien ne l'indique.
|
||
2. **Les données collectées sont jetées sans tentative de sauvegarde.**
|
||
|
||
⚠️ À la différence de § 2.3, ici `statusMessage` **est** renseigné : l'utilisateur
|
||
voit « Erreur séance ». Il sait qu'il s'est passé quelque chose ; il ne sait pas
|
||
qu'il a perdu la séance, et le GPS tourne toujours.
|
||
|
||
---
|
||
|
||
## 2.6 — Accès concurrent non protégé sur le tampon GPS
|
||
|
||
**`ios/App/App/CoachHealthRoute.swift:183-205`** (`collectLocations`)
|
||
|
||
```swift
|
||
var all: [CLLocation] = []
|
||
let group = DispatchGroup()
|
||
var pluginErr: Error?
|
||
for route in routes {
|
||
group.enter()
|
||
let q = HKWorkoutRouteQuery(route: route) { _, locations, done, err in
|
||
if let err = err { pluginErr = err; group.leave(); return }
|
||
if let locs = locations { all.append(contentsOf: locs) } // ⚠️ sans verrou
|
||
…
|
||
```
|
||
|
||
`all` et `pluginErr` sont mutés depuis les callbacks de N requêtes, livrées sur
|
||
des files HealthKit indépendantes. **Aucun verrou.**
|
||
|
||
**Le même fichier utilise `NSLock` pour ce motif exact à quatre endroits** :
|
||
lignes 283, 380, 442 et 565. Ce n'est donc pas une convention absente du
|
||
projet — c'est une omission à un seul endroit.
|
||
|
||
**Déclencheur** : un workout portant plusieurs `HKWorkoutRoute` (multisport, ou
|
||
reprise après interruption). Avec une seule route, la boucle n'a qu'une
|
||
itération et le défaut est inobservable.
|
||
|
||
⚠️ **On ne sait pas s'il s'est déjà produit** : les routes vivent dans
|
||
HealthKit, pas sur le serveur, et rien ne journalise `routes.count`. Une
|
||
corruption de tableau en Swift ne lève pas forcément — elle peut donner une
|
||
trace tronquée ou un crash, sans rien qui désigne la cause.
|
||
|
||
---
|
||
|
||
## Ce que ce diagnostic ne dit pas
|
||
|
||
- **Aucune de ces corrections n'a été écrite.** Le gel du 22/09 sur le natif
|
||
tient ; Sylvain a autorisé la lecture, pas la modification.
|
||
- **Rien n'est vérifiable sans build Mac.** Ces cinq constats sont établis par
|
||
lecture du code, pas par exécution. Une lecture peut manquer un chemin.
|
||
- **La dette assumée du 22/09 reste ouverte** : le `defer { completion() }` de
|
||
`CoachWorkoutObserver` déclare la livraison traitée alors que les requêtes
|
||
sont encore en vol, sans `beginBackgroundTask`. Elle n'est pas revue ici, elle
|
||
était déjà documentée et assumée.
|
||
- **Le § 4 de l'audit (gravité 3, justesse et cohérence) n'a pas été revérifié.**
|
||
|
||
## Si un seul devait être corrigé
|
||
|
||
**§ 1.2**, et pas parce qu'il est le plus spectaculaire : parce que c'est le seul
|
||
dont la conséquence est un **chiffre faux affiché pendant l'effort**, chez
|
||
quelqu'un qui règle son intensité dessus. Les quatre autres perdent de la donnée
|
||
ou de la batterie — c'est réparable après coup. Une borne de zone périmée au
|
||
poignet ne se rattrape pas : elle a déjà servi à décider.
|
||
|
||
---
|
||
|
||
## Ce qui a été corrigé (§ 1.2, le 24/09 après-midi)
|
||
|
||
Trois couches, parce que le défaut en traversait trois.
|
||
|
||
### 1. L'envoi ne se tait plus — `CoachWidgetBridge.swift`
|
||
|
||
`pushToWatch` rend désormais la **raison** de son échec, ou `nil` s'il a réussi,
|
||
et journalise dans `Logger(subsystem: "ch.hypnotruck.coach", category: "widget")`.
|
||
Les quatre sorties muettes deviennent **six sorties nommées** :
|
||
WatchConnectivity non supportée · session non activée · **aucune montre
|
||
appairée** · **app Watch non installée** · encodage impossible · envoi refusé.
|
||
Les deux gardes en gras viennent de `CoachRoutineBridge` : sans elles, l'absence
|
||
de montre se confondait avec un échec d'envoi.
|
||
|
||
Le log de succès nomme ce qui compte : `zones=oui depuis=2026-08-12`. Savoir
|
||
quelles bornes sont au poignet, et depuis quand, sans ouvrir la montre.
|
||
|
||
⚠️ **Un échec ne fait PAS échouer `setSnapshot`**, contrairement à
|
||
`CoachRoutineBridge` où l'envoi *est* l'opération. Ici l'App Group est déjà écrit
|
||
et les widgets iPhone rafraîchis : une montre verrouillée n'invalide rien. D'où
|
||
un `resolve` enrichi (`watchPushed`, `watchReason`) et non un `reject` — `ok`
|
||
est conservé, aucun appelant JS existant ne casse.
|
||
|
||
### 2. La date de version voyage — `HeartRateZones.swift` + `widget-bridge.js`
|
||
|
||
`zonesSince` (date de la version figée) et `zonesFrom` (`"frozen"` ou `"live"`)
|
||
rejoignent le modèle, en optionnels — un snapshot écrit avant reste décodable.
|
||
|
||
**Le serveur les servait déjà** : `/api/cardiac-zones` rend `zones_since` et
|
||
`zones_from` depuis `hr_zones_history.effective()`. Rien à changer côté backend ;
|
||
c'est `widget-bridge.js` qui ne les faisait pas monter dans le snapshot. Deux
|
||
lignes.
|
||
|
||
La montre peut donc maintenant distinguer des bornes **validées** de bornes
|
||
**provisoires**, et dire leur âge. Ce qu'elle en affiche reste à décider — c'est
|
||
de l'UI watchOS, hors de ce correctif.
|
||
|
||
### 3. Le garde-fou — `tests-linux/`
|
||
|
||
Trois tests dans `DayRolloverTests` : la date survit au passage de minuit
|
||
(`asOf`), elle survit à l'aller-retour JSON (ce qui n'est pas encodé n'arrive
|
||
pas au poignet), et un snapshot **antérieur** à ces champs se décode toujours —
|
||
un décodage qui lèverait laisserait la montre sans zones du tout, pire que des
|
||
zones sans date. La doublure `HeartRateZonesStub.swift` est tenue à l'identique.
|
||
|
||
91 tests verts (88 avant).
|
||
|
||
### Ce que ce correctif ne fait pas
|
||
|
||
- **Il ne prouve pas que le push fonctionne.** Il rend son échec *visible*. Si
|
||
l'envoi échouait pour une raison qu'on ignore, on le saura au prochain build —
|
||
c'était impossible jusqu'ici.
|
||
- **Il ne périme pas les zones.** Après une révision, la montre affichera
|
||
toujours les anciennes bornes tant qu'un push n'a pas réussi ; elle saura
|
||
seulement les dater. Décider qu'une borne de plus de N jours n'est plus
|
||
affichable est un choix produit, pas une correction de défaut.
|
||
- **Il ne touche pas au flux course à pied** (WorkoutKit, live workout FC, app
|
||
Exercice), conformément à la consigne du 22/09.
|
||
- ✅ **Compilé et vérifié le 24/09** — sans Console : le diagnostic de
|
||
`/settings` rend le statut en clair (`Montre : snapshot reçu` · `Zones
|
||
envoyées : version frozen du 2026-08-12`), parce que le plugin remonte
|
||
désormais `watchPushed` / `watchReason` jusqu'au JS.
|
||
⏳ **Reste à voir au poignet** : que la montre affiche ces bornes pendant une
|
||
séance. L'envoi est accepté par iOS ; la livraison est asynchrone.
|
||
|
||
---
|
||
|
||
## Les quatre autres, corrigés le 24/09 (« pour le reste vas-y »)
|
||
|
||
⛔ **Aucun n'est compilé.** Syntaxe vérifiée (`swiftc -parse`), 91 tests du banc
|
||
Linux verts — mais ces fichiers importent HealthKit, WatchKit et Capacitor,
|
||
absents de Swift pour Linux. Le banc ne les couvre pas et ne peut pas les
|
||
couvrir.
|
||
|
||
### § 1.3 — `.notDetermined` n'est plus confondu avec une nuit vide
|
||
|
||
`CoachSleep.readSamples` teste désormais `authorizationStatus(for: sleepType)`
|
||
et rend `{available: false, reason: "authorization not determined"}`.
|
||
|
||
⚠️ **Ce qui n'est PAS corrigé, et ne peut pas l'être** : un refus *explicite*
|
||
reste indiscernable d'une absence de données. HealthKit ne le révèle pas en
|
||
lecture, par conception — c'est une protection de vie privée, pas un oubli
|
||
d'Apple. Seul `.notDetermined` est fiable, et c'est le cas fréquent
|
||
(réinstallation, permission jamais demandée). L'en-tête du fichier dit
|
||
maintenant exactement ce que la promesse couvre, au lieu de promettre plus.
|
||
|
||
### § 2.3 — « séance sauvée » et « rien de sauvé » ne se confondent plus
|
||
|
||
Les deux `try?` deviennent des `do/catch`. **La logique de récupération n'a pas
|
||
changé** — `recentlySavedWorkout()` reste le repli quand `finishWorkout()` rend
|
||
`nil` sur montre verrouillée. Ce qui change : un `endCollection` qui lève est
|
||
journalisé, et le `else` final distingue les deux cas, dont un seul est une
|
||
perte. Dans ce cas seulement, `statusMessage` le dit à l'écran — une montre
|
||
verrouillée n'est pas une perte, et l'annoncer serait une fausse alerte.
|
||
|
||
### § 2.4 — le GPS s'arrête, et la séance est sauvée si elle peut l'être
|
||
|
||
`didFailWithError` commence par `stopTracking()` : c'est le seul geste qui
|
||
réussit à coup sûr, et le seul dont l'omission se paie en batterie pendant des
|
||
heures. Puis une tentative de clôture en best-effort (`endCollection`,
|
||
`finishWorkout`), avec `recentlySavedWorkout()` en second recours — une session
|
||
qui a échoué refusera souvent de clore, mais ne pas essayer garantissait la
|
||
perte. `builder = nil` n'intervient plus qu'à la fin, après les tentatives.
|
||
|
||
Trois issues, trois messages distincts : « Séance interrompue, mais
|
||
enregistrée », « Séance perdue : … », ou le message d'erreur d'origine.
|
||
|
||
### § 2.6 — le tampon GPS est verrouillé
|
||
|
||
`collectLocations` protège `all` et `pluginErr` par un `NSLock`, comme les
|
||
quatre autres endroits du même fichier le faisaient déjà.
|
||
|
||
⚠️ **Invérifiable sans le déclencheur** : il faut un workout portant plusieurs
|
||
`HKWorkoutRoute` (multisport, reprise après interruption). Avec une seule route,
|
||
la boucle n'a qu'une itération — c'est pourquoi le défaut a pu vivre longtemps
|
||
sans se manifester, et pourquoi sa correction ne se constatera pas non plus.
|
||
|
||
### Ce qu'il faut regarder au prochain build
|
||
|
||
| Quoi | Où | Ce qui prouve |
|
||
|---|---|---|
|
||
| § 1.3 | `/settings` → synchro santé | Retirer « Sommeil » dans Réglages → Santé, réinstaller : la synchro doit dire « non autorisé », pas enregistrer des nuits vides |
|
||
| § 2.3 | Console, catégorie `workout` | Une séance normale ne doit RIEN journaliser de neuf. Les nouveaux logs ne sortent qu'en cas d'échec |
|
||
| § 2.4 | Batterie, après une séance interrompue | Le GPS ne doit plus tourner une fois l'écran de fin affiché |
|
||
| § 2.6 | — | Rien à voir : le correctif supprime une corruption qui ne se manifestait pas de façon lisible |
|
||
|
||
⚠️ **§ 2.3 et § 2.4 sont sur le chemin de fin de séance du live workout.** Le
|
||
test qui compte n'est pas un log, c'est **une séance réelle menée jusqu'au bout,
|
||
puis retrouvée dans Santé et dans coach**. Tant qu'elle n'a pas eu lieu, ces
|
||
deux correctifs sont des intentions.
|