Les quatre constats restants, corrigés — dont deux sur le chemin qu'il fallait éviter
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>
This commit is contained in:
@@ -381,13 +381,33 @@ extension WorkoutManager: HKWorkoutSessionDelegate {
|
||||
// sauvegarder la séance, et SEULEMENT ensuite clore la trace en
|
||||
// l'associant au workout obtenu.
|
||||
LocationTracker.shared.stopTracking()
|
||||
try? await builder.endCollection(at: date)
|
||||
/* ⚠️ Les deux `try?` d'avant rendaient indiscernables deux
|
||||
situations OPPOSÉES, toutes deux menant à `workout == nil` :
|
||||
· montre verrouillée à l'arrêt → la séance EST écrite ;
|
||||
· `endCollection` a levé → il n'y a rien à retrouver.
|
||||
Elles tombaient dans le même `else`, et seul le sort de la TRACE
|
||||
était journalisé — pas celui de la séance. Après deux heures de
|
||||
sortie, l'écran de fin restait le même.
|
||||
§ 2.3 du diagnostic du 24/09/2026. La logique de récupération
|
||||
ci-dessous n'a pas changé : seul l'échec devient visible. */
|
||||
var collecteClose = true
|
||||
do {
|
||||
try await builder.endCollection(at: date)
|
||||
} catch {
|
||||
collecteClose = false
|
||||
workoutLog.error("endCollection a echoue : \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
// ⚠️ `finishWorkout()` rend `nil` SANS erreur quand la montre est
|
||||
// verrouillée : ce n'est pas un échec, la séance EST écrite dans
|
||||
// HealthKit — seul l'objet manque. Comme `finishRoute(with:)`
|
||||
// exige un `HKWorkout` non optionnel (vérifié dans la doc Apple),
|
||||
// on va rechercher celle qui vient d'être sauvegardée.
|
||||
let workout = try? await builder.finishWorkout()
|
||||
var workout: HKWorkout?
|
||||
do {
|
||||
workout = try await builder.finishWorkout()
|
||||
} catch {
|
||||
workoutLog.error("finishWorkout a echoue : \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
if let workout {
|
||||
await LocationTracker.shared.finishRoute(with: workout)
|
||||
} else if let recovered = await self.recentlySavedWorkout() {
|
||||
@@ -395,6 +415,18 @@ extension WorkoutManager: HKWorkoutSessionDelegate {
|
||||
await LocationTracker.shared.finishRoute(with: recovered)
|
||||
} else {
|
||||
LocationTracker.shared.discardRoute()
|
||||
if collecteClose {
|
||||
// La collecte s'est close proprement mais rien n'est
|
||||
// retrouvable : anormal, et jamais journalisé jusqu'ici.
|
||||
workoutLog.error("seance introuvable apres un finishWorkout() nil : rien a associer a la trace")
|
||||
} else {
|
||||
workoutLog.error("SEANCE PERDUE : endCollection a leve, rien n'a ete ecrit dans HealthKit")
|
||||
/* Le dire à l'écran. C'est le cœur du constat : l'utilisateur
|
||||
voyait l'écran de fin habituel après une séance perdue.
|
||||
Uniquement dans ce cas — une montre verrouillée n'est pas
|
||||
une perte, et l'annoncer serait une fausse alerte. */
|
||||
self.statusMessage = "Séance non enregistrée — rien n'a pu être écrit dans Santé"
|
||||
}
|
||||
}
|
||||
}
|
||||
Task { @MainActor in
|
||||
@@ -453,9 +485,43 @@ extension WorkoutManager: HKWorkoutSessionDelegate {
|
||||
let message = error.localizedDescription
|
||||
let ns = error as NSError
|
||||
workoutLog.error("session didFailWithError domain=\(ns.domain) code=\(ns.code): \(message)")
|
||||
let builder = workoutSession.associatedWorkoutBuilder()
|
||||
Task { @MainActor in
|
||||
self.statusMessage = "Erreur séance : \(message)"
|
||||
/* ⚠️ Ce chemin ne clôturait RIEN. Comparé à `.ended`, il manquait
|
||||
`stopTracking()`, `endCollection`, `finishWorkout` et
|
||||
`finishRoute`/`discardRoute` — et `self.builder = nil` détruisait
|
||||
la seule référence au builder : la perte devenait définitive dans
|
||||
l'instruction même qui l'ignorait. Le GPS, lui, continuait de
|
||||
tourner après la fin apparente de la séance, sans que rien ne
|
||||
l'indique. § 2.4 du diagnostic du 24/09/2026.
|
||||
|
||||
Le GPS 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.
|
||||
La sauvegarde ensuite, en best-effort — une session qui a échoué
|
||||
refusera souvent de clore, mais ne pas essayer garantit la perte. */
|
||||
LocationTracker.shared.stopTracking()
|
||||
var sauvee: HKWorkout?
|
||||
do {
|
||||
try await builder.endCollection(at: Date())
|
||||
sauvee = try await builder.finishWorkout()
|
||||
} catch {
|
||||
workoutLog.error("cloture apres echec impossible : \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
if let sauvee {
|
||||
workoutLog.info("seance sauvee malgre l'echec de session")
|
||||
await LocationTracker.shared.finishRoute(with: sauvee)
|
||||
self.statusMessage = "Séance interrompue, mais enregistrée"
|
||||
} else if let recovered = await self.recentlySavedWorkout() {
|
||||
workoutLog.info("seance retrouvee dans HealthKit apres l'echec de session")
|
||||
await LocationTracker.shared.finishRoute(with: recovered)
|
||||
self.statusMessage = "Séance interrompue, mais enregistrée"
|
||||
} else {
|
||||
LocationTracker.shared.discardRoute()
|
||||
workoutLog.error("SEANCE PERDUE apres didFailWithError : rien n'a pu etre sauve")
|
||||
self.statusMessage = "Séance perdue : \(message)"
|
||||
}
|
||||
self.isRunning = false
|
||||
self.isPaused = false
|
||||
self.session = nil
|
||||
self.builder = nil
|
||||
ConnectivityManager.shared.sendEnded(reason: "session-failed")
|
||||
|
||||
Reference in New Issue
Block a user