DOCS/v2/offline-visit-plan.md
Thomas Fransolet a5a8ecdb20 Documentation interne MyInfoMate / Unov
Import initial de la documentation : statut, roadmap, plans V1/V2,
specs verticales (creche, sport), audits securite, plan de test,
analyse concurrentielle et maquettes de design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:17:01 +02:00

8.6 KiB
Raw Blame History

Visite hors ligne — Plan de remise en état

Contexte : constats du 2026-08-07 en analysant le coût d'egress (media-storage-plan.md). Le téléchargement hors ligne est le seul poste d'egress significatif, et c'est aussi l'argument de vente sur les lieux sans réseau (Fort Saint Héribert et ses murs épais).


État actuel : la visite hors ligne ne fonctionne quasiment pas

Ce n'est pas un manque de types supportés, c'est un pipeline désactivé des deux côtés.

Côté backend — ConfigurationController.Export

L'endpoint ne retourne que trois choses :

configuration.ImageId        // image de la config
configuration.LoaderImageId  // splash
section.imageId              // vignette de chaque section

Tout le switch (section.type) qui collectait les ressources internes est commenté (~150 lignes) : images d'articles, fichiers audio, images de quiz, points de carte, contenus de slider. Rien de ce qui fait le contenu réel d'une visite n'est exporté.

Côté visitapp — downloadConfiguration.dart

Symétriquement, le bloc qui téléchargeait les images d'articles et les audios est commenté lui aussi, et le filtre ne garde que deux types de sections :

sections.where((s) => s.type == SectionType.Article || s.type == SectionType.Quiz)
// TODO: supporter tous les types (Game, Menu, Map, PDF, Video, Slider, Web, Weather, Agenda)

Conséquence

Une « visite téléchargée » contient aujourd'hui les métadonnées des sections Article et Quiz, plus quelques vignettes. Pas les images de contenu, pas les audios. Le visiteur du Fort qui télécharge sa visite avant d'entrer n'a rien d'exploitable une fois hors réseau.

À vérifier sur un device avant toute chose : c'est peut-être déjà remonté comme « l'appli marche mal dans le fort » sans que la cause ait été identifiée.


Pourquoi ça a rouillé — et ce qu'il faut changer

La collecte des ressources était un switch géant sur le type de section, dans le contrôleur. Chaque nouveau type (SectionEvent, SectionParcours, SectionMap…) imposait d'aller modifier ce switch, loin de l'entité concernée. Personne ne l'a fait, le switch est devenu faux, puis on l'a commenté.

Le remettre en l'état reproduirait le problème dans six mois.

Correctif structurel : chaque sous-type déclare ses ressources

L'héritage TPH fait que _myInfoMateDbContext.Sections rend déjà les sous-types concrets. Une méthode abstraite suffit :

public abstract class Section
{
    public abstract IEnumerable<string> GetReferencedResourceIds(string language = null);
}

L'export devient :

var resourceIds = sections.SelectMany(s => s.GetReferencedResourceIds(language)).Distinct();

Trois bénéfices : la logique vit à côté des champs qu'elle parcourt, le compilateur oblige à l'implémenter sur tout nouveau sous-type, et le contrôleur n'a plus rien à connaître des types.

Même pattern que GetEmbeddableText() prévu dans rag-pgvector-integration-plan.md. Les deux méthodes vivent sur le sous-type de section et parcourent les mêmes structures. À écrire dans la même passe : une fois qu'on sait extraire les resourceIds d'un SectionMap, en extraire le texte est le même travail.


« Tous les types » — la nuance

Tous les types ne peuvent pas fonctionner hors ligne. Le but est de télécharger ce qui est téléchargeable et de dégrader explicitement le reste.

Type Hors ligne Note
Article oui images de contenu + audios par langue — le cœur de la visite
Quiz oui images de questions/réponses, images de niveaux de résultat
Slider oui contenus du slider
Game oui image du puzzle, messages début/fin
PDF oui fichiers PDF par langue
Menu oui pas de ressource propre, mais la navigation doit fonctionner
Parcours oui étapes + ressources associées
Map partiel image de fond, icônes, points et leurs contenus téléchargeables. Un fond de carte tuilé en ligne, non
Video partiel seulement si la vidéo est une ressource uploadée. YouTube/Vimeo nécessitent le réseau
Event oui AgendaSyncService copie déjà les événements distants dans la table locale EventAgendas — ils sont donc exportables (tranché 2026-08-07)
Agenda oui, en instantané idem — voir la réserve ci-dessous
Weather non API météo temps réel
Web non webview vers une URL externe

