feat(data): refresh périodique non bloquant des données statiques #14

Open
opened 2026-08-23 18:43:45 +02:00 by CyrilLeblanc · 0 comments
Owner

Contexte

Après les deux tickets précédents (CI de publication sur le registry + bootstrap téléchargé au premier lancement), les données statiques en base ne sont plus jamais rafraîchies après l'install. Or le diagnostic du 23 août 2026 a montré que l'API M réso ré-identifie ses clusters en continu (~336 IDs changés en 2 mois) : une base qui vieillit reproduit le bug des « poteaux muets » (HTTP 204 sur les vieux IDs) — c'est exactement ce qu'on veut éliminer.

La fondation existe déjà après les tickets précédents : generatedAt stocké en base (migration Room du ticket bootstrap), manifest latest lisible anonymement en ~150 octets, importBundledData() transactionnel réutilisable tel quel (une base n'est jamais vidée avant l'import : un refresh raté laisse les données actives intactes).

Décisions déjà actées :

  • Refresh non bloquant avec bannière — jamais de gate forcé sur une base déjà remplie.
  • Politique de déclenchement initiale : check au démarrage de l'app (comparaison generatedAt local vs manifest) + bouton manuel dans les réglages/à propos. La périodicité stricte (ex. « au plus tard tous les 30 jours ») est un paramètre ajustable, pas une mécanique dédiée.
  • Ce ticket est le 3e de la dépendance : ne pas démarrer avant que le ticket bootstrap soit mergé.

Comportement attendu

Au lancement (app déjà initialisée) :

  1. L'app démarre normalement sur la map, immédiatement — aucun blocage.
  2. En tâche de fond (après le rendu initial), GET anonyme de latest/manifest.json.
  3. Si manifest.generatedAt > generatedAt local (comparaison ISO-8601) et version ≠ → afficher une bannière discrète : « Des données plus récentes sont disponibles » + bouton Mettre à jour.
  4. Appui sur Mettre à jour → même pipeline que le bootstrap (download → sha256 → import transactionnel), avec indicateur de progression non bloquant (bannière qui montre la progression, app restant utilisable — ou état « en cours » si l'équipe préfère bloquer doucement pendant l'import SQLite, à trancher à l'implémentation).
  5. Refresh réussi → bannière de confirmation brève, generatedAt local mis à jour, les nouvelles données sont utilisées (recharger les caches mémoire sequences/geometries du TransitContainer).
  6. Refresh raté (offline, 5xx, sha256 mismatch) → bannière d'erreur non intrusive avec Réessayer ; les données actuelles restent intactes et actives.

Bouton manuel : dans l'écran réglages/à propos (à créer minimal si inexistant — un simple item suffit), affiche la fraîcheur actuelle (« Données du 23 août 2026 ») et force le check + refresh.

Détails :

  • Le check de démarrage est throttlé : pas plus d'une fois toutes les 24 h (timestamp du dernier check en DataStore/SharedPreferences), pour ne pas ticker le registry à chaque ouverture d'app.
  • Si offline au check → silencieux (pas de bannière d'erreur au démarrage ; le check retentera au prochain lancement).
  • Un version de manifest identique au local → ne rien afficher.

Pistes d'implémentation

  • Domain : use case domain/RefreshStaticData.kt — réutilise le client du registry et importBundledData() du ticket bootstrap ; la décision needsRefresh(localGeneratedAt, manifest) est une fonction pure (comparaison de chaînes ISO normalisées, testable).
  • État : sealed interface RefreshState { Idle, Checking, Available(version), Downloading(bytesRead, bytesTotal), Importing, Done, Failed(error) } exposé par un ViewModel (étendre TransitViewModel ou VM dédié — au choix de l'implémentation, mais événements via TransitEventBus si cross-VM).
  • Après import réussi : recharger les données en mémoire (le TransitContainer expose sequences/geometries @Volatile — les réassigner depuis la base, et notifier les VMs via l'event bus pour reconstruire couches map/StopIndex/ClusterIndex).
  • Bannière : composant discret en haut (sous la top bar), material 3, ne pas obstruer la map ; textes FR dans strings.xml.
  • Throttle du check : SharedPreferences ("last_data_check_epoch_ms") — simple et suffisant, pas besoin de DataStore pour deux clés.
  • Le download de refresh écrit dans le même fichier temp/cache que le bootstrap (réutiliser le client, factoriser le pipeline commun bootstrap/refresh en un objet domaine partagé — attention à ne pas dupliquer le code de download/sha256).

Critères d'acceptation

  • Avec une base plus ancienne que le manifest publié : au lancement, la map s'affiche immédiatement et la bannière « données plus récentes » apparaît sans interaction.
  • Appui sur « Mettre à jour » : progression visible, app utilisable pendant le téléchargement, données rechargées à la fin (nouvelles lignes/arrêts visibles sur la map sans redémarrage manuel).
  • Kill de l'app pendant le refresh : au relancement, les données anciennes sont intactes et actives (vérifier sentinel/rollback), la bannière se ré-affiche.
  • Refresh avec sha256 corrompu (proxy de test) : bannière d'erreur, données locales intactes.
  • Offline au lancement : aucune bannière d'erreur, app normale, check retenté au lancement suivant (throttle 24 h respecté — vérifier avec un timestamp artificiellement vieux).
  • Le check de démarrage ne s'exécute pas plus d'une fois / 24 h (logs ou SharedPreferences vérifiables).
  • Manifest identique au local → aucune bannière.
  • L'écran réglages/à propos affiche la date de fraîcheur locale et permet de forcer le refresh.
  • Après un refresh réussi, generatedAt en base vaut celui du manifest téléchargé.
  • Aucune duplication du pipeline download/sha256/import entre bootstrap et refresh (objet domaine partagé).
  • Textes FR externalisés, mêmes standards de code que le ticket bootstrap (pas de !!, constantes extraites, <300 lignes/fichier).

Notes / captures (optionnel)

  • Ordre des tickets : CI (données publiées) → bootstrap (premier lancement téléchargé) → ce ticket.
  • La comparaison de fraîcheur se fait sur generatedAt ISO-8601 (tri lexicographique valide si format uniforme yyyy-MM-dd'T'HH:mm:ss'Z' — normaliser à la génération).
  • Le cas « l'API M réso a cassé les IDs et la CI a publié des données dégénérées malgré les garde-fous » reste possible : c'est la raison d'être des versions datées immuables — un rollback manuel = re-PUT du pointeur latest vers une version antérieure saine (pas de mécanisme app à prévoir ici).
## Contexte Après les deux tickets précédents (CI de publication sur le registry + bootstrap téléchargé au premier lancement), les données statiques en base ne sont plus jamais rafraîchies après l'install. Or le diagnostic du 23 août 2026 a montré que l'API M réso ré-identifie ses clusters en continu (~336 IDs changés en 2 mois) : une base qui vieillit reproduit le bug des « poteaux muets » (HTTP 204 sur les vieux IDs) — c'est exactement ce qu'on veut éliminer. La fondation existe déjà après les tickets précédents : `generatedAt` stocké en base (migration Room du ticket bootstrap), manifest `latest` lisible anonymement en ~150 octets, `importBundledData()` transactionnel réutilisable tel quel (une base n'est jamais vidée avant l'import : un refresh raté laisse les données actives intactes). **Décisions déjà actées** : - Refresh **non bloquant** avec bannière — jamais de gate forcé sur une base déjà remplie. - Politique de déclenchement initiale : **check au démarrage de l'app** (comparaison `generatedAt` local vs manifest) + **bouton manuel** dans les réglages/à propos. La périodicité stricte (ex. « au plus tard tous les 30 jours ») est un paramètre ajustable, pas une mécanique dédiée. - Ce ticket est le 3e de la dépendance : **ne pas démarrer avant que le ticket bootstrap soit mergé**. ## Comportement attendu **Au lancement (app déjà initialisée) :** 1. L'app démarre normalement sur la map, immédiatement — aucun blocage. 2. En tâche de fond (après le rendu initial), GET anonyme de `latest/manifest.json`. 3. Si `manifest.generatedAt` > `generatedAt` local (comparaison ISO-8601) **et** version ≠ → afficher une bannière discrète : « Des données plus récentes sont disponibles » + bouton **Mettre à jour**. 4. Appui sur Mettre à jour → même pipeline que le bootstrap (download → sha256 → import transactionnel), avec indicateur de progression non bloquant (bannière qui montre la progression, app restant utilisable — ou état « en cours » si l'équipe préfère bloquer doucement pendant l'import SQLite, à trancher à l'implémentation). 5. Refresh réussi → bannière de confirmation brève, `generatedAt` local mis à jour, les nouvelles données sont utilisées (recharger les caches mémoire `sequences`/`geometries` du `TransitContainer`). 6. Refresh raté (offline, 5xx, sha256 mismatch) → bannière d'erreur non intrusive avec Réessayer ; **les données actuelles restent intactes et actives**. **Bouton manuel** : dans l'écran réglages/à propos (à créer minimal si inexistant — un simple item suffit), affiche la fraîcheur actuelle (« Données du 23 août 2026 ») et force le check + refresh. **Détails :** - Le check de démarrage est throttlé : pas plus d'une fois toutes les **24 h** (timestamp du dernier check en DataStore/SharedPreferences), pour ne pas ticker le registry à chaque ouverture d'app. - Si offline au check → silencieux (pas de bannière d'erreur au démarrage ; le check retentera au prochain lancement). - Un `version` de manifest identique au local → ne rien afficher. ## Pistes d'implémentation - **Domain** : use case `domain/RefreshStaticData.kt` — réutilise le client du registry et `importBundledData()` du ticket bootstrap ; la décision `needsRefresh(localGeneratedAt, manifest)` est une fonction pure (comparaison de chaînes ISO normalisées, testable). - **État** : `sealed interface RefreshState { Idle, Checking, Available(version), Downloading(bytesRead, bytesTotal), Importing, Done, Failed(error) }` exposé par un ViewModel (étendre `TransitViewModel` ou VM dédié — au choix de l'implémentation, mais événements via `TransitEventBus` si cross-VM). - Après import réussi : recharger les données en mémoire (le `TransitContainer` expose `sequences`/`geometries` `@Volatile` — les réassigner depuis la base, et notifier les VMs via l'event bus pour reconstruire couches map/`StopIndex`/`ClusterIndex`). - Bannière : composant discret en haut (sous la top bar), material 3, ne pas obstruer la map ; textes FR dans `strings.xml`. - Throttle du check : `SharedPreferences` ("last_data_check_epoch_ms") — simple et suffisant, pas besoin de DataStore pour deux clés. - Le download de refresh écrit dans le même fichier temp/cache que le bootstrap (réutiliser le client, factoriser le pipeline commun bootstrap/refresh en un objet domaine partagé — attention à ne pas dupliquer le code de download/sha256). ## Critères d'acceptation - [ ] Avec une base plus ancienne que le manifest publié : au lancement, la map s'affiche immédiatement et la bannière « données plus récentes » apparaît sans interaction. - [ ] Appui sur « Mettre à jour » : progression visible, app utilisable pendant le téléchargement, données rechargées à la fin (nouvelles lignes/arrêts visibles sur la map sans redémarrage manuel). - [ ] Kill de l'app pendant le refresh : au relancement, les données anciennes sont intactes et actives (vérifier sentinel/rollback), la bannière se ré-affiche. - [ ] Refresh avec sha256 corrompu (proxy de test) : bannière d'erreur, données locales intactes. - [ ] Offline au lancement : aucune bannière d'erreur, app normale, check retenté au lancement suivant (throttle 24 h respecté — vérifier avec un timestamp artificiellement vieux). - [ ] Le check de démarrage ne s'exécute pas plus d'une fois / 24 h (logs ou SharedPreferences vérifiables). - [ ] Manifest identique au local → aucune bannière. - [ ] L'écran réglages/à propos affiche la date de fraîcheur locale et permet de forcer le refresh. - [ ] Après un refresh réussi, `generatedAt` en base vaut celui du manifest téléchargé. - [ ] Aucune duplication du pipeline download/sha256/import entre bootstrap et refresh (objet domaine partagé). - [ ] Textes FR externalisés, mêmes standards de code que le ticket bootstrap (pas de `!!`, constantes extraites, <300 lignes/fichier). ## Notes / captures (optionnel) - Ordre des tickets : CI (données publiées) → bootstrap (premier lancement téléchargé) → ce ticket. - La comparaison de fraîcheur se fait sur `generatedAt` ISO-8601 (tri lexicographique valide si format uniforme `yyyy-MM-dd'T'HH:mm:ss'Z'` — normaliser à la génération). - Le cas « l'API M réso a cassé les IDs et la CI a publié des données dégénérées malgré les garde-fous » reste possible : c'est la raison d'être des versions datées immuables — un rollback manuel = re-PUT du pointeur `latest` vers une version antérieure saine (pas de mécanisme app à prévoir ici).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
CyrilLeblanc/gresit#14
No description provided.