DOCS/test-plan.md
Thomas Fransolet 2944fb5a05 QR / nom d'app / proximité : STATUS, plan de test §23, kanban régénéré
Le §23 couvre les réglages du manager, les trois formats de QR (id
brut du Fort, URL MDLF, nouvelle URL /download), la page de
téléchargement et les notifications de proximité. §9 mis à jour :
au premier plan c'est la popup, la notification n'arrive qu'écran
verrouillé, et meterZoneGPS sert aux zones GPS, pas aux beacons.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-10 12:07:04 +02:00

66 KiB
Raw Blame History

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

  1. Porte d'entrée — « est-ce que ça build ? » ⚠️ à passer en premier
  2. Migration DB & prérequis
  3. SectionAgenda
  4. SectionWeather
  5. SectionEvent
  6. Game — Escape & Puzzle
  7. SectionMap / Parcours guidés
  8. Notifications push
  9. Statistiques
  10. Beacons / géofencing
  11. AI Assistant
  12. Quotas & Plans d'abonnement
  13. API Keys — date d'expiration
  14. Manager-app — GuidedStep champs avancés
  15. Manager-app — SectionEvent annotations par bloc
  16. Backend — Bug GuidedPath SectionGameId
  17. Non-régression
  18. SectionVideo — multi-sources
  19. Onboarding self-service (plan Essentiel)
  20. Parité manager-app → apps visiteur (par type de section)
  21. Écarts de parité connus — à rejouer après correction
  22. Visite hors ligne — diagnostic terrain ⚠️ bugs suspectés, à jouer tôt
  23. QR codes, nom d'application, page de téléchargement, proximité

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 § É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 App au premier plan : popup de suggestion, sans notification. App en arrière-plan / écran verrouillé : notification locale beaconFound (cf. §23)
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 ⚠️ proximity_suggestion_service.dart = beaconMaxDistanceMeters = 100 (accuracy du ranging). Section.meterZoneGPS sert désormais aux zones GPS, pas aux beacons. 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.

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 isGoodisCorrect 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. 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. 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.1 bis — Entrée dans le téléchargement depuis la home v3

Ajouté le 2026-09-08 avec le portage du téléchargement dans le bento (home_3.0.dart). Avant ce portage, une visite hors ligne non téléchargée ouvrait un détail vide, sans message.

# Action Attendu OK
21.1.7 Ouvrir la home v3 avec une configuration isOffline jamais téléchargée Tuile ternie, pastille en haut à droite
21.1.8 Taper la tuile (pas la pastille) Le dialogue de téléchargement s'ouvre — pas un détail vide
21.1.9 Télécharger, fermer, revenir à la home La pastille passe au vert, la tuile n'est plus ternie
21.1.10 Taper la tuile téléchargée Le détail s'ouvre avec ses sections
21.1.11 Relancer l'app puis regarder les tuiles ⚠️ Les visites non téléchargées restent en . Le piège : le fetch écrit une ligne par visite dans la table configurations pour cacher order/gridSpan — si l'état se remettait à se déduire de ces lignes, tout passerait en au 2ᵉ lancement
21.1.12 Choisir une langue dans le dialogue, puis télécharger Seules les ressources de cette langue sont récupérées (sauf mode admin toutes langues)
21.1.13 Taper la pastille d'une visite déjà téléchargée Dialogue de mise à jour (downloadPromptUpdate), pas de premier téléchargement
21.1.14 Ouvrir une visite dont les langues ne contiennent pas la langue courante Snackbar « langue non supportée », pas de navigation
21.1.15 Désactiver une configuration dans manager-app (interrupteur du lien de canal), relancer la home La tuile disparaît

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.
À 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

# 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()));'
# 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.

# 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));'
# 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.

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.

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

23. QR codes, nom d'application, page de téléchargement, proximité

Prérequis : migration AddAppNameQrAndStoresToApplicationInstance appliquée ; visitapp buildée après flutter clean (empreinte de libapp.so vérifiée).

23.1 — Réglages dans manager-app (écrans Mobile et Web)

