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

155 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 :
```csharp
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 :
```dart
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 :
```csharp
public abstract class Section
{
public abstract IEnumerable<string> GetReferencedResourceIds(string language = null);
}
```
L'export devient :
```csharp
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 :
```dart
!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 | Où |
|---|---|---|
| 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.md``GetEmbeddableText()`, même pattern sur les mêmes sous-types