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
- Porte d'entrée — « est-ce que ça build ? » ⚠️ à passer en premier
- Migration DB & prérequis
- SectionAgenda
- SectionWeather
- SectionEvent
- Game — Escape & Puzzle
- SectionMap / Parcours guidés
- Notifications push
- Statistiques
- Beacons / géofencing
- AI Assistant
- Quotas & Plans d'abonnement
- API Keys — date d'expiration
- Manager-app — GuidedStep champs avancés
- Manager-app — SectionEvent annotations par bloc
- Backend — Bug GuidedPath SectionGameId
- Non-régression
- SectionVideo — multi-sources
- Onboarding self-service (plan Essentiel)
- Parité manager-app → apps visiteur (par type de section)
- Écarts de parité connus — à rejouer après correction
- 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 |
|
| 9.6 |
iOS — balise émettant un proximityUUID autre que FDA50693-A4E2-4FB1-AFCF-C6EB07647825 |
Le cas doit échouer : la région iOS est filtrée sur cet UUID en dur (geo_beacon_trigger_service.dart:148-155). Si ça déclenche quand même, le code ne fait pas ce qu'il dit — Android, lui, scanne sans filtre |
|
| 9.7 |
Déclenchement proactif de l'assistant par balise (chemin distinct de 9.1) |
Le prompt part depuis geo_beacon_trigger_service._onBeaconResult, uniquement si le mode proactif est actif |
|
| 9.8 |
Distance réelle de déclenchement, mètre en main |
⚠️ configuration_page.dart:43 = meterToBeacon = 100 alors que le commentaire dit 15 m, et Section.meterZoneGPS est ignoré. Noter la distance mesurée, pas celle attendue |
|
| 9.9 |
Stationner 2 min devant un POI |
Une seule notification (cooldown), pas de répétition |
|
| 9.10 |
Balise avec major porteur d'un niveau de batterie |
Aucun effet sur l'identification du POI — l'app ne lit que le minorId. C'est ce qui rend le major utilisable pour la télémétrie |
|
10. AI Assistant
| # |
Action |
Résultat attendu |
✓ |
| 10.1 |
Ouvrir l'assistant depuis home_3.0.dart |
AssistantChatSheet s'ouvre |
|
| 10.2 |
Ouvrir depuis configuration_page.dart |
Idem |
|
| 10.3 |
Envoyer un message |
Réponse de l'assistant affichée en bulle |
|
| 10.4 |
Réponse avec cards |
Cards affichées sous la réponse texte |
|
| 10.5 |
Tap sur une card de navigation |
Navigue vers la bonne section |
|
| 10.6 |
Instance avec isAssistant = false |
Bouton assistant absent |
|
11. Quotas & Plans d'abonnement
11.1 CRUD SubscriptionPlan (SuperAdmin)
| # |
Action |
Résultat attendu |
✓ |
| 11.1.1 |
POST /api/SubscriptionPlan → { "name": "Standard", "storageQuotaBytes": 5368709120, "aiTokensPerMonth": 5000000 } |
200 + id généré |
|
| 11.1.2 |
POST /api/SubscriptionPlan → { "name": "Premium", "storageQuotaBytes": 21474836480, "aiTokensPerMonth": 20000000 } |
200 |
|
| 11.1.3 |
GET /api/SubscriptionPlan |
200 + liste des plans |
|
| 11.1.4 |
PUT /api/SubscriptionPlan — modifier aiTokensPerMonth |
200 + valeur mise à jour |
|
| 11.1.5 |
DELETE d'un plan non assigné |
202 |
|
| 11.1.6 |
DELETE d'un plan assigné |
409 Conflict |
|
| 11.1.7 |
Appel avec token non-SuperAdmin |
401 / 403 |
|
5368709120 = 5 GB · 21474836480 = 20 GB
11.2 Assignation d'un plan
| # |
Action |
Résultat attendu |
✓ |
| 11.2.1 |
PUT /api/Instance avec subscriptionPlanId: "{id}" |
Plan assigné |
|
| 11.2.2 |
GET /api/Instance/{id} |
Champ subscriptionPlan présent dans la réponse |
|
| 11.2.3 |
PUT /api/Instance avec subscriptionPlanId: "" |
Plan effacé (null en base) |
|
| 11.2.4 |
PUT /api/Instance sans le champ subscriptionPlanId |
Plan existant inchangé |
|
11.3 Endpoint quota
| # |
Action |
Résultat attendu |
✓ |
| 11.3.1 |
GET /api/Instance/{id}/quota — instance sans plan |
storageQuotaBytes: 0, aiTokensPerMonth: 0 |
|
| 11.3.2 |
GET /api/Instance/{id}/quota — instance avec plan Standard |
storageQuotaBytes: 5368709120, aiTokensPerMonth: 5000000 |
|
| 11.3.3 |
Appel avec token Viewer |
200 (l'endpoint accepte Viewer) |
|
11.4 Tracking stockage
| # |
Action |
Résultat attendu |
✓ |
| 11.4.1 |
Uploader une image |
Resource.SizeBytes = taille réelle du fichier |
|
| 11.4.2 |
Uploader un PDF |
Idem |
|
| 11.4.3 |
GET /quota après uploads |
storageUsedBytes = somme des SizeBytes |
|
| 11.4.4 |
Supprimer une ressource + rappeler /quota |
storageUsedBytes diminue |
|
| 11.4.5 |
Ajouter une ImageUrl (externe) |
SizeBytes = 0 — pas comptée |
|
11.5 Tracking quota IA (en tokens)
| # |
Action |
Résultat attendu |
✓ |
| 11.5.1 |
Envoyer un message IA (/api/AI/chat) |
AiTokensThisMonth en base = Usage.TotalTokenCount retourné par Gemini pour cet appel (visible dans AiChatResponse.TokensUsed) |
|
| 11.5.2 |
Envoyer un 2e message |
Compteur = somme des TokensUsed des deux appels (pas juste +1) |
|
| 11.5.3 |
GET /quota |
aiTokensUsed = valeur cumulée en base |
|
| 11.5.4 |
Modifier AiUsageMonthKey → "2025-01" en base, envoyer un message |
Compteur repart au TokensUsed du nouvel appel (pas cumulé avec l'ancien mois), AiUsageMonthKey = mois courant |
|
| 11.5.5 |
Instance avec AiTokensThisMonth déjà ≥ AiTokensPerMonth (quota atteint) |
Nouvel appel → 429 "Quota IA mensuel dépassé", AssistantService pas appelé (pas d'incrément supplémentaire) |
|
| 11.5.6 |
Appel /api/AI/translate sur un texte HTML long (plusieurs paragraphes, plusieurs langues cibles) vs un texte court |
TokensUsed nettement plus élevé pour le texte long — vérifie que le comptage reflète bien le coût réel et pas juste "1 appel = 1 unité" |
|
| # |
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 |
|
| # |
À configurer |
Attendu mobile |
Attendu web |
✓ |
| 19.4.1 |
4 sous-sections de types différents |
grille de 4 entrées, tap → bonne section |
idem |
|
| 19.4.2 |
Sous-section avec isActive = false |
absente |
absente |
|
| 19.4.3 |
isSectionImageBackground = true (sur l'AppConfigurationLink) |
image en fond des cards du menu |
⚠️ écart connu W4 : flag ignoré en web |
|
19.5 SectionPDF
| # |
À configurer |
Attendu mobile |
Attendu web |
✓ |
| 19.5.1 |
2 PDF, un par langue |
le PDF de la langue active s'ouvre |
idem (PdfViewer) |
|
| 19.5.2 |
PDF volumineux (> 10 Mo) |
pas de freeze, indicateur de chargement |
idem |
|
19.6 SectionQuiz
| # |
À configurer |
Attendu mobile |
Attendu web |
✓ |
| 19.6.1 |
4 questions QCM avec bonne/mauvaise réponse |
enchaînement, score correct |
idem |
|
| 19.6.2 |
Question avec image sur la question et sur les réponses |
images affichées |
images affichées |
|
| 19.6.3 |
Messages bad/medium/good/great_level remplis |
message de fin correspondant au score (< 25 % → bad, ≥ 75 % → great) |
idem (QuizSection.tsx:74-77) |
|
| 19.6.4 |
Vérifier le mapping isGood → isCorrect côté web |
n/a |
la bonne réponse est bien marquée correcte (piège connu du client web) |
|
| 19.6.5 |
— |
rappel : une SectionQuiz autonome ne supporte que le QCM. Texte libre / digicode / puzzle n'existent que dans un GuidedStep de Parcours (chantier « unifier SectionQuiz sur QuizQuestion », non fait) |
idem |
|
19.7 SectionGame
| # |
À configurer |
Attendu mobile |
Attendu web |
✓ |
| 19.7.1 |
gameType = Puzzle, image + rows/cols = 3×3 |
puzzle jigsaw jouable, victoire détectée |
PuzzleGame |
|
| 19.7.2 |
gameType = SlidingPuzzle |
taquin, mélange solvable |
SlidingPuzzle |
|
| 19.7.3 |
messageDebut / messageFin |
affichés au début / à la victoire |
idem (MessageDialog) |
|
| 19.7.4 |
Aucune image de puzzle |
message d'erreur propre, pas d'écran vide |
idem |
|
| 19.7.5 |
— |
⚠️ GameTypes.Escape n'existe plus : un escape game se configure via SectionParcours + IsGameMode. Voir §19.13 |
idem |
|
19.8 SectionWeather
| # |
À configurer |
Attendu mobile |
Attendu web |
✓ |
| 19.8.1 |
city = "Namur" |
météo du jour + prévisions |
idem |
|
| 19.8.2 |
city vide |
pas de crash, section vide ou masquée |
idem |
|
| 19.8.3 |
Après passage du job weather-sync-* |
données rafraîchies dans les deux apps |
idem |
|
19.9 SectionAgenda
| # |
À configurer |
Attendu mobile |
Attendu web |
✓ |
| 19.9.1 |
3 events manuels (dont un aujourd'hui, un passé) |
l'event du jour apparaît, le passé non |
idem |
|
| 19.9.2 |
Event avec adresse + géométrie |
mini-carte / lien vers la position |
EventMiniMap |
|
| 19.9.3 |
Event avec idVideoYoutube, un autre avec videoLink, un autre avec ressource vidéo |
les 3 lecteurs fonctionnent dans le popup |
idem |
|
| 19.9.4 |
Event avec website, phone, email |
affichés et cliquables |
affichés |
|
| 19.9.5 |
isOnlineAgenda = true + ressource JSON |
events synchronisés visibles (isSynced), coexistent avec les manuels |
idem |
|
| 19.9.6 |
agendaMapProvider = Google |
carte agenda selon le provider |
⚠️ écart connu W9 : Leaflet en dur |
|
19.10 SectionMap — la section la plus à risque
| # |
À configurer |
Attendu mobile |
Attendu web |
✓ |
| 19.10.1 |
centerLatitude / centerLongitude via le picker « Centrer sur » |
carte centrée sur ce point |
✅ centré (MapSection.tsx:62) |
|
|
|
✅ 2026-08-06 — priorité au « Centrer sur », repli sur le point de section puis un défaut |
|
|
| 19.10.2 |
zoom = 15 |
niveau de zoom respecté |
idem |
|
| 19.10.3 |
mapProvider = Google puis Mapbox |
la vue change de provider |
⚠️ écart connu W1 : toujours OSM/Leaflet |
|
| 19.10.4 |
3 catégories avec couleur et icône |
marqueurs avec l'icône de leur catégorie |
✅ 2026-08-06 — icône dans la tête du pin |
|
| 19.10.5 |
Filtrer par catégorie |
seuls les points de la catégorie restent |
idem |
|
| 19.10.6 |
GeoPoint : title, description, contents, imageUrl |
popup complet |
idem |
|
| 19.10.7 |
GeoPoint : prices, phone, email, site |
tous affichés dans le popup |
idem |
|
| 19.10.8 |
GeoPoint : schedules (horaires) |
✅ 2026-08-06 |
✅ bloc « Horaires » |
|
| 19.10.9 |
GeoPoint polygone + polyColor |
polygone à la bonne couleur |
idem |
|
| 19.10.10 |
isListViewEnabled = true |
bouton bascule Liste/Carte |
idem |
|
| 19.10.11 |
Tap sur un POI |
event stat mapPoiTap émis |
idem |
|
19.11 SectionEvent
| # |
À configurer |
Attendu mobile |
Attendu web |
✓ |
| 19.11.1 |
startDate / endDate + image |
header avec dates et image |
idem |
|
| 19.11.2 |
programme : 3 blocs horaires dont un « en cours » |
timeline, bloc courant mis en évidence |
idem |
|
| 19.11.3 |
Tap sur un bloc |
détail + annotations du bloc |
idem |
|
| 19.11.4 |
baseSectionMapId + globalMapAnnotations |
onglet carte avec annotations globales |
EventMap |
|
| 19.11.5 |
Annotations sur un bloc précis |
affichées en couleur distincte par-dessus les globales |
idem |
|
| 19.11.6 |
GuidedPaths liés à l'event |
onglet Parcours, tap → progression |
idem |
|
| 19.11.7 |
sectionEventId sur la config |
bloc « à la une » en haut du home |
idem |
|
19.12 Home / configuration / theming (hors section)
| # |
À configurer |
Attendu mobile |
Attendu web |
✓ |
| 19.12.1 |
primaryColor / secondaryColor |
couleurs appliquées partout |
idem (CSS vars) |
|
| 19.12.2 |
languages : 3 langues |
sélecteur avec 3 langues, contenu traduit |
idem |
|
| 19.12.3 |
gridColSpan / gridRowSpan (bento) |
placement identique à l'aperçu manager-app |
idem (CSS Grid) — comparer les deux visuellement |
|
| 19.12.4 |
roundedValue |
arrondis appliqués |
⚠️ écart connu W3 : ignoré |
|
| 19.12.5 |
loaderImageUrl |
⚠️ splash statique, pas l'URL d'instance (connu) |
⚠️ champ présent, pas de splash |
|
| 19.12.6 |
mainImageUrl sur l'ApplicationInstance |
⚠️ écart connu M5 : mobile affiche config.imageSource |
✅ utilise mainImageUrl |
|
| 19.12.7 |
Instance en essai (isTrialActive = true) |
⚠️ écart connu M6 : aucun watermark |
✅ TrialWatermark |
|
| 19.12.8 |
isAssistant = true sur l'instance et sur l'app Web |
bouton assistant + chat fonctionnel |
✅ 2026-08-06. Vérifier aussi : un seul des deux drapeaux à false → aucune bulle ; réponse avec cards ; bouton de navigation qui route vers la bonne section ; quota 429 → message neutre sans mention technique |
|
| 19.12.9 |
Section avec isActive = false |
absente du home |
absente |
|
19.13 SectionParcours — les 5 cas d'usage
C'est le chantier le plus récent et le moins vérifié. Les 5 configurations ci-dessous couvrent
l'ensemble des combinaisons vendues.
Cas 0 — la configuration elle-même dans manager-app — ⚠️ à passer avant les cas A-E, les corrections du 2026-08-09 portent toutes sur cet écran
| # |
Manipulation |
Attendu |
✓ |
| 0.1 |
Créer un parcours, ne pas toucher à « Comment le visiteur progresse-t-il ? », sauvegarder, rouvrir |
« Dans l'ordre » toujours sélectionné, et en base isLinear = true / requireSuccessToAdvance = false. ⚠️ Avant le 2026-08-09 la popup affichait « Dans l'ordre » et enregistrait « Libre » — le parcours ne se comportait pas comme annoncé. flutter test test/progression_mode_test.dart (12/12) ne voyait pas le bug : il couvre la relecture des booléens, pas le DTO créé par la popup |
|
| 0.2 |
Bloc « Ambiance » |
Deux options radio Visite / Jeu (plus une case à cocher), même forme que les deux autres questions. « Jeu » → les champs message de début / de fin apparaissent |
|
| 0.3 |
Basculer Jeu → Visite après avoir saisi des messages |
Les messages sont conservés en base (juste masqués), rien n'est effacé à l'insu du client |
|
| 0.4 |
« Sur le terrain » dans une configuration sans aucune section Carte |
Le menu « Carte de base » reste visible mais désactivé, avec « Aucune section Carte dans cette configuration… ». ⚠️ Avant le 2026-08-09 il disparaissait sans un mot |
|
| 0.5 |
Étape → bloc « Contenu riche » |
Le champ s'appelle « Médias : » et le texte vide mentionne image / vidéo / audio — le sélecteur accepte les trois depuis toujours, seul le libellé disait « Images » |
|
| 0.6 |
Ajouter une vidéo puis un audio en média d'étape, ouvrir l'étape côté visiteur |
Mobile : lecteur vidéo / lecteur audio dans le carrousel. Web : idem (ResourceViewer). ⚠️ Avant le 2026-08-09 les deux apps forçaient le rendu en image → vignette cassée |
|
| 0.7 |
Média image, toujours côté visiteur |
Tap → plein écran (lightbox) comme avant. Sur une vidéo ou un audio, pas de lightbox : les contrôles du lecteur restent cliquables |
|
Cas A — visite guidée simple (ShowMap = true, isLinear = true, sans question)
| # |
Attendu mobile |
Attendu web |
✓ |
| A.1 |
carte + pins des étapes + position live |
carte Leaflet + pins |
|
| A.2 |
pins : ✓ complétée / ◉ courante / ○ suivante / 🔒 verrouillée |
équivalent visuel |
|
| A.3 |
« Dans l'ordre » : pas de saut possible vers une étape non atteinte (pins verrouillés) ; retour arrière autorisé |
✅ 2026-08-06, identique web |
|
| A.4 |
estimatedDurationMinutes affiché sur la card du parcours |
idem |
|
Cas B — parcours libre (ShowMap = true, isLinear = false)
| # |
Attendu mobile |
Attendu web |
✓ |
| B.1 |
« Libre » : tap sur n'importe quel pin ou segment → on y va directement |
✅ 2026-08-06, identique web |
|
| B.2 |
hideNextStepsUntilComplete = true → étapes futures masquées + badge de progression |
idem |
|
Cas C — parcours pédagogique avec questions (ShowMap indifférent, quiz par étape)
| # |
Attendu mobile |
Attendu web |
✓ |
| C.1 |
QCM sur une étape : validation, feedback |
idem |
|
| C.2 |
Question texte libre (QuestionType.Simple) : réponse insensible à la casse et aux accents |
idem |
|
| C.3 |
Réponse attendue numérique → pavé digicode 0-9 auto-détecté |
idem (DigicodePad) |
|
| C.4 |
Question puzzle |
idem (QuestionPuzzle) |
|
| C.5 |
requireSuccessToAdvance = true → « Suivant » bloqué tant que raté |
idem |
|
| C.6 |
isStepTimer + timerSeconds → compte à rebours, timerExpiredMessage à l'expiration |
idem |
|
Cas D — escape game indoor (ShowMap = false, IsGameMode = true) — ⚠️ le cas le plus fragile
| # |
Attendu |
Mobile |
Web |
✓ |
| D.0 |
Vérifier d'abord quel mode s'affiche réellement. ⚠️ écart X1 : la bascule diffère — mobile exige baseSectionMapId, web exige des étapes géolocalisées. Tester les 3 combinaisons du tableau X1 et noter le mode obtenu de chaque côté avant de conclure quoi que ce soit sur D.2-D.5 |
|
|
|
| D.1 |
gameMessageDebut à l'ouverture, gameMessageFin à la victoire |
✅ |
✅ (StartSheet / EndView) |
|
| D.2 |
Aucun champ de position/zone proposé sur les étapes |
✅ le bloc « Emplacement » est masqué dans manager-app quand le parcours est « En salle » |
idem |
|
| D.3 |
Progression « Étape par étape » → étape suivante non atteignable tant que le défi n'est pas réussi |
✅ 2026-08-06 |
✅ 2026-08-06 |
|
| D.4 |
+ case « masquer les étapes pas encore atteintes » → segments futurs invisibles |
✅ 2026-08-06 |
✅ 2026-08-06 |
|
| D.5 |
Retour arrière vers une étape déjà vue |
✅ toujours possible |
✅ toujours possible |
|
| D.6 |
Progression « Libre » → tous les segments de la barre sont cliquables |
✅ 2026-08-06 |
✅ 2026-08-06 |
|
Cas E — chasse au trésor / carnaval (ShowMap = true, IsGameMode = true, BaseSectionMapId renseigné)
| # |
Attendu mobile |
Attendu web |
✓ |
| E.1 |
carte de fond = les GeoPoints de la SectionMap liée |
idem |
|
| E.2 |
3 étapes géolocalisées : « Approchez-vous (X m) » → « Vous êtes dans la zone ✓ » |
idem |
|
| E.3 |
notification locale à l'entrée dans une zone |
(web : pas de notif locale, indicateur visuel seulement) |
|
| E.4 |
question texte libre à chaque étape, puis message de victoire |
idem |
|
| E.5 |
C'est le « use case carnaval » de todo-features.md — le jouer de bout en bout depuis la création dans manager-app vaut validation de tout le chapitre |
|
|
20. Écarts de parité connus — à rejouer après correction
Table de suivi des écarts listés dans parity-manager-visitapp.md.
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.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 |
|
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 |
❌ |