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>
155 lines
8.6 KiB
Markdown
155 lines
8.6 KiB
Markdown
# 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
|