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

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

49 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

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

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.

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 L'image mise à jour remplace l'ancienne sur le device
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

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