Trois défauts du même chemin, tous vérifiés ligne à ligne avant correction.
1. Toutes les séances d'un lot sauf une étaient perdues DÉFINITIVEMENT.
L'ancre est sauvegardée ligne 298, puis seul `latest` était traité ligne 319 —
or scheduleNotification était le seul appelant de reportToRoutineIfStrength et
reportPlanOrigin. Renfo le matin, vélo le soir, iPhone resté à la maison : au
premier fire la requête ancrée rend les deux, le vélo est notifié, le renfo
n'est jamais POSTé, et l'ancre ayant avancé il ne reviendra jamais. Le log
disait « 2 new workout(s), notifying latest » : exact, et parfaitement muet sur
ce qu'il venait de jeter. Les rapports serveur bouclent désormais sur tout le
lot ; la notification reste unique, un « 🎉 » par séance serait du bruit.
2. Le statut HTTP n'était jamais lu. Les deux POST faisaient
`{ data, _, error in }` et ne regardaient que `error`. Un 401 sur token rotaté,
un 404 ou un 500 n'en produisent pas : reportPlanOrigin journalisait donc
« plan-origin envoyé » sur une requête refusée, et auto-complete sortait en
silence au guard de parsing. Un helper `envoyer(_:libelle:traiter:)` vérifie
maintenant (200..<300) et journalise le code — le pattern existait déjà dans
CoachRoutineBridge.postDay, à deux fichiers de là.
3. Une séance de musculation classique était étiquetée fonctionnelle.
L'activité était écrite en dur alors que le garde accepte aussi
traditionalStrengthTraining. Si la règle de correspondance côté serveur
discrimine sur ce champ, l'appariement était faussé. L'UUID et l'heure de début
sont ajoutés au passage : sans eux le serveur ne peut pas dédupliquer et doit
évaluer sa fenêtre de 12 h depuis l'heure de réception — une séance remontée
avec six heures de retard était datée de son arrivée.
⚠️ PAS corrigé, et assumé : le `defer { completion() }` dit à iOS que la
livraison est traitée alors que les requêtes sont encore en vol, sans
beginBackgroundTask. Le faire proprement demande un import UIKit, une bascule
sur le fil principal et un essai sur appareil — à traiter à part plutôt qu'en
marge. Le statut HTTP étant désormais lu, un échec laissera au moins une trace.
Relevé § 2.1, § 2.2 et § 3.2 de docs/AUDIT-2026-09-22.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>