914 lines
59 KiB
Markdown
914 lines
59 KiB
Markdown
# Plan de test — MyInfoMate
|
||
|
||
> Légende : ✓ = case à cocher · ❌ Non implémenté (hors scope test) · ⚠️ Partiel
|
||
|
||
---
|
||
|
||
## 0. Porte d'entrée — « est-ce que ça build ? »
|
||
|
||
> **À passer en premier, avant tout test fonctionnel.** Tester une app qui ne compile pas fait perdre
|
||
> une demi-journée. Ces 5 commandes sont la condition minimale pour parler de déploiement.
|
||
>
|
||
> Dernière mesure : **2026-08-06** — les 4 apps du périmètre buildent ; seul `tablet-app` reste cassé.
|
||
|
||
| # | Repo | Commande | Attendu | Mesuré le 2026-08-06 | ✓ |
|
||
|---|---|---|---|---|---|
|
||
| 0.1 | manager-service | `dotnet build` | succès | ✅ succès (warnings CA1416 / MSB3277 tolérés) | |
|
||
| 0.2 | manager-service | `dotnet test ManagerService.Tests/` | tests verts | ✅ **124/124** (2026-08-06). Réparé : fakes remis à niveau (`FakeEmailService`, `FakeConfiguration`, `IHttpContextAccessor`), tests d'autorisation manquants, méthodes devenues async. **A révélé un vrai bug backend** — voir 0.2bis | |
|
||
| 0.2bis | manager-app | `flutter test test/progression_mode_test.dart` | 12/12 | ✅ mapping des 3 questions ↔ booléens du GuidedPath, dont l'aller-retour | |
|
||
| 0.3 | manager-app | `flutter build web` | `√ Built build\web` | ✅ après correction du `// @dart=2.18` manquant dans `manager_api_new/lib/api/onboarding_api.dart` (cassait le build avant) | |
|
||
| 0.4 | mymuseum-visitapp | `flutter build apk --debug` | APK produits sous `build/app/outputs/apk/<flavor>/debug/` | ✅ après la même correction + typage de `guided_step_challenge.dart:83`. ⚠️ le message « failed to produce an .apk » est **normal** : le projet a 3 flavors (`dev`, `fortsaintheribert`, `mdlf`), l'outil cherche un chemin sans flavor. Vérifier la présence des `.apk`, pas le message | |
|
||
| 0.5 | visitapp-web | `npm run build` | build Next OK | ✅ après extraction de `getGeoPointLatLng()` dans `src/lib/geo.ts` (les 7 erreurs venaient d'un `unknown` non narrowé). ⚠️ `next dev` **ne montre pas** ce type d'erreur (Turbopack ne type-checke pas) → toujours valider avec `npm run build`, jamais avec `dev` seul | |
|
||
| 0.6 | tablet-app | `flutter build apk --debug` | APK produit | ❌ échec Gradle sur `flutter_tools/gradle/build.gradle.kts:7` — pas lié au client API, à investiguer | |
|
||
|
||
**Règle** : une ligne rouge ici = pas de déploiement de l'app concernée, quel que soit l'état des
|
||
tests fonctionnels ci-dessous.
|
||
|
||
---
|
||
|
||
## Table des matières
|
||
|
||
0. [Porte d'entrée — « est-ce que ça build ? »](#0-porte-dentrée--est-ce-que-ça-build-) ⚠️ **à passer en premier**
|
||
1. [Migration DB & prérequis](#1-migration-db--prérequis)
|
||
2. [SectionAgenda](#2-sectionagenda)
|
||
3. [SectionWeather](#3-sectionweather)
|
||
4. [SectionEvent](#4-sectionevent)
|
||
5. [Game — Escape & Puzzle](#5-game--escape--puzzle)
|
||
6. [SectionMap / Parcours guidés](#6-sectionmap--parcours-guidés)
|
||
7. [Notifications push](#7-notifications-push)
|
||
8. [Statistiques](#8-statistiques)
|
||
9. [Beacons / géofencing](#9-beacons--géofencing)
|
||
10. [AI Assistant](#10-ai-assistant)
|
||
11. [Quotas & Plans d'abonnement](#11-quotas--plans-dabonnement)
|
||
12. [API Keys — date d'expiration](#12-api-keys--date-dexpiration)
|
||
13. [Manager-app — GuidedStep champs avancés](#13-manager-app--guidedstep-champs-avancés)
|
||
14. [Manager-app — SectionEvent annotations par bloc](#14-manager-app--sectionevent-annotations-par-bloc)
|
||
15. [Backend — Bug GuidedPath SectionGameId](#15-backend--bug-guidedpath-sectiongameid)
|
||
16. [Non-régression](#16-non-régression)
|
||
17. [SectionVideo — multi-sources](#17-sectionvideo--multi-sources)
|
||
18. [Onboarding self-service (plan Essentiel)](#18-onboarding-self-service-plan-essentiel)
|
||
19. [**Parité manager-app → apps visiteur (par type de section)**](#19-parité-manager-app--apps-visiteur-par-type-de-section)
|
||
20. [Écarts de parité connus — à rejouer après correction](#20-écarts-de-parité-connus--à-rejouer-après-correction)
|
||
21. [Visite hors ligne — diagnostic terrain](#21-visite-hors-ligne--diagnostic-terrain) ⚠️ **bugs suspectés, à jouer tôt**
|
||
|
||
---
|
||
|
||
## 1. Migration DB & prérequis
|
||
|
||
**Avant de commencer :** `dotnet ef database update` appliqué sur tous les contextes.
|
||
|
||
| # | Vérification | Résultat attendu | ✓ |
|
||
|---|-------------|-----------------|---|
|
||
| 1.1 | Table `SubscriptionPlans` existe | Colonnes `Id`, `Name`, `StorageQuotaBytes`, `AiTokensPerMonth` (bigint) | |
|
||
| 1.2 | Table `Instances` — nouvelles colonnes | `SubscriptionPlanId`, `AiTokensThisMonth` (bigint), `AiUsageMonthKey` présentes | |
|
||
| 1.3 | Table `Resources` — colonne `SizeBytes` | Présente, défaut `0` | |
|
||
| 1.4 | Table `EventAgendas` — colonnes vidéo | `IsSynced`, `IdVideoYoutube`, `VideoLink`, `VideoResourceId` présentes | |
|
||
| 1.5 | Hangfire dashboard accessible | `/hangfire` — jobs récurrents `agenda-sync-daily`, `weather-sync-morning`, `weather-sync-afternoon` listés | |
|
||
|
||
---
|
||
|
||
## 2. SectionAgenda
|
||
|
||
### 2.1 Synchro Hangfire des events
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 2.1.1 | Déclencher manuellement le job `agenda-sync-daily` depuis Hangfire | Events chargés depuis `agenda.php`, upsert en base avec `IsSynced = true` | |
|
||
| 2.1.2 | Modifier une `SectionAgenda` avec `isOnlineAgenda = true` dans manager-app | Job déclenché automatiquement, events mis à jour | |
|
||
| 2.1.3 | Créer un event manuel (depuis manager-app) sur une SectionAgenda avec synchro active | Event avec `IsSynced = false` coexiste avec les events synchros | |
|
||
|
||
### 2.2 Endpoint events upcoming
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 2.2.1 | `GET /api/SectionAgenda/{id}/events/upcoming` | 200 + liste filtrée `dateFrom >= aujourd'hui (début du jour)` | |
|
||
| 2.2.2 | Vérifier qu'un event dont `dateFrom` = hier n'apparaît pas | Event passé absent de la réponse | |
|
||
| 2.2.3 | Vérifier qu'un event dont `dateFrom` = aujourd'hui apparaît | Event du jour **présent** dans la réponse | |
|
||
| 2.2.4 | Visitapp et tablet-app — affichage SectionAgenda | Events chargés via l'endpoint backend (pas l'URL externe directe) | |
|
||
|
||
### 2.3 EventAgenda — infos complètes + vidéo
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 2.3.1 | Ajouter un `IdVideoYoutube` sur un event dans manager-app | Champ sauvegardé en base | |
|
||
| 2.3.2 | Tapper sur l'event dans visitapp/tablet-app | Popup affiche le player YouTube | |
|
||
| 2.3.3 | Ajouter un `VideoLink` (lien direct) | Popup affiche le lecteur vidéo via lien direct | |
|
||
| 2.3.4 | Associer une ressource vidéo interne (`VideoResourceId`) | Popup affiche la vidéo depuis la ressource | |
|
||
| 2.3.5 | Event sans vidéo | Popup normal sans bloc vidéo | |
|
||
|
||
---
|
||
|
||
## 3. SectionWeather
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 3.1 | Vérifier les jobs dans Hangfire dashboard | `weather-sync-morning` (6h00) et `weather-sync-afternoon` (13h00) présents et schedulés | |
|
||
| 3.2 | Déclencher `weather-sync-morning` manuellement | Toutes les `SectionWeather` avec une ville non-vide sont mises à jour | |
|
||
| 3.3 | SectionWeather avec ville vide | Pas de crash, section ignorée | |
|
||
|
||
---
|
||
|
||
## 4. SectionEvent
|
||
|
||
### 4.1 Page dédiée (visitapp)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 4.1.1 | Tapper sur une section de type `SectionEvent` | Page SectionEvent s'ouvre (pas de navigation vide) | |
|
||
| 4.1.2 | Header de la page | Titre de l'event, dates `startDate…endDate`, image de fond | |
|
||
| 4.1.3 | Onglet **Programme** | Timeline verticale des `ProgrammeBlock` — bloc actif mis en évidence | |
|
||
| 4.1.4 | Tapper sur un bloc programme | BottomSheet détail + annotations du bloc | |
|
||
| 4.1.5 | Onglet **Carte** | FlutterMap chargé depuis `baseSectionMapId`, couche annotations globales | |
|
||
| 4.1.6 | Bloc actif sélectionné sur carte | Annotations du bloc affichées en couleur distincte | |
|
||
| 4.1.7 | Onglet **Parcours** | Liste des `GuidedPath` liés à l'event, tap → `GuidedPathMapProgressionPage` | |
|
||
| 4.1.8 | Bouton retour | Cerclé `kMainColor`, positionné en `top: 35, left: 10` | |
|
||
| 4.1.9 | Titre/description multilingues | Affichés dans la langue active du visiteur | |
|
||
|
||
### 4.2 Mise en avant home screen
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 4.2.1 | Configuration avec `sectionEventId` renseigné | Bloc "à la une" affiché en haut de `home_3.0` à la place de la photo principale | |
|
||
| 4.2.2 | Contenu du bloc | Image de l'event, titre, bouton "Ouvrir" | |
|
||
| 4.2.3 | Tapper "Ouvrir" | Navigue vers la page SectionEvent | |
|
||
| 4.2.4 | Configuration **sans** `sectionEventId` | Home affiche la photo principale normale | |
|
||
|
||
---
|
||
|
||
## 5. Game — Escape & Puzzle
|
||
|
||
### 5.1 Escape game
|
||
|
||
> ⚠️ **Obsolète (révisé 2026-08-05)** : `GameTypes.Escape` a été retiré du backend — l'enum ne contient
|
||
> plus que `Puzzle` et `SlidingPuzzle`. Un escape game se configure désormais via **SectionParcours**
|
||
> avec `IsGameMode = true` sur le GuidedPath. Les tests correspondants sont au **§19.13, cas D et E**.
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 5.1.1 | Ouvrir une SectionGame | Aucun onglet / type « Escape » proposé nulle part dans manager-app | |
|
||
| 5.1.2 | Escape game | → tester via SectionParcours, §19.13 cas D | |
|
||
| 5.1.3 | `GameTypes.Puzzle` ou null | Comportement puzzle inchangé | |
|
||
|
||
### 5.2 Puzzle sliding
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 5.2.1 | Ouvrir un jeu de type `SlidingPuzzle` | Puzzle affiché avec `SlidingPuzzlePiece` — coins arrondis, ombres | |
|
||
| 5.2.2 | Glisser une pièce | Animation de glissement fluide | |
|
||
| 5.2.3 | Résoudre le puzzle | Victoire détectée | |
|
||
| 5.2.4 | Mélange initial | Toujours solvable (algorithme validé) | |
|
||
| 5.2.5 | Aide visuelle | Numéros sur les tuiles visibles | |
|
||
|
||
---
|
||
|
||
## 6. SectionMap / Parcours guidés
|
||
|
||
### 6.1 Affichage des GuidedPaths sur la carte
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 6.1.1 | SectionMap avec `guidedPaths` non vides | Bouton "Parcours" visible dans `map_page.dart` | |
|
||
| 6.1.2 | Tapper "Parcours" | `GuidedPathListSheet` — bottom sheet avec liste triée par `order` | |
|
||
| 6.1.3 | Tapper sur un parcours | `GuidedPathMapProgressionPage` — carte plein écran avec pins | |
|
||
| 6.1.4 | Pins des étapes | ✓ complétée · ◉ courante (animation pulsante) · ○ suivante · 🔒 verrouillée | |
|
||
| 6.1.5 | `hideNextStepsUntilComplete = true` | Pins futurs masqués, badge progression affiché | |
|
||
|
||
### 6.2 Vue liste
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 6.2.1 | SectionMap avec `isListViewEnabled = true` | Bouton toggle "Liste / Carte" en bas à droite | |
|
||
| 6.2.2 | Basculer en vue liste | `ListView` des geopoints filtrés (titre, description, miniature) | |
|
||
| 6.2.3 | `isListViewEnabled = false` | Bouton absent, carte seule | |
|
||
|
||
### 6.3 Parcours guidés — UI de progression (GuidedPathMapProgressionPage)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 6.3.1 | Position live sur la carte | Point de position mise à jour en temps réel | |
|
||
| 6.3.2 | `GuidedStepTimer` | Countdown MM:SS en mode peek, barre de progression colorée en mode étendu | |
|
||
| 6.3.3 | Quiz par étape | `QuestionsListWidget` affiché, réponses évaluées | |
|
||
| 6.3.4 | `requireSuccessToAdvance = true` | Bouton Suivant désactivé si quiz raté | |
|
||
| 6.3.5 | `isLinear = true` | Navigation séquentielle, pas de bouton Précédent | |
|
||
| 6.3.6 | Géodéclenchement | Indicateur "Approchez-vous (Xm)" → "Vous êtes dans la zone ✓" selon `zoneRadiusMeters` | |
|
||
| 6.3.7 | `GuidedPathContentProgressionPage` (escape) | Même logique, vue plein écran centrée sur le contenu sans carte | |
|
||
|
||
### 6.4 Annotations carte — SectionEvent
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 6.4.1 | `globalMapAnnotations` | Affichées en couleur principale dans la preview et `EventMapFullPage` | |
|
||
| 6.4.2 | Annotations du bloc actif | Affichées en orange par-dessus les globales | |
|
||
| 6.4.3 | `MapAnnotation.geometry.coordinates` | Accessibles (fix client généré `EventAddressDTOGeometry`) | |
|
||
|
||
---
|
||
|
||
## 7. Notifications push
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 7.1 | Envoyer une notification depuis manager-app | Notification reçue sur le device visitapp | |
|
||
| 7.2 | App en **foreground** | `MaterialBanner` affiché dans `configuration_page.dart` | |
|
||
| 7.3 | App en **background** | Notification locale affichée dans la barre système | |
|
||
| 7.4 | App **terminée** | Notification affichée, tap rouvre l'app | |
|
||
| 7.5 | Tap sur notification | Navigue vers le bon écran | |
|
||
| 7.6 | Subscribe/unsubscribe dans `home_3.0` | Préférence sauvegardée, plus de notifs si unsubscribed | |
|
||
| 7.7 | ⚠️ **Manuel** | `google-services.json` et `GoogleService-Info.plist` placés dans les bons dossiers | |
|
||
|
||
---
|
||
|
||
## 8. Statistiques
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 8.1 | Ouvrir une section puis la quitter | Event `sectionView` + `sectionLeave` (avec durée) envoyé | |
|
||
| 8.2 | Compléter un quiz | Event `quizComplete` avec `score` et `totalQuestions` | |
|
||
| 8.3 | Terminer un jeu | Event `gameComplete` avec `gameType` et durée | |
|
||
| 8.4 | Tapper un POI sur la carte | Event `mapPoiTap` avec `geoPointId` et titre | |
|
||
| 8.5 | Lire un article | Event `articleRead` | |
|
||
| 8.6 | Vérifier les stats dans manager-app | Events visibles dans l'écran statistiques | |
|
||
|
||
### 8bis. Écran « Fréquentation » refondu (jamais lancé — livré le 2026-08-07)
|
||
|
||
Voir [v2/stats-screen-plan.md](v2/stats-screen-plan.md) § État d'implémentation.
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 8.7 | Ouvrir l'écran sur une instance **à un seul canal** | Pas de filtre « Canal », pas de carte « Canaux », 4ᵉ KPI = « Contenus consultés » | |
|
||
| 8.8 | Ouvrir l'écran sur une instance **multi-canal** | Une chip par canal réellement présent, chacune avec son volume ; aucune chip pour un canal que l'instance n'a pas | |
|
||
| 8.9 | Changer de période (7 / 30 / 90 / Année) | Les périodes au-delà de `StatsHistoryDays` sont grisées ; le sous-titre de date suit la sélection | |
|
||
| 8.10 | Regarder les variations des KPI sur un plan à historique court (30 j) en période 30 j | **Aucune variation affichée** — la période précédente sortirait de l'historique et serait tronquée par le backend | |
|
||
| 8.11 | Contenu au titre long (« La vie quotidienne des soldats ») | Titre complet sur 2 lignes max, jamais tronqué à 8 caractères | |
|
||
| 8.12 | Survoler la courbe | Trait vertical + date et nombre de visites ; le pic est cerclé et nommé sous le graphe | |
|
||
| 8.13 | Période contenant des jours sans aucun event | La courbe descend à zéro ces jours-là au lieu de les sauter ; les bandes de week-end tombent bien sur samedi/dimanche | |
|
||
| 8.14 | Instance **sans** `HasAdvancedStats` | Bloc « plan Premium » à la place des tableaux détaillés, carte « Langues » absente (le backend vide la distribution) | |
|
||
| 8.15 | Basculer la langue du manager en NL puis EN | Toutes les chaînes suivent, y compris les deux phrases du bandeau « à retenir » | |
|
||
|
||
### 8ter. Export PDF à la demande (livré le 2026-08-07)
|
||
|
||
`flutter test test/statistics_report_test.dart` couvre la génération du document (4 cas). Ce qui suit ne se vérifie que dans un vrai navigateur.
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 8.16 | Cliquer sur « Télécharger le PDF » dans le bloc rapport | Le fichier se télécharge, nommé `<instance>-frequentation-<date>.pdf` | |
|
||
| 8.17 | Ouvrir le PDF | Page de garde au nom de l'instance, filet à sa couleur principale, période lisible ; les chiffres sont **identiques** à ceux de l'écran | |
|
||
| 8.18 | Vérifier les accents et les guillemets (« Fréquentation », « Connais-tu… ») | Rendus correctement — la police OpenSans est embarquée dans le document | |
|
||
| 8.19 | Instance dont l'`ApplicationInstance` mobile porte un logo | Logo en page de garde. **S'il manque**, c'est le CORS du bucket Firebase : le rapport doit quand même se générer, sans logo | |
|
||
| 8.20 | Instance **sans** `HasAdvancedStats` | Pas de tableaux POI/quiz/jeux/QR dans le PDF, et la 4ᵉ puce du bloc rapport n'est pas affichée | |
|
||
| 8.21 | Période sans aucune visite | Le bouton n'est pas atteignable (l'écran affiche l'état vide) — rien à générer | |
|
||
|
||
### 8quater. Historique unifié à 13 mois (décidé et appliqué le 2026-08-09)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 8.22 | `dotnet ef database update` puis `SELECT "Id","StatsHistoryDays" FROM "SubscriptionPlans"` | 395 sur `plan-essentiel`, `plan-standard`, `plan-premium` | |
|
||
| 8.23 | `SELECT "Name","StatsHistoryDays" FROM "Instances" WHERE "HasStats"` | 395 partout — **c'est cette valeur que lit le contrôleur**, pas celle du plan. Si elle est restée à 30, la reprise SQL de la migration n'a pas tourné | |
|
||
| 8.24 | Ouvrir l'écran sur un plan Essentiel | Les 4 périodes sont sélectionnables, **« Année » comprise** — elle était grisée avant | |
|
||
| 8.25 | Sélectionner « Année » | La courbe s'affiche, mais **aucune variation sur les KPI** : comparer 365 jours exigerait 730 jours d'historique. C'est voulu — mieux vaut rien qu'un chiffre faux | |
|
||
| 8.26 | Hangfire `/hangfire` → job `visit-events-purge` | Job listé, cron 3 h. **Le déclencher à la main** : il doit logger « purge ignorée : Stats:RetentionDays non défini » et **ne rien supprimer** | |
|
||
| 8.27 | ⛔ N'activer `Stats__RetentionDays=395` **qu'après** avoir mis en place le `pg_dump` quotidien | Suppression définitive — le snapshot OVH ne permet pas de restaurer une table seule | |
|
||
|
||
---
|
||
|
||
## 9. Beacons / géofencing
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 9.1 | Visitapp en range d'un iBeacon connu | Notification locale `beaconFound` affichée | |
|
||
| 9.2 | Notification beacon | Titre/corps dans la langue active (clé `beaconFound` / `beaconFoundBody`) | |
|
||
| 9.3 | Entrée dans une zone geo d'un `GuidedStep` | Notification locale `geoZone` affichée | |
|
||
| 9.4 | Filtre par `minorId` + `accuracy` + cooldown | Pas de spam de notifications pour le même beacon | |
|
||
| 9.5 | `beacon_scanner: ^0.0.4` | Scanner opérationnel sur Android et iOS | |
|
||
| 9.6 | **iOS** — balise émettant un `proximityUUID` **autre** que `FDA50693-A4E2-4FB1-AFCF-C6EB07647825` | Le cas doit **échouer** : la région iOS est filtrée sur cet UUID en dur (`geo_beacon_trigger_service.dart:148-155`). Si ça déclenche quand même, le code ne fait pas ce qu'il dit — Android, lui, scanne sans filtre | |
|
||
| 9.7 | Déclenchement **proactif de l'assistant** par balise (chemin distinct de 9.1) | Le prompt part depuis `geo_beacon_trigger_service._onBeaconResult`, uniquement si le mode proactif est actif | |
|
||
| 9.8 | Distance réelle de déclenchement, mètre en main | ⚠️ `configuration_page.dart:43` = `meterToBeacon = 100` alors que le commentaire dit 15 m, et `Section.meterZoneGPS` est ignoré. Noter la distance **mesurée**, pas celle attendue | |
|
||
| 9.9 | Stationner 2 min devant un POI | Une seule notification (cooldown), pas de répétition | |
|
||
| 9.10 | Balise avec `major` porteur d'un niveau de batterie | Aucun effet sur l'identification du POI — l'app ne lit que le `minorId`. C'est ce qui rend le `major` utilisable pour la télémétrie | |
|
||
|
||
---
|
||
|
||
## 10. AI Assistant
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 10.1 | Ouvrir l'assistant depuis `home_3.0.dart` | `AssistantChatSheet` s'ouvre | |
|
||
| 10.2 | Ouvrir depuis `configuration_page.dart` | Idem | |
|
||
| 10.3 | Envoyer un message | Réponse de l'assistant affichée en bulle | |
|
||
| 10.4 | Réponse avec cards | Cards affichées sous la réponse texte | |
|
||
| 10.5 | Tap sur une card de navigation | Navigue vers la bonne section | |
|
||
| 10.6 | Instance avec `isAssistant = false` | Bouton assistant absent | |
|
||
|
||
---
|
||
|
||
## 11. Quotas & Plans d'abonnement
|
||
|
||
### 11.1 CRUD SubscriptionPlan (SuperAdmin)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.1.1 | `POST /api/SubscriptionPlan` → `{ "name": "Standard", "storageQuotaBytes": 5368709120, "aiTokensPerMonth": 5000000 }` | 200 + `id` généré | |
|
||
| 11.1.2 | `POST /api/SubscriptionPlan` → `{ "name": "Premium", "storageQuotaBytes": 21474836480, "aiTokensPerMonth": 20000000 }` | 200 | |
|
||
| 11.1.3 | `GET /api/SubscriptionPlan` | 200 + liste des plans | |
|
||
| 11.1.4 | `PUT /api/SubscriptionPlan` — modifier `aiTokensPerMonth` | 200 + valeur mise à jour | |
|
||
| 11.1.5 | `DELETE` d'un plan **non assigné** | 202 | |
|
||
| 11.1.6 | `DELETE` d'un plan **assigné** | 409 Conflict | |
|
||
| 11.1.7 | Appel avec token non-SuperAdmin | 401 / 403 | |
|
||
|
||
> `5368709120` = 5 GB · `21474836480` = 20 GB
|
||
|
||
### 11.2 Assignation d'un plan
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.2.1 | `PUT /api/Instance` avec `subscriptionPlanId: "{id}"` | Plan assigné | |
|
||
| 11.2.2 | `GET /api/Instance/{id}` | Champ `subscriptionPlan` présent dans la réponse | |
|
||
| 11.2.3 | `PUT /api/Instance` avec `subscriptionPlanId: ""` | Plan effacé (null en base) | |
|
||
| 11.2.4 | `PUT /api/Instance` sans le champ `subscriptionPlanId` | Plan existant inchangé | |
|
||
|
||
### 11.3 Endpoint quota
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.3.1 | `GET /api/Instance/{id}/quota` — instance sans plan | `storageQuotaBytes: 0`, `aiTokensPerMonth: 0` | |
|
||
| 11.3.2 | `GET /api/Instance/{id}/quota` — instance avec plan Standard | `storageQuotaBytes: 5368709120`, `aiTokensPerMonth: 5000000` | |
|
||
| 11.3.3 | Appel avec token Viewer | 200 (l'endpoint accepte Viewer) | |
|
||
|
||
### 11.4 Tracking stockage
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.4.1 | Uploader une image | `Resource.SizeBytes` = taille réelle du fichier | |
|
||
| 11.4.2 | Uploader un PDF | Idem | |
|
||
| 11.4.3 | `GET /quota` après uploads | `storageUsedBytes` = somme des `SizeBytes` | |
|
||
| 11.4.4 | Supprimer une ressource + rappeler `/quota` | `storageUsedBytes` diminue | |
|
||
| 11.4.5 | Ajouter une `ImageUrl` (externe) | `SizeBytes = 0` — pas comptée | |
|
||
|
||
### 11.5 Tracking quota IA (en tokens)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.5.1 | Envoyer un message IA (`/api/AI/chat`) | `AiTokensThisMonth` en base = `Usage.TotalTokenCount` retourné par Gemini pour cet appel (visible dans `AiChatResponse.TokensUsed`) | |
|
||
| 11.5.2 | Envoyer un 2e message | Compteur = somme des `TokensUsed` des deux appels (pas juste `+1`) | |
|
||
| 11.5.3 | `GET /quota` | `aiTokensUsed` = valeur cumulée en base | |
|
||
| 11.5.4 | Modifier `AiUsageMonthKey` → `"2025-01"` en base, envoyer un message | Compteur repart au `TokensUsed` du nouvel appel (pas cumulé avec l'ancien mois), `AiUsageMonthKey` = mois courant | |
|
||
| 11.5.5 | Instance avec `AiTokensThisMonth` déjà ≥ `AiTokensPerMonth` (quota atteint) | Nouvel appel → `429 "Quota IA mensuel dépassé"`, `AssistantService` **pas appelé** (pas d'incrément supplémentaire) | |
|
||
| 11.5.6 | Appel `/api/AI/translate` sur un texte HTML long (plusieurs paragraphes, plusieurs langues cibles) vs un texte court | `TokensUsed` nettement plus élevé pour le texte long — vérifie que le comptage reflète bien le coût réel et pas juste "1 appel = 1 unité" | |
|
||
|
||
### 11.6 Widget sidebar (Flutter)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.6.1 | Se connecter à manager-app | Widget quota visible **au-dessus** de l'email dans la sidebar | |
|
||
| 11.6.2 | Instance sans plan | "Illimité" affiché | |
|
||
| 11.6.3 | Instance avec plan | "X MB / 5 GB" · tokens IA formatés en K/M, ex. "1.2M / 5M tokens" | |
|
||
| 11.6.4 | Stockage < 80% | Barre bleue | |
|
||
| 11.6.5 | Stockage ~85% (modifier `SizeBytes` en base) | Barre **orange** | |
|
||
| 11.6.6 | Stockage ~97% | Barre **rouge** | |
|
||
| 11.6.7 | `isAssistant = false` | Bloc IA absent | |
|
||
| 11.6.8 | `isAssistant = true` | Les deux barres visibles | |
|
||
|
||
### 11.7 Dialog config plan (SuperAdmin)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.7.1 | Cliquer sur le bouton swap | Dialog avec ✏️ sur chaque instance | |
|
||
| 11.7.2 | Cliquer sur ✏️ | Dialog "Plan — NomInstance" avec radio buttons | |
|
||
| 11.7.3 | Sélectionner un plan + "Enregistrer" | Plan assigné via `PUT /api/Instance` | |
|
||
| 11.7.4 | Sélectionner "Aucun plan" + "Enregistrer" | Plan effacé | |
|
||
| 11.7.5 | Connexion en non-SuperAdmin | Bouton swap invisible | |
|
||
|
||
---
|
||
|
||
## 12. API Keys — date d'expiration
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 12.1 | Créer une API key dans manager-app | Date picker d'expiration disponible | |
|
||
| 12.2 | Créer une key avec date d'expiration | Date sauvegardée, colonne "Expiration" affichée dans le tableau | |
|
||
| 12.3 | Key expirée | Colonne "Expiration" rouge dans le tableau | |
|
||
| 12.4 | Appel API avec une key expirée | 401 / rejeté | |
|
||
| 12.5 | Révoquer une key (`IsActive = false`) | Appels suivants avec cette key refusés | |
|
||
|
||
---
|
||
|
||
## 13. Manager-app — GuidedStep champs avancés
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 13.1 | Ouvrir le formulaire d'un GuidedStep | Toggles `isHiddenInitially`, `isStepLocked`, `isStepTimer` visibles | |
|
||
| 13.2 | Activer `isStepTimer` | Champs `timerSeconds` et `timerExpiredMessage` (multilingue) apparaissent | |
|
||
| 13.3 | Sauvegarder avec timer | Valeurs persistées en base | |
|
||
| 13.4 | Géodéclenchement | Zone posée via le `GeometryInputContainer` (mini-carte) + toggle `isGeoTriggered` + `zoneRadiusMeters`. ⚠️ **révisé 2026-08-05** : il n'y a **pas** de champ `triggerGeoPointId`, l'ancienne ligne de test portait sur un champ inexistant | |
|
||
| 13.5 | `isHiddenInitially` activé puis sauvegardé | Valeur persistée — mais ⚠️ **aucun effet visiteur** (écart M4, voir §20) | |
|
||
|
||
---
|
||
|
||
## 14. Manager-app — SectionEvent annotations par bloc
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 14.1 | Ouvrir le formulaire d'un `ProgrammeBlock` | Section "Annotations" présente | |
|
||
| 14.2 | Ajouter une annotation | Formulaire `showNewOrUpdateMapAnnotation` (label, géométrie, couleur, icône) | |
|
||
| 14.3 | Modifier une annotation existante | Mise à jour sauvegardée | |
|
||
| 14.4 | Supprimer une annotation | Supprimée de la liste | |
|
||
|
||
---
|
||
|
||
## 15. Backend — Bug GuidedPath SectionGameId
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 15.1 | Créer un `GuidedPath` avec un `SectionGameId` | `SectionGameId` sauvegardé | |
|
||
| 15.2 | `PUT` pour mettre à jour le `GuidedPath` | `SectionGameId` **conservé** après update (pas remis à null) | |
|
||
|
||
---
|
||
|
||
## 16. Non-régression
|
||
|
||
| # | Vérification | Résultat attendu | ✓ |
|
||
|---|-------------|-----------------|---|
|
||
| 16.1 | Navigation dans toutes les sections de manager-app | Aucune régression | |
|
||
| 16.2 | Upload de ressources | Fonctionne, `SizeBytes` renseigné | |
|
||
| 16.3 | `GET /api/Instance/{id}` existant | Retourne les anciens champs + les nouveaux | |
|
||
| 16.4 | Authentification JWT manager-app | Login/logout fonctionnels | |
|
||
| 16.5 | tablet-app — sélection par pincode | Inchangé | |
|
||
| 16.6 | mymuseum-visitapp — boot et chargement config | Inchangé | |
|
||
| 16.7 | Swagger / OpenAPI | Tous les nouveaux endpoints documentés | |
|
||
|
||
---
|
||
|
||
## 17. SectionVideo — multi-sources
|
||
|
||
### 17.1 Configuration (manager-app)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 17.1.1 | Ouvrir la config d'une SectionVideo existante avec une URL YouTube | Sélecteur positionné sur "YouTube", URL pré-remplie | |
|
||
| 17.1.2 | Ouvrir une SectionVideo avec une URL Vimeo | Sélecteur positionné sur "Vimeo" | |
|
||
| 17.1.3 | Ouvrir une SectionVideo avec une URL Firebase Storage | Sélecteur positionné sur "Ressource" | |
|
||
| 17.1.4 | Changer de mode | `source_` remis à null, champ vidé | |
|
||
| 17.1.5 | Mode Ressource → sélectionner une vidéo | Modal filtré sur `Video` et `VideoUrl` uniquement, label de la ressource affiché après sélection | |
|
||
| 17.1.6 | Sauvegarder dans chaque mode | `source_` contient l'URL correcte en base | |
|
||
|
||
### 17.2 Lecture tablet-app
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 17.2.1 | SectionVideo avec URL YouTube | Player YouTube affiché, lecture sans erreur 152 | |
|
||
| 17.2.2 | SectionVideo avec URL Vimeo | Player Vimeo affiché via WebView | |
|
||
| 17.2.3 | SectionVideo avec URL Firebase Storage (mp4) | `VideoViewer` affiché, lecture directe | |
|
||
| 17.2.4 | `source_` vide | Message "La vidéo ne peut pas être affichée, l'url est incorrecte" | |
|
||
| 17.2.5 | URL Vimeo malformée (sans ID numérique) | Message "Impossible d'extraire l'identifiant Vimeo depuis l'URL" | |
|
||
|
||
### 17.3 Lecture mymuseum-visitapp
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 17.3.1 | SectionVideo avec URL YouTube | Player YouTube affiché, lecture sans erreur 152 | |
|
||
| 17.3.2 | SectionVideo avec URL Vimeo | Player Vimeo affiché via WebView | |
|
||
| 17.3.3 | SectionVideo avec URL Firebase Storage (mp4) | `VideoViewer` affiché, lecture directe | |
|
||
| 17.3.4 | Bouton retour en mode normal | Retour fonctionnel | |
|
||
| 17.3.5 | Bouton retour en mode fullscreen YouTube | `exitFullScreen()` appelé puis retour | |
|
||
|
||
---
|
||
|
||
## 18. Onboarding self-service (plan Essentiel)
|
||
|
||
Implémenté le 2026-08-05, **jamais exécuté**. La checklist détaillée (prérequis Stripe CLI,
|
||
inscription, conversion payante, watermark, auth par token, job Hangfire, quota IA) vit dans
|
||
`todo-features.md` → section **« 🧪 Checklist de test end-to-end »** du chapitre
|
||
*Onboarding & Acquisition*, pour rester à côté des specs correspondantes.
|
||
|
||
Points de vigilance à ne pas oublier au moment de tester :
|
||
- Le `whsec_` de `stripe listen` est **différent** de celui du dashboard → à mettre dans
|
||
`appsettings.Development.json`, sinon la vérification de signature échoue en local.
|
||
- `IsAssistant` vaut `false` par défaut sur une instance neuve → à passer à `true` en base pour
|
||
pouvoir tester quoi que ce soit sur le quota IA.
|
||
- Ne pas tester la désactivation d'essai sur une instance réelle : `IsActive = false` coupe
|
||
immédiatement l'accès visiteur via la `PublicApiKey`.
|
||
|
||
---
|
||
|
||
## 19. Parité manager-app → apps visiteur (par type de section)
|
||
|
||
> **But de ce chapitre** : répondre à « tout ce que je peux configurer est-il visible côté visiteur ? ».
|
||
> Un bloc par type de section, à jouer indépendamment des autres. Analyse de référence :
|
||
> [parity-manager-visitapp.md](parity-manager-visitapp.md).
|
||
>
|
||
> **Protocole commun à tous les blocs** — 1 configuration de test, 3 fenêtres ouvertes côte à côte :
|
||
> manager-app, `mymuseum-visitapp` (device ou émulateur), `visitapp-web` (`/{slug}/{configId}`).
|
||
> Remplir **toutes** les langues activées sur au moins un champ pour vérifier le multilingue au passage.
|
||
>
|
||
> Les lignes **⚠️ écart connu** sont des échecs **attendus** aujourd'hui : elles ne sont pas des bugs à
|
||
> découvrir mais des cas à confirmer (et à rejouer après correction).
|
||
|
||
### 19.1 SectionArticle
|
||
|
||
| # | À configurer | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|---|
|
||
| 19.1.1 | `content` (HTML riche : gras, liste, lien) | rendu HTML fidèle | idem | |
|
||
| 19.1.2 | `contents` (2-3 images ordonnées) | carrousel dans l'ordre `order` | carrousel `ImageCarousel` dans l'ordre | |
|
||
| 19.1.3 | `isContentTop = true` | images **au-dessus** du texte | ⚠️ **écart connu W5** : le carrousel est toujours au-dessus, le flag est ignoré | |
|
||
| 19.1.4 | `audioIds` (1 audio par langue) | lecteur audio présent, bonne piste selon la langue | idem | |
|
||
| 19.1.5 | `isReadAudioAuto = true` | lecture démarre seule à l'ouverture | idem | |
|
||
|
||
### 19.2 SectionSlider
|
||
|
||
| # | À configurer | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|---|
|
||
| 19.2.1 | `contents` : 3 items (image + titre + description) | slider, ordre respecté | idem | |
|
||
| 19.2.2 | Un item avec une ressource vidéo | lecteur vidéo dans le slide | idem | |
|
||
|
||
### 19.3 SectionVideo / SectionWeb
|
||
|
||
> Le détail multi-sources (YouTube / Vimeo / ressource) est déjà couvert au **§17**. Ne rejouer ici que
|
||
> la parité web.
|
||
|
||
| # | À configurer | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|---|
|
||
| 19.3.1 | SectionVideo `source` = YouTube | player YouTube | player YouTube | |
|
||
| 19.3.2 | SectionVideo `source` = ressource mp4 | lecture directe | lecture directe | |
|
||
| 19.3.3 | SectionWeb `source` = URL externe | WebView | iframe | |
|
||
|
||
### 19.4 SectionMenu
|
||
|
||
| # | À configurer | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|---|
|
||
| 19.4.1 | 4 sous-sections de types différents | grille de 4 entrées, tap → bonne section | idem | |
|
||
| 19.4.2 | Sous-section avec `isActive = false` | absente | absente | |
|
||
| 19.4.3 | `isSectionImageBackground = true` (sur l'AppConfigurationLink) | image en fond des cards du menu | ⚠️ **écart connu W4** : flag ignoré en web | |
|
||
|
||
### 19.5 SectionPDF
|
||
|
||
| # | À configurer | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|---|
|
||
| 19.5.1 | 2 PDF, un par langue | le PDF de la langue active s'ouvre | idem (`PdfViewer`) | |
|
||
| 19.5.2 | PDF volumineux (> 10 Mo) | pas de freeze, indicateur de chargement | idem | |
|
||
|
||
### 19.6 SectionQuiz
|
||
|
||
| # | À configurer | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|---|
|
||
| 19.6.1 | 4 questions QCM avec bonne/mauvaise réponse | enchaînement, score correct | idem | |
|
||
| 19.6.2 | Question avec image sur la question et sur les réponses | images affichées | images affichées | |
|
||
| 19.6.3 | Messages `bad/medium/good/great_level` remplis | message de fin correspondant au score (< 25 % → bad, ≥ 75 % → great) | idem (`QuizSection.tsx:74-77`) | |
|
||
| 19.6.4 | Vérifier le mapping `isGood` → `isCorrect` côté web | n/a | la bonne réponse est bien marquée correcte (piège connu du client web) | |
|
||
| 19.6.5 | — | rappel : une SectionQuiz **autonome** ne supporte que le QCM. Texte libre / digicode / puzzle n'existent que dans un `GuidedStep` de Parcours (chantier « unifier SectionQuiz sur QuizQuestion », non fait) | idem | |
|
||
|
||
### 19.7 SectionGame
|
||
|
||
| # | À configurer | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|---|
|
||
| 19.7.1 | `gameType = Puzzle`, image + `rows`/`cols` = 3×3 | puzzle jigsaw jouable, victoire détectée | `PuzzleGame` | |
|
||
| 19.7.2 | `gameType = SlidingPuzzle` | taquin, mélange solvable | `SlidingPuzzle` | |
|
||
| 19.7.3 | `messageDebut` / `messageFin` | affichés au début / à la victoire | idem (`MessageDialog`) | |
|
||
| 19.7.4 | Aucune image de puzzle | message d'erreur propre, pas d'écran vide | idem | |
|
||
| 19.7.5 | — | ⚠️ `GameTypes.Escape` n'existe plus : un escape game se configure via **SectionParcours** + `IsGameMode`. Voir §19.13 | idem | |
|
||
|
||
### 19.8 SectionWeather
|
||
|
||
| # | À configurer | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|---|
|
||
| 19.8.1 | `city` = "Namur" | météo du jour + prévisions | idem | |
|
||
| 19.8.2 | `city` vide | pas de crash, section vide ou masquée | idem | |
|
||
| 19.8.3 | Après passage du job `weather-sync-*` | données rafraîchies dans les deux apps | idem | |
|
||
|
||
### 19.9 SectionAgenda
|
||
|
||
| # | À configurer | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|---|
|
||
| 19.9.1 | 3 events manuels (dont un aujourd'hui, un passé) | l'event du jour **apparaît**, le passé non | idem | |
|
||
| 19.9.2 | Event avec adresse + géométrie | mini-carte / lien vers la position | `EventMiniMap` | |
|
||
| 19.9.3 | Event avec `idVideoYoutube`, un autre avec `videoLink`, un autre avec ressource vidéo | les 3 lecteurs fonctionnent dans le popup | idem | |
|
||
| 19.9.4 | Event avec `website`, `phone`, `email` | affichés et cliquables | affichés | |
|
||
| 19.9.5 | `isOnlineAgenda = true` + ressource JSON | events synchronisés visibles (`isSynced`), coexistent avec les manuels | idem | |
|
||
| 19.9.6 | `agendaMapProvider` = Google | carte agenda selon le provider | ⚠️ **écart connu W9** : Leaflet en dur | |
|
||
|
||
### 19.10 SectionMap — la section la plus à risque
|
||
|
||
| # | À configurer | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|---|
|
||
| 19.10.1 | `centerLatitude` / `centerLongitude` via le picker « Centrer sur » | carte centrée sur ce point | ✅ centré (`MapSection.tsx:62`) | |
|
||
| | | ✅ 2026-08-06 — priorité au « Centrer sur », repli sur le point de section puis un défaut | | |
|
||
| 19.10.2 | `zoom` = 15 | niveau de zoom respecté | idem | |
|
||
| 19.10.3 | `mapProvider` = Google puis Mapbox | la vue change de provider | ⚠️ **écart connu W1** : toujours OSM/Leaflet | |
|
||
| 19.10.4 | 3 catégories avec couleur **et** icône | marqueurs avec l'icône de leur catégorie | ✅ 2026-08-06 — icône dans la tête du pin | |
|
||
| 19.10.5 | Filtrer par catégorie | seuls les points de la catégorie restent | idem | |
|
||
| 19.10.6 | GeoPoint : `title`, `description`, `contents`, `imageUrl` | popup complet | idem | |
|
||
| 19.10.7 | GeoPoint : `prices`, `phone`, `email`, `site` | tous affichés dans le popup | idem | |
|
||
| 19.10.8 | GeoPoint : **`schedules`** (horaires) | ✅ 2026-08-06 | ✅ bloc « Horaires » | |
|
||
| 19.10.9 | GeoPoint polygone + `polyColor` | polygone à la bonne couleur | idem | |
|
||
| 19.10.10 | `isListViewEnabled = true` | bouton bascule Liste/Carte | idem | |
|
||
| 19.10.11 | Tap sur un POI | event stat `mapPoiTap` émis | idem | |
|
||
|
||
### 19.11 SectionEvent
|
||
|
||
| # | À configurer | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|---|
|
||
| 19.11.1 | `startDate` / `endDate` + image | header avec dates et image | idem | |
|
||
| 19.11.2 | `programme` : 3 blocs horaires dont un « en cours » | timeline, bloc courant mis en évidence | idem | |
|
||
| 19.11.3 | Tap sur un bloc | détail + annotations du bloc | idem | |
|
||
| 19.11.4 | `baseSectionMapId` + `globalMapAnnotations` | onglet carte avec annotations globales | `EventMap` | |
|
||
| 19.11.5 | Annotations sur un bloc précis | affichées en couleur distincte par-dessus les globales | idem | |
|
||
| 19.11.6 | GuidedPaths liés à l'event | onglet Parcours, tap → progression | idem | |
|
||
| 19.11.7 | `sectionEventId` sur la config | bloc « à la une » en haut du home | idem | |
|
||
|
||
### 19.12 Home / configuration / theming (hors section)
|
||
|
||
| # | À configurer | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|---|
|
||
| 19.12.1 | `primaryColor` / `secondaryColor` | couleurs appliquées partout | idem (CSS vars) | |
|
||
| 19.12.2 | `languages` : 3 langues | sélecteur avec 3 langues, contenu traduit | idem | |
|
||
| 19.12.3 | `gridColSpan` / `gridRowSpan` (bento) | placement identique à l'aperçu manager-app | idem (CSS Grid) — **comparer les deux visuellement** | |
|
||
| 19.12.4 | `roundedValue` | arrondis appliqués | ⚠️ **écart connu W3** : ignoré | |
|
||
| 19.12.5 | `loaderImageUrl` | ⚠️ splash statique, pas l'URL d'instance (connu) | ⚠️ champ présent, pas de splash | |
|
||
| 19.12.6 | `mainImageUrl` sur l'ApplicationInstance | ⚠️ **écart connu M5** : mobile affiche `config.imageSource` | ✅ utilise `mainImageUrl` | |
|
||
| 19.12.7 | Instance en essai (`isTrialActive = true`) | ⚠️ **écart connu M6** : aucun watermark | ✅ `TrialWatermark` | |
|
||
| 19.12.8 | `isAssistant = true` **sur l'instance et sur l'app Web** | bouton assistant + chat fonctionnel | ✅ 2026-08-06. Vérifier aussi : un seul des deux drapeaux à `false` → aucune bulle ; réponse avec cards ; bouton de navigation qui route vers la bonne section ; quota 429 → message neutre sans mention technique | |
|
||
| 19.12.9 | Section avec `isActive = false` | absente du home | absente | |
|
||
|
||
### 19.13 SectionParcours — les 5 cas d'usage
|
||
|
||
> C'est le chantier le plus récent et le moins vérifié. Les 5 configurations ci-dessous couvrent
|
||
> l'ensemble des combinaisons vendues.
|
||
|
||
**Cas 0 — la configuration elle-même dans manager-app** — ⚠️ **à passer avant les cas A-E**, les corrections du 2026-08-09 portent toutes sur cet écran
|
||
|
||
| # | Manipulation | Attendu | ✓ |
|
||
|---|---|---|---|
|
||
| 0.1 | Créer un parcours, **ne pas toucher** à « Comment le visiteur progresse-t-il ? », sauvegarder, rouvrir | « Dans l'ordre » toujours sélectionné, et en base `isLinear = true` / `requireSuccessToAdvance = false`. ⚠️ Avant le 2026-08-09 la popup affichait « Dans l'ordre » et enregistrait « Libre » — le parcours ne se comportait pas comme annoncé. `flutter test test/progression_mode_test.dart` (12/12) ne voyait pas le bug : il couvre la relecture des booléens, pas le DTO créé par la popup | |
|
||
| 0.2 | Bloc « Ambiance » | Deux options radio **Visite / Jeu** (plus une case à cocher), même forme que les deux autres questions. « Jeu » → les champs message de début / de fin apparaissent | |
|
||
| 0.3 | Basculer Jeu → Visite après avoir saisi des messages | Les messages sont conservés en base (juste masqués), rien n'est effacé à l'insu du client | |
|
||
| 0.4 | « Sur le terrain » dans une configuration **sans aucune section Carte** | Le menu « Carte de base » reste visible mais désactivé, avec « Aucune section Carte dans cette configuration… ». ⚠️ Avant le 2026-08-09 il disparaissait sans un mot | |
|
||
| 0.5 | Étape → bloc « Contenu riche » | Le champ s'appelle **« Médias : »** et le texte vide mentionne image / vidéo / audio — le sélecteur accepte les trois depuis toujours, seul le libellé disait « Images » | |
|
||
| 0.6 | Ajouter une **vidéo** puis un **audio** en média d'étape, ouvrir l'étape côté visiteur | **Mobile** : lecteur vidéo / lecteur audio dans le carrousel. **Web** : idem (`ResourceViewer`). ⚠️ Avant le 2026-08-09 les deux apps forçaient le rendu en image → vignette cassée | |
|
||
| 0.7 | Média image, toujours côté visiteur | Tap → plein écran (lightbox) comme avant. Sur une vidéo ou un audio, **pas** de lightbox : les contrôles du lecteur restent cliquables | |
|
||
|
||
**Cas A — visite guidée simple** (`ShowMap = true`, `isLinear = true`, sans question)
|
||
|
||
| # | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|
|
||
| A.1 | carte + pins des étapes + position live | carte Leaflet + pins | |
|
||
| A.2 | pins : ✓ complétée / ◉ courante / ○ suivante / 🔒 verrouillée | équivalent visuel | |
|
||
| A.3 | « Dans l'ordre » : pas de saut possible vers une étape non atteinte (pins verrouillés) ; retour arrière autorisé | ✅ 2026-08-06, identique web | |
|
||
| A.4 | `estimatedDurationMinutes` affiché sur la card du parcours | idem | |
|
||
|
||
**Cas B — parcours libre** (`ShowMap = true`, `isLinear = false`)
|
||
|
||
| # | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|
|
||
| B.1 | « Libre » : tap sur n'importe quel pin ou segment → on y va directement | ✅ 2026-08-06, identique web | |
|
||
| B.2 | `hideNextStepsUntilComplete = true` → étapes futures masquées + badge de progression | idem | |
|
||
|
||
**Cas C — parcours pédagogique avec questions** (`ShowMap` indifférent, quiz par étape)
|
||
|
||
| # | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|
|
||
| C.1 | QCM sur une étape : validation, feedback | idem | |
|
||
| C.2 | Question **texte libre** (`QuestionType.Simple`) : réponse insensible à la casse **et aux accents** | idem | |
|
||
| C.3 | Réponse attendue **numérique** → pavé digicode 0-9 auto-détecté | idem (`DigicodePad`) | |
|
||
| C.4 | Question **puzzle** | idem (`QuestionPuzzle`) | |
|
||
| C.5 | `requireSuccessToAdvance = true` → « Suivant » bloqué tant que raté | idem | |
|
||
| C.6 | `isStepTimer` + `timerSeconds` → compte à rebours, `timerExpiredMessage` à l'expiration | idem | |
|
||
|
||
**Cas D — escape game indoor** (`ShowMap = false`, `IsGameMode = true`) — ⚠️ **le cas le plus fragile**
|
||
|
||
| # | Attendu | Mobile | Web | ✓ |
|
||
|---|---|---|---|---|
|
||
| D.0 | **Vérifier d'abord quel mode s'affiche réellement.** ⚠️ **écart X1** : la bascule diffère — mobile exige `baseSectionMapId`, web exige des étapes géolocalisées. Tester les 3 combinaisons du tableau X1 et noter le mode obtenu de chaque côté **avant** de conclure quoi que ce soit sur D.2-D.5 | | | |
|
||
| D.1 | `gameMessageDebut` à l'ouverture, `gameMessageFin` à la victoire | ✅ | ✅ (`StartSheet` / `EndView`) | |
|
||
| D.2 | Aucun champ de position/zone proposé sur les étapes | ✅ le bloc « Emplacement » est masqué dans manager-app quand le parcours est « En salle » | idem | |
|
||
| D.3 | Progression « Étape par étape » → étape suivante non atteignable tant que le défi n'est pas réussi | ✅ 2026-08-06 | ✅ 2026-08-06 | |
|
||
| D.4 | + case « masquer les étapes pas encore atteintes » → segments futurs invisibles | ✅ 2026-08-06 | ✅ 2026-08-06 | |
|
||
| D.5 | Retour arrière vers une étape déjà vue | ✅ toujours possible | ✅ toujours possible | |
|
||
| D.6 | Progression « Libre » → tous les segments de la barre sont cliquables | ✅ 2026-08-06 | ✅ 2026-08-06 | |
|
||
|
||
**Cas E — chasse au trésor / carnaval** (`ShowMap = true`, `IsGameMode = true`, `BaseSectionMapId` renseigné)
|
||
|
||
| # | Attendu mobile | Attendu web | ✓ |
|
||
|---|---|---|---|
|
||
| E.1 | carte de fond = les GeoPoints de la SectionMap liée | idem | |
|
||
| E.2 | 3 étapes géolocalisées : « Approchez-vous (X m) » → « Vous êtes dans la zone ✓ » | idem | |
|
||
| E.3 | notification locale à l'entrée dans une zone | (web : pas de notif locale, indicateur visuel seulement) | |
|
||
| E.4 | question texte libre à chaque étape, puis message de victoire | idem | |
|
||
| E.5 | **C'est le « use case carnaval » de `todo-features.md`** — le jouer de bout en bout depuis la création dans manager-app vaut validation de tout le chapitre | | |
|
||
|
||
---
|
||
|
||
## 20. Écarts de parité connus — à rejouer après correction
|
||
|
||
> Table de suivi des écarts listés dans [parity-manager-visitapp.md](parity-manager-visitapp.md).
|
||
> Cocher « corrigé » quand le code est changé, « retesté » quand le test §19 correspondant repasse au vert.
|
||
|
||
| Réf | Écart | Test §19 | Corrigé | Retesté |
|
||
|---|---|---|---|---|
|
||
| **X1** | Bascule carte/contenu différente entre les 2 apps | D.0 | ✅ 2026-08-06 — unifiée sur `ShowMap` seul | |
|
||
| **M4** | `isHiddenInitially` sans consommateur | D.5 | ✅ 2026-08-06 — champ supprimé du modèle | |
|
||
| **M7** | Mode contenu : verrouillage et masquage absents | D.2-D.4 | ✅ 2026-08-06 | |
|
||
| **M8** | `isLinear = false` (navigation libre) inexistant | A.3 | ✅ 2026-08-06 | |
|
||
| **W6** | `isLinear` / `isStepLocked` absents en web | A.3, D.3 | ✅ 2026-08-06 | |
|
||
| **W7** | Mode contenu web sans masquage | D.4 | ✅ 2026-08-06 | |
|
||
| M1 | Centrage carte mobile (`centerLatitude/Longitude`) | 19.10.1 | ✅ 2026-08-06 — helper `Helpers/mapCenter.dart` utilisé par les 3 vues | |
|
||
| M2 | Horaires d'un GeoPoint absents sur mobile | 19.10.8 | ✅ 2026-08-06 — bloc ajouté dans `marker_view.dart` | |
|
||
| M3 | `meterZoneGPS` ignoré (constante 100 m en dur) | §9 | | |
|
||
| M5 | `mainImageUrl` non utilisé sur mobile | 19.12.6 | | |
|
||
| M6 | Watermark d'essai absent sur mobile | 19.12.7 | | |
|
||
| W1 | Provider de carte ignoré en web | 19.10.3 | | |
|
||
| W2 | Icônes de catégorie ignorées en web | 19.10.4 | ✅ 2026-08-06 — `resourceDTO.url` posé dans la tête du pin | |
|
||
| W3 | `roundedValue` ignoré en web | 19.12.4 | | |
|
||
| W4 | `isSectionImageBackground` ignoré en web | 19.4.3 | | |
|
||
| W5 | `isContentTop` ignoré en web | 19.1.3 | | |
|
||
| W8 | Assistant IA absent de visitapp-web | 19.12.8 | ✅ 2026-08-06 — bulle flottante + suggestions dérivées | |
|
||
| W9 | `agendaMapProvider` ignoré en web | 19.9.6 | | |
|
||
| W10 | `iconResourceId` (Map / Event) ignoré en web | — | | |
|
||
|
||
---
|
||
|
||
## 21. Visite hors ligne — diagnostic terrain
|
||
|
||
> Ajouté le 2026-08-07. **À jouer en premier** : conditionne l'urgence du chantier [v2/offline-visit-plan.md](v2/offline-visit-plan.md).
|
||
> Analyse du code : le `switch` de collecte des ressources est commenté dans `ConfigurationController.Export` **et** le bloc symétrique dans `downloadConfiguration.dart`. Attendu : une visite téléchargée ne contient ni images d'articles ni audios. À confirmer sur un vrai device. **Corrigé en D1 le 2026-08-11** — ce §21 vérifie désormais la correction, pas le diagnostic.
|
||
>
|
||
> ✅ **Périmètre arrêté le 2026-08-13 : installation neuve, et rien d'autre.** Aucun visiteur n'a l'app installée à ce jour, et la consigne aux 4 clients est « supprimer l'app et la réinstaller ». Ça retire **deux cas** que D2 avait ouverts, et il vaut mieux savoir pourquoi avant de les remettre :
|
||
>
|
||
> - ⛔ **La montée v3 → v4 ne se teste pas** : `_onCreate` appelle `createTable(resources)`, qui contient déjà `dateUpdate TEXT` (`DatabaseHelper.dart:239`). Une installation neuve naît en v4 et n'emprunte jamais `_onUpgrade`. Vérifié plutôt que supposé — c'est exactement l'endroit où une colonne ajoutée en migration et oubliée à la création aurait fait planter toutes les installations neuves.
|
||
> - ⛔ **Le cas « device portant des `<id>.unknown` » ne se teste pas non plus** : ces fichiers datent d'avant D3, donc d'installations qui n'existent plus. Le traitement de `.unknown` par `isResourceOutdated` **reste dans le code** — il ne coûte rien et redevient utile au premier device qui garde une vieille installation. On ne teste pas un chemin de réparation pour une population de zéro ; on ne le supprime pas pour autant.
|
||
>
|
||
> ⚠️ **Ce qui fait tenir ce périmètre, et qui peut sauter** : la table rase n'est gratuite que tant qu'il n'y a pas de visiteur réel. Dès qu'un téléphone garde une installation — après la bascule, typiquement — les deux cas ci-dessus redeviennent la seule preuve que le parc existant est réparé.
|
||
|
||
### 21.1 — État réel du téléchargement
|
||
|
||
| # | Action | Attendu (après correction) | OK |
|
||
|---|--------|---------------------------|-----|
|
||
| 21.1.1 | Télécharger une visite depuis visitapp, puis passer le device en mode avion | La visite s'ouvre sans erreur | |
|
||
| 21.1.2 | Ouvrir une section Article contenant des images de contenu | Les images s'affichent | |
|
||
| 21.1.3 | Lancer un audio dans cette section Article | L'audio se lit | |
|
||
| 21.1.4 | Inspecter le dossier local de la configuration sur le device | Un fichier par ressource, **avec une vraie extension** (pas `.unknown`) | |
|
||
| 21.1.5 | Ouvrir un Quiz avec images de questions/réponses | Les images s'affichent | |
|
||
| 21.1.6 | Ouvrir une SectionMap téléchargée | Fond de carte, icônes et points présents | |
|
||
|
||
### 21.2 — Fraîcheur du contenu
|
||
|
||
| # | Action | Attendu | OK |
|
||
|---|--------|---------|-----|
|
||
| 21.2.1 | ⛔ ~~Télécharger une visite, puis **remplacer une image** dans manager-app (même ressource), puis relancer le téléchargement~~ **Cas impossible, réécrit le 2026-08-13** : `Upload` appelle `GenerateHexId()` à chaque téléversement (`ResourceController:276`), donc « remplacer une image » crée **un nouvel id et une nouvelle URL** — aucun flux ne réécrit le blob d'un id existant. Le cas d'origine testait un scénario qui ne peut pas se produire.<br>**À jouer à la place** : téléverser une **nouvelle** image, la placer dans une section déjà téléchargée, re-télécharger | La nouvelle image arrive sur le device ; l'ancienne, si plus référencée, est purgée (cf. 21.2.3) | |
|
||
| 21.2.2 | Relancer un téléchargement sans aucune modification | Rien n'est re-téléchargé, message « à jour » | |
|
||
| 21.2.3 | Supprimer une ressource dans manager-app puis re-télécharger | Le fichier local correspondant est purgé | |
|
||
|
||
### 21.3 — Types non disponibles hors ligne
|
||
|
||
| # | Action | Attendu | OK |
|
||
|---|--------|---------|-----|
|
||
| 21.3.1 | En mode avion, ouvrir une section Weather | Message explicite « nécessite une connexion », pas d'écran blanc ni de spinner infini | |
|
||
| 21.3.2 | Idem sur une section Web (webview externe) | Message explicite | |
|
||
| 21.3.3 | Idem sur une section Agenda (événements distants) | Message explicite | |
|
||
| 21.3.4 | Ouvrir une SectionVideo pointant sur YouTube en mode avion | Message explicite, distinct d'une vidéo uploadée qui, elle, doit se lire | |
|
||
|
||
### 21.4 — Robustesse
|
||
|
||
| # | Action | Attendu | OK |
|
||
|---|--------|---------|-----|
|
||
| 21.4.1 | Couper le réseau pendant un téléchargement | Le nombre d'échecs est remonté à l'utilisateur, la visite n'est pas annoncée « téléchargée » | |
|
||
| 21.4.2 | Relancer après un téléchargement partiel | Seules les ressources manquantes sont récupérées | |
|
||
|
||
---
|
||
|
||
## 22. Recette de bascule — l'app d'aujourd'hui vs la base migrée
|
||
|
||
> Ajoutée le 2026-08-11, à la demande. **À jouer juste après le `POST /api/migration/run` du jour J**, avant la bascule DNS et pendant que Mongo est encore lisible. C'est la seule fenêtre où les deux bases coexistent : après, la comparaison n'est plus possible.
|
||
>
|
||
> Le dry run compare des **comptages**. Cette recette compare le **contenu** — un compte juste ne dit pas qu'une section a gardé ses questions, ses traductions et ses médias.
|
||
>
|
||
> Rappel : `MigrationController` lit un MongoDB **vivant**, pas les fichiers de `migration-data/`.
|
||
|
||
### 22.1 — Comptages globaux, les deux bases côte à côte
|
||
|
||
```bash
|
||
# Mongo (source) — adapter l'URI au Mongo de prod ou au dump restauré
|
||
docker run --rm --network container:myim_mongo_dryrun mongo:6 \
|
||
mongosh "mongodb://localhost:27017/TabletDb" --quiet --eval '
|
||
["Instances","Users","Configurations","Resources","Sections","Devices"]
|
||
.forEach(c => print(c.padEnd(16) + db[c].countDocuments()));'
|
||
```
|
||
|
||
```bash
|
||
# Postgres (cible)
|
||
docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL'
|
||
SELECT 'Instances' AS entite, count(*) FROM "Instances"
|
||
UNION ALL SELECT 'Users', count(*) FROM "Users"
|
||
UNION ALL SELECT 'Configurations', count(*) FROM "Configurations"
|
||
UNION ALL SELECT 'Resources', count(*) FROM "Resources"
|
||
UNION ALL SELECT 'Sections', count(*) FROM "Sections"
|
||
UNION ALL SELECT 'Devices', count(*) FROM "Devices";
|
||
SQL
|
||
```
|
||
|
||
| # | Contrôle | Attendu | OK |
|
||
|---|---|---|---|
|
||
| 22.1.1 | Instances, Users, Configurations, Resources, Devices | **Identiques** des deux côtés | |
|
||
| 22.1.2 | Sections | Postgres = Mongo **moins les orphelines signalées**. Le rapport les liste dans `Skipped` : leur nombre doit expliquer *exactement* l'écart | |
|
||
| 22.1.3 | `FatalError` et code HTTP | Vide, et **200**. Un 500 veut dire que la transaction a été annulée : rien n'a été écrit, on corrige et on rejoue | |
|
||
|
||
### 22.2 — Sections par type : c'est là que les pertes se cachent
|
||
|
||
Un compte global juste peut masquer 19 articles perdus compensés ailleurs. À comparer **type par type**.
|
||
|
||
```bash
|
||
# Mongo — Type est un entier : 0=Map 1=Slider 2=Video 3=Web 4=Menu 5=Quiz
|
||
# 6=Article 7=PDF 8=Game 9=Agenda 10=Weather
|
||
docker run --rm --network container:myim_mongo_dryrun mongo:6 \
|
||
mongosh "mongodb://localhost:27017/TabletDb" --quiet --eval '
|
||
db.Sections.aggregate([{$group:{_id:"$Type",n:{$sum:1}}},{$sort:{_id:1}}])
|
||
.forEach(r => print(r._id + " : " + r.n));'
|
||
```
|
||
|
||
```bash
|
||
# Postgres — héritage TPH, le type vit dans Discriminator
|
||
docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL'
|
||
SELECT "Discriminator", count(*) FROM "Sections" GROUP BY 1 ORDER BY 1;
|
||
SQL
|
||
```
|
||
|
||
| # | Contrôle | Attendu | OK |
|
||
|---|---|---|---|
|
||
| 22.2.1 | Chaque type de Mongo se retrouve en Postgres | Écart nul, **ou** entièrement expliqué par les orphelines de ce type | |
|
||
| 22.2.2 | `Event` et `Parcours` | **0 des deux côtés** — ces types n'existaient pas dans Mongo. En voir apparaître signalerait une erreur de mapping | |
|
||
| 22.2.3 | `Game` | Autant que de type 8 dans Mongo, avec `GameType = Puzzle` | |
|
||
|
||
### 22.3 — Les collections filles, invisibles dans les comptages de sections
|
||
|
||
C'est l'écart (b) : un quiz peut arriver sans une seule question sans que rien ne le signale.
|
||
|
||
```bash
|
||
docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL'
|
||
SELECT count(*) AS questions,
|
||
count(DISTINCT "SectionQuizId") AS quiz_pourvus
|
||
FROM "QuizQuestions";
|
||
SQL
|
||
```
|
||
|
||
| # | Contrôle | Attendu | OK |
|
||
|---|---|---|---|
|
||
| 22.3.1 | Total `QuizQuestions` | **41** sur l'export d'avril — à recalculer sur le dump du jour J | |
|
||
| 22.3.2 | Ouvrir un quiz migré dans manager-app | Ses questions sont là, **avec leurs réponses et la bonne réponse cochée** | |
|
||
| 22.3.3 | Ordre des réponses | Identique à l'ancienne app | |
|
||
| 22.3.4 | `EventAgendas` et `GuidedPaths` | **Vides** — Mongo n'en contient pas. Non vides = un mapping a inventé des données | |
|
||
|
||
### 22.4 — Ce que la migration génère ou décide
|
||
|
||
Ces colonnes n'existent pas dans Mongo : rien à confronter, tout à vérifier. **C'est ici que se logent les pannes silencieuses du §1quinquies.**
|
||
|
||
```bash
|
||
docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL'
|
||
SELECT "Name", "WebSlug", left("PublicApiKey", 6) AS cle, "SubscriptionPlanId",
|
||
"AiTokensPerMonth", "IsMobile", "IsTablet", "IsWeb", "IsAssistant"
|
||
FROM "Instances" ORDER BY "Name";
|
||
SQL
|
||
```
|
||
|
||
| # | Contrôle | Attendu | OK |
|
||
|---|---|---|---|
|
||
| 22.4.1 | `PublicApiKey` | **Non nulle pour les 4 instances**, préfixe `ap_`. Nulle = les apps visiteur ne s'authentifient plus | |
|
||
| 22.4.2 | `WebSlug` | Non nul et **unique**, dérivé du nom | |
|
||
| 22.4.3 | `SubscriptionPlanId` | `plan-premium` pour MyInfoMate et VisitNamur, `plan-pro` pour MDLF et le Fort | |
|
||
| 22.4.4 | `AiTokensPerMonth` | > 0 sur les deux Premium. **0 sur les deux Pro, c'est voulu** — Pro n'inclut pas l'IA | |
|
||
| 22.4.5 | `IsMobile` / `IsTablet` | Cohérents avec les apps réellement utilisées par chaque client | |
|
||
| 22.4.6 | `IsWeb` | **Faux partout** — le canal web n'existait pas avant, il s'active à la main | |
|
||
| 22.4.7 | Rôles des utilisateurs | Tous `InstanceAdmin`. **Aucun `SuperAdmin` : à poser à la main sur ton compte**, sinon les écrans SuperAdmin sont inatteignables | |
|
||
| 22.4.8 | `Resources.StoragePath` | Renseigné pour les types fichier, **nul pour les types URL** (`ImageUrl`, `VideoUrl`, `JSONUrl`) | |
|
||
| 22.4.9 | `Resources.SizeBytes` à 0 | Uniquement celles listées dans les `Errors` du rapport (HEAD en échec). Le quota les comptera pour rien tant qu'un backfill ne passe pas | |
|
||
|
||
### 22.5 — Comparaison à l'écran, ancienne app vs nouvelle
|
||
|
||
Les comptages ne disent rien des traductions, des médias liés ni de l'ordre. **Prendre une configuration par instance** et les ouvrir côte à côte.
|
||
|
||
| # | Contrôle | Attendu | OK |
|
||
|---|---|---|---|
|
||
| 22.5.1 | La liste des sections, dans l'ordre | Même ordre, mêmes libellés | |
|
||
| 22.5.2 | Une section Article | Contenu HTML identique, **dans chaque langue de la configuration** | |
|
||
| 22.5.3 | Une SectionMap | Mêmes points, mêmes catégories, même icône, même centrage et zoom | |
|
||
| 22.5.4 | Un Menu | Les sections liées sont les mêmes (`LinkMenuSectionsAsync` relie après coup) | |
|
||
| 22.5.5 | Un PDF | Le fichier s'ouvre, et c'est le bon | |
|
||
| 22.5.6 | Une sous-section | Toujours rattachée au bon parent | |
|
||
| 22.5.7 | Couleurs et loader de la configuration | Identiques | |
|
||
| 22.5.8 | **Une app visiteur avec sa vraie clé API** | Se connecte et affiche le contenu — le seul contrôle qui teste `PublicApiKey` de bout en bout | |
|
||
|
||
### 22.6 — Le contenu qu'on sait perdu
|
||
|
||
| # | Contrôle | Attendu | OK |
|
||
|---|---|---|---|
|
||
| 22.6.1 | Relire les `Skipped` du rapport | Chaque ligne est une section orpheline **connue et acceptée**. Une surprise ici = on arrête et on regarde | |
|
||
| 22.6.2 | Les 20 sections MDLF orphelines | Soit récupérées (les 2 configurations recréées dans Mongo **avant** la bascule), soit leur perte **actée avec le client** | |
|
||
|
||
---
|
||
|
||
## Hors scope (non implémenté)
|
||
|
||
| Feature | Statut |
|
||
|---------|--------|
|
||
| Ressource 360° / panorama | ❌ — 📦 **V2** (2026-08-07) |
|
||
| AR image tracking (Mind AR) | ❌ — 📦 **V2** (2026-08-07) |
|
||
| SectionForm (formulaires personnalisés) | ❌ — 📦 **V2** (2026-08-07) |
|
||
| Audit log API Keys | ❌ |
|
||
| Rate limiting API Keys | ❌ |
|