Réserve sur l'agenda et les événements : le job Hangfire les rapatrie en base, donc ils partent hors ligne — mais c'est un instantané figé au moment du téléchargement. Un événement annulé après coup reste affiché comme maintenu. Afficher la date de fraîcheur (« données du 12/08 ») plutôt que de laisser croire à du temps réel.

Ce que ça implique côté produit

  • Dégradation explicite : une section indisponible hors ligne affiche un message clair (« nécessite une connexion »), pas un écran blanc ni un spinner infini.
  • Prévenir à la configuration, pas sur le terrain : dans manager-app, signaler au client quelles sections de sa configuration ne fonctionneront pas hors ligne. Il le découvre au moment où il conçoit sa visite, pas quand un visiteur se plaint.

Bugs à corriger dans la même passe

1. Fraîcheur — les ressources modifiées ne se mettent jamais à jour

Le filtre incrémental teste la présence du fichier local, pas sa version :

!fileList.any((fileL) => fileL.uri.pathSegments.last.contains(resource.id!))

Or quand un client remplace une image, Update conserve l'id et ne change que l'Url. Le fichier existe donc localement, la ressource est sautée, le visiteur garde l'ancienne version indéfiniment. Les ajouts fonctionnent, les mises à jour non.

Correctif : stocker Resource.DateUpdate (déjà présent sur l'entité) dans le SQLite local à côté du chemin, et re-télécharger si la valeur distante est plus récente. Le champ doit être exposé dans ResourceDTO s'il ne l'est pas.

C'est un bug de correction, invisible côté client — qui voit son contenu à jour dans le CMS et sur le web.

2. audio/mpeg absent de la table d'extensions

_getExtensionFromContentType mappe audio/mp3, qui n'est pas un type MIME standard. Le vrai MIME d'un MP3 est audio/mpeg, absent de la table — les audios atterrissent donc probablement en .unknown. À vérifier sur un device en priorité : combiné au point précédent, ça expliquerait beaucoup.

La fonction devrait aussi journaliser explicitement tout Content-Type non reconnu plutôt que de retourner "unknown" en silence.

3. Purge des fichiers obsolètes désactivée

La liste resourceToDelete est calculée, puis le deleteSync() est commenté (// Preserve call to firebase // TODO uncomment if needed), et cleanLocalResources l'est aussi. Le stockage occupé sur le téléphone du visiteur ne diminue jamais.

4. Échecs silencieux

Un téléchargement raté affiche print("NOT SUCCESSS") et passe à la suivante. La visite est annoncée « téléchargée » alors qu'elle est incomplète. Il faut compter les échecs, les remonter à l'utilisateur, et permettre de relancer les manquants.


Ordre d'implémentation

# Tâche
1 Vérifier sur un device l'état réel du hors ligne (audios, images d'articles) test terrain
2 GetReferencedResourceIds() abstrait + implémentations par sous-type manager-service
3 Export réécrit sur cette méthode, switch commenté supprimé manager-service
4 DateUpdate exposé dans ResourceDTO manager-service + client généré
5 Filtre incrémental basé sur la version, plus sur la présence visitapp
6 Table Content-Type → extension complétée + log des types inconnus visitapp
7 Téléchargement étendu à tous les types marqués « oui » dans la matrice visitapp
8 Dégradation explicite des types non disponibles hors ligne visitapp
9 Réactivation de la purge des fichiers obsolètes visitapp
10 Comptage et remontée des échecs de téléchargement visitapp
11 Signalement à la configuration dans manager-app manager-app

Les étapes 1 à 6 sont des corrections de bugs — à traiter indépendamment du reste. Les étapes 7 à 11 sont l'extension fonctionnelle.


Liens

  • media-storage-plan.md — compression, quota, StoragePath (la compression réduit le paquet hors ligne de ~12×)
  • rag-pgvector-integration-plan.mdGetEmbeddableText(), même pattern sur les mêmes sous-types