# Action Résultat attendu
23.1.1 Saisir le nom de l'application en FR/EN/NL, recharger Valeurs conservées, par app (Mobile ≠ Web)
23.1.2 Décocher Afficher le scan de QR code, recharger Case restée décochée
23.1.3 En SuperAdmin, saisir les liens App Store / Play Store (écran Mobile) Enregistrés
23.1.4 En InstanceAdmin Les champs de liens stores n'apparaissent pas ; un PUT forgé avec d'autres liens ne les modifie pas
23.1.5 QR d'une section (instance avec app mobile) Encode https://app.myinfomate.be/download/{instanceId}/{configId}/{sectionId}
23.1.6 QR d'une section (instance web seule) Encode https://app.myinfomate.be/{slug}/{configId}/sections/{sectionId}

23.2 — Scan dans l'app mobile

# QR scanné Depuis Résultat attendu
23.2.1 Id brut d'une section (QR du Fort) Accueil La visite s'ouvre directement sur la section
23.2.2 Id brut d'une section de la visite ouverte Visite La section s'ouvre
23.2.3 Id brut d'une section d'une autre visite Visite Popup « autre visite » puis ouverture sur la section
23.2.4 URL web.mymuseum.be/… d'un vrai QR MDLF imprimé Accueil et visite Même comportement qu'avant
23.2.5 Nouvelle URL app.myinfomate.be/download/… Accueil et visite Ouvre la section
23.2.6 QR d'une autre instance Accueil « QR code invalide »
23.2.7 Visite hors ligne, mode avion, id brut Accueil Résolu depuis la base locale
23.2.8 Scan QR désactivé dans le manager Bouton absent sur l'accueil, dans la visite et dans un menu
23.2.9 Nom de l'app renseigné En-tête de l'accueil = nom de l'app (langue active) ; sans nom : titre de la 1re visite comme avant

23.3 — Web (visitapp-web)

# Action Résultat attendu
23.3.1 Ouvrir /download/{instanceId}/{c}/{s} (téléphone, appareil photo) Image principale, nom de l'app, boutons stores renseignés, textes dans la langue du navigateur
23.3.2 Même URL sans liens stores « Bientôt disponible sur les stores »
23.3.3 Instance sans app mobile 404
23.3.4 Scanner un QR download depuis l'app web Ouvre la section web
23.3.5 Scan QR désactivé sur l'app Web Bouton absent (accueil et visite)
23.3.6 Nom de l'app renseigné Titre sur l'accueil et dans l'onglet
23.3.7 Visite avec sections géolocalisées, activer le bouton de proximité, forcer la position dans les DevTools Carte « À proximité » une fois par section et par session, 20 s minimum entre deux
23.3.8 ⚠️ Prendre l'instanceId d'un vrai QR MDLF imprimé et ouvrir /download/{cet id}/… La page de MDLF s'affiche (son nom, son image, ses liens). Si elle ne trouve rien, l'id a changé à la migration Mongo → Postgres : la redirection ne servirait à rien, à traiter avant de la poser
23.3.9 Instance sans nom d'application renseigné Le titre retombe sur le nom de l'instance, jamais sur « Application mobile »
23.3.10 Ancien QR d'une instance web seule redirigé vers /download/… Redirection vers la visite web à la bonne section

23.4 — Proximité et notifications (mobile, device réel)

# Action Résultat attendu
23.4.1 Visite avec zones GPS (sans beacon) Le bouton de proximité apparaît une fois les sections chargées
23.4.2 Activer, entrer dans une zone (mock location Android), app ouverte Popup de suggestion, pas de notification
23.4.3 Même chose écran verrouillé Notification « Contenu à proximité » + titre de la section
23.4.4 Toucher la notification L'app s'ouvre sur la section
23.4.5 Rester dans la zone écran verrouillé Une seule notification par section et par visite
23.4.6 Android : pendant le scan Notification persistante « Suggestions de contenu à proximité activées » ; disparaît en coupant le bouton ou en quittant la visite
23.4.7 iOS : écran verrouillé Indicateur de localisation en arrière-plan ; notification reçue (beacon et GPS)
23.4.8 App tuée Aucune notification (hors périmètre, par choix)
23.4.9 Mode vocal lunettes sans permission de localisation (Android 14+) Pas de crash ; le service de premier plan ne démarre pas

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