fix(watch): transmet les blocs de routine, pas seulement l'état coché

pushToWatch ne lisait que `date` et `done` dans l'appel JS et ignorait
`blocks`. La montre recevait donc un snapshot sans aucun bloc et restait sur
son écran vide « Ouvre l'app Coach sur l'iPhone » — alors que le log confirmait
que l'envoi partait bien (constaté sur device le 2026-08-03).

sanitizeBlocks ne garde que les clés attendues en types property-list stricts :
updateApplicationContext lève une exception ObjC sur tout ce qui ne l'est pas,
et les entiers venus du JS arrivent en NSNumber — un `as? Int` direct échoue
quand JavaScriptCore les passe en double (même piège que le score perdu dans
CoachWidgetBridge).

Le log distingue désormais blocs transmis et blocs faits : « 0 blocs » était
ambigu et désignait le compte des cochés, pas celui des blocs envoyés.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Sylvain Bettinelli
2026-08-03 15:53:08 +00:00
parent df843368ce
commit f9f8f95642

View File

@@ -54,10 +54,14 @@ public class CoachRoutineBridgePlugin: CAPPlugin, CAPBridgedPlugin {
// MARK: - JS natif
/// `pushToWatch({ date: "2026-08-03", done: ["hanches", ...] })`
/// `pushToWatch({ date, done: [...], blocks: [{id, num, title, duration_min, moment}] })`
///
/// Les blocs voyagent avec l'état : la montre n'a aucune liste codée en dur,
/// modifier la routine côté web ne demande donc pas de rebuild watchOS.
@objc func pushToWatch(_ call: CAPPluginCall) {
let date = call.getString("date") ?? Self.todayISO()
let done = call.getArray("done", String.self) ?? []
let blocks = Self.sanitizeBlocks(call.getArray("blocks") as? [[String: Any]] ?? [])
guard WCSession.isSupported() else {
call.resolve(["pushed": false, "reason": "WCSession non supporté"])
@@ -75,10 +79,10 @@ public class CoachRoutineBridgePlugin: CAPPlugin, CAPBridgedPlugin {
do {
try session.updateApplicationContext([
"routineSnapshot": ["date": date, "done": done]
"routineSnapshot": ["date": date, "done": done, "blocks": blocks]
])
routineLog.info("routine poussée vers la Watch (\(done.count, privacy: .public) blocs)")
call.resolve(["pushed": true, "count": done.count])
routineLog.info("routine poussée vers la Watch : \(blocks.count, privacy: .public) blocs, \(done.count, privacy: .public) faits")
call.resolve(["pushed": true, "blocks": blocks.count, "done": done.count])
} catch {
routineLog.error("updateApplicationContext a échoué : \(error.localizedDescription, privacy: .public)")
call.reject("Envoi vers la Watch impossible : \(error.localizedDescription)")
@@ -151,6 +155,23 @@ public class CoachRoutineBridgePlugin: CAPPlugin, CAPBridgedPlugin {
}.resume()
}
/// Ne garde que les clés attendues, en types property-list stricts.
///
/// Deux raisons : `updateApplicationContext` rejette (exception ObjC) tout
/// ce qui n'est pas plist-compatible, et les entiers venus du JS arrivent en
/// `NSNumber` un `as? Int` direct échoue quand JavaScriptCore les a passés
/// en double. Même piège que le score perdu dans `CoachWidgetBridge`.
private static func sanitizeBlocks(_ raw: [[String: Any]]) -> [[String: Any]] {
raw.compactMap { b in
guard let id = b["id"] as? String, let title = b["title"] as? String else { return nil }
var out: [String: Any] = ["id": id, "title": title]
out["num"] = (b["num"] as? NSNumber)?.intValue ?? 0
out["duration_min"] = (b["duration_min"] as? NSNumber)?.intValue ?? 0
out["moment"] = (b["moment"] as? String) ?? "matin"
return out
}
}
private static func todayISO() -> String {
let fmt = DateFormatter()
fmt.dateFormat = "yyyy-MM-dd"