--- issueRef: "#175" version: 4 updatedBy: {"kind":"user"} updatedAt: 1785315994252 --- ## #175 — Cadrage Main (2026-07-28) ### Symptôme utilisateur Si l'utilisateur tue l'app téléphone sans quitter d'abord la séance, la montre conserve une séance active apparente et continue d'ouvrir l'ancien écran de séance. ### Gravité Critique côté cohérence : - la montre affirme un état actif sans source de vérité confirmée ; - l'utilisateur peut croire que la séance continue alors que le téléphone a disparu du cycle ; - cela contredit le principe "logique métier côté téléphone". ### Décision de cadrage La montre ne doit pas garder indéfiniment un état "séance active" orphelin. Comportement cible à trancher techniquement : - soit le téléphone envoie explicitement un état terminal/deconnexion avant extinction contrôlée ; - soit la montre invalide l'état de séance après une fenêtre d'absence de heartbeat/projection ; - soit combinaison des deux, avec dégradation vers un état clair "Téléphone indisponible" ou retour hors séance. ### Garde-fous - ne pas perdre une vraie séance lors d'une micro-coupure normale ; - ne pas laisser une séance fantôme persister après fermeture forcée ; - distinguer reconnexion brève et disparition durable du téléphone ; - l'écran montre doit refléter un état confirmé ou explicitement dégradé, jamais un faux "tout va bien". ### Attendu suite Ticket d'architecture/implémentation à sortir de celui-ci : - stratégie heartbeat / TTL projection ; - comportement UI montre si la source téléphone disparaît ; - tests kill téléphone / reprise / relance app. ## Cadrage Architect (2026-07-28) Statut : **cadrage exploitable, prêt pour découpage dev**. ### Diagnostic Le bug vient du mécanisme de reprise montre : au démarrage ou à la reconnexion, la montre relit le dernier `DataItem` de projection et continue de l'afficher même si le téléphone a disparu. Le `WatchSessionViewModel` peut marquer la projection `stale` puis `connectionLost`, mais n'invalide jamais la projection elle-même. ### Décision d'architecture - Le téléphone reste l'unique source de vérité. - La projection montre doit être traitée comme un **cache temporaire**, jamais comme une preuve durable qu'une séance est encore active. - Une projection phone -> watch devient **expirable**. - Sans rafraîchissement explicite du téléphone dans une fenêtre courte, la montre doit considérer la projection invalide et revenir à un état `téléphone indisponible / aucune séance confirmée`. ### Mécanisme recommandé - Étendre `WatchSessionProjection` avec un TTL explicite, par exemple `expiresAtEpochMs`. - À chaque projection publiée, le téléphone renseigne `expiresAtEpochMs = now + ttl`. - Recommandation : TTL de `12s`, cohérent avec le refresh périodique existant toutes les `2s`. - Si l'app téléphone est killée, le heartbeat s'arrête ; la montre laisse expirer la projection. - Si le téléphone sait explicitement qu'il n'y a plus de séance, il publie `phase = noActiveSession` immédiatement comme aujourd'hui ; le TTL couvre le cas brutal. ### Comportement montre attendu - Avant expiration : garder l'écran courant, avec l'état `stale` existant si utile. - Après expiration durable : - remplacer localement la projection active par une projection synthétique `noActiveSession` ; - vider `sensorSample`, commandes pending, score optimiste ; - arrêter ongoing / foreground liés à la séance ; - afficher un état type `Téléphone indisponible / rouvre GameTime sur le téléphone`, pas l'ancienne séance. - Si le téléphone republie ensuite une projection valide, la montre se recale normalement. ### Garde-fous micro-coupures - Ne pas invalider sur simple `connectionLost` capability. - Invalider seulement quand aucune projection fraîche n'est reçue au-delà du TTL et qu'aucun ack/commande retour ne prouve une communication en cours. - Fenêtres recommandées : - refresh téléphone : `2s` inchangé ; - seuil `stale` : `6s` actuel conservé ; - expiration dure : `12s` à `15s`. ### Impacts - Contrat : `WatchSessionProjection` gagne `expiresAtEpochMs`, `schemaVersion` incrémentée. - Téléphone : renseigne ce champ à chaque publish. - Bridge natif watch : pas de refonte structurelle ; la lecture du dernier `DataItem` peut rester, mais ce snapshot sera auto-invalidé par TTL. - UI montre : ajouter l'invalidation locale dans `_syncFreshnessState()` ; injecter une projection synthétique `noActiveSession` après expiration. ### Lots recommandés 1. `B1 [DevBackend] Projection expirable côté téléphone` 2. `F1 [DevFrontend] Invalidation locale montre sur TTL expiré` 3. `F2 [DevFrontend] Ajustements natifs reprise/ongoing` 4. `QA1 [QA] Validation disparition source téléphone`