feat(watch): routine quotidienne cochable sur l'Apple Watch
Pendant natif du backend coach_sportif (commit 772385b, déjà en prod). - CoachRoutineBridge (target App) : pousse blocs + état du jour vers la Watch via updateApplicationContext, relaie les coches Watch vers /api/routine/day puis notifie la WebView (event routineUpdated) - CoachWatch/RoutineStore : état persisté en UserDefaults, reset au changement de jour. Les blocs viennent de l'iPhone — rien codé en dur côté watchOS, donc modifier la routine côté web ne demande aucun rebuild - CoachWatch/RoutineView : liste cochable, progression, haptique, bouton de renvoi si la synchro a échoué - ConnectivityManager (watch) : sendRoutine() en sendMessage avec repli transferUserInfo (coche faite iPhone hors de portée -> livraison différée) ⚠️ CoachLiveBridge touché : routeIfNotLiveSample() aiguille les messages routineDone vers NotificationCenter. WCSession.delegate est unique côté iPhone, impossible d'en ajouter un second. Le flux live workout n'est pas modifié mais doit être re-vérifié au build. Les 3 nouveaux fichiers sont référencés dans project.pbxproj (bonne target) : aucun "Add Files" à faire sur le Mac. Runbook : docs/routine-watch-runbook-mac.md NON COMPILÉ — nécessite une session Xcode sur le Mac mini. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,31 @@ final class ConnectivityManager: NSObject, ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
/// Envoie l'état COMPLET de la routine du jour vers l'iPhone, qui le POSTe
|
||||
/// au serveur. Retourne `false` si rien n'a pu partir (l'appelant affiche
|
||||
/// alors un bouton « Renvoyer »).
|
||||
///
|
||||
/// `sendMessage` quand l'iPhone est joignable, `transferUserInfo` sinon :
|
||||
/// la coche faite pendant une séance, iPhone resté à la maison, partira au
|
||||
/// prochain rapprochement.
|
||||
@discardableResult
|
||||
func sendRoutine(date: String, done: [String]) -> Bool {
|
||||
let session = WCSession.default
|
||||
guard session.activationState == .activated else { return false }
|
||||
let payload: [String: Any] = ["routineDone": done, "date": date]
|
||||
|
||||
if session.isReachable {
|
||||
session.sendMessage(payload, replyHandler: nil) { _ in
|
||||
// Échec temps réel : bascule en livraison différée garantie.
|
||||
session.transferUserInfo(payload)
|
||||
}
|
||||
} else {
|
||||
session.transferUserInfo(payload)
|
||||
}
|
||||
connLog.info("routine envoyée à l'iPhone (\(done.count, privacy: .public) blocs)")
|
||||
return true
|
||||
}
|
||||
|
||||
/// À appeler sur `queue` uniquement.
|
||||
private func flushLocked() {
|
||||
guard !buffer.isEmpty else { return }
|
||||
@@ -86,10 +111,22 @@ extension ConnectivityManager: WCSessionDelegate {
|
||||
|
||||
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
|
||||
storeWidgetSnapshot(applicationContext)
|
||||
storeRoutineSnapshot(applicationContext)
|
||||
}
|
||||
|
||||
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
|
||||
storeWidgetSnapshot(userInfo)
|
||||
storeRoutineSnapshot(userInfo)
|
||||
}
|
||||
|
||||
/// Routine quotidienne poussée par l'iPhone (`CoachRoutineBridge`).
|
||||
/// 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.
|
||||
private func storeRoutineSnapshot(_ dict: [String: Any]) {
|
||||
guard let snapshot = dict["routineSnapshot"] as? [String: Any] else { return }
|
||||
Task { @MainActor in
|
||||
RoutineStore.shared.apply(snapshot: snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
private func storeWidgetSnapshot(_ dict: [String: Any]) {
|
||||
|
||||
Reference in New Issue
Block a user