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
- QR codes, nom d'application, page de téléchargement, proximité
- Onglet XR — flotte de casques (lot XR-2)
- App VR — appairage et lecture du contenu (XR-4, items E2-E3) ⚠️ jamais exécuté
- Médias immersifs dans le manager — ce que voit le gestionnaire (XR-4)
- Hors ligne complet et fond immersif (XR-4, E4 et E5) ⚠️ jamais exécuté
1. Migration DB & prérequis
Avant de commencer : dotnet ef database update appliqué sur tous les contextes.
| # |
Vérification |
Résultat attendu |
✓ |
| 1.1 |
Table SubscriptionPlans existe |
Colonnes Id, Name, StorageQuotaBytes, AiTokensPerMonth (bigint) |
|
| 1.2 |
Table Instances — nouvelles colonnes |
SubscriptionPlanId, AiTokensThisMonth (bigint), AiUsageMonthKey présentes |
|
| 1.3 |
Table Resources — colonne SizeBytes |
Présente, défaut 0 |
|
| 1.4 |
Table EventAgendas — colonnes vidéo |
IsSynced, IdVideoYoutube, VideoLink, VideoResourceId présentes |
|
| 1.5 |
Hangfire dashboard accessible |
/hangfire — jobs récurrents agenda-sync-daily, weather-sync-morning, weather-sync-afternoon listés |
|
2. SectionAgenda
2.1 Synchro Hangfire des events
| # |
Action |
Résultat attendu |
✓ |
| 2.1.1 |
Déclencher manuellement le job agenda-sync-daily depuis Hangfire |
Events chargés depuis agenda.php, upsert en base avec IsSynced = true |
|
| 2.1.2 |
Modifier une SectionAgenda avec isOnlineAgenda = true dans manager-app |
Job déclenché automatiquement, events mis à jour |
|
| 2.1.3 |
Créer un event manuel (depuis manager-app) sur une SectionAgenda avec synchro active |
Event avec IsSynced = false coexiste avec les events synchros |
|
2.2 Endpoint events upcoming
| # |
Action |
Résultat attendu |
✓ |
| 2.2.1 |
GET /api/SectionAgenda/{id}/events/upcoming |
200 + liste filtrée dateFrom >= aujourd'hui (début du jour) |
|
| 2.2.2 |
Vérifier qu'un event dont dateFrom = hier n'apparaît pas |
Event passé absent de la réponse |
|
| 2.2.3 |
Vérifier qu'un event dont dateFrom = aujourd'hui apparaît |
Event du jour présent dans la réponse |
|
| 2.2.4 |
Visitapp et tablet-app — affichage SectionAgenda |
Events chargés via l'endpoint backend (pas l'URL externe directe) |
|
2.3 EventAgenda — infos complètes + vidéo
| # |
Action |
Résultat attendu |
✓ |
| 2.3.1 |
Ajouter un IdVideoYoutube sur un event dans manager-app |
Champ sauvegardé en base |
|
| 2.3.2 |
Tapper sur l'event dans visitapp/tablet-app |
Popup affiche le player YouTube |
|
| 2.3.3 |
Ajouter un VideoLink (lien direct) |
Popup affiche le lecteur vidéo via lien direct |
|
| 2.3.4 |
Associer une ressource vidéo interne (VideoResourceId) |
Popup affiche la vidéo depuis la ressource |
|
| 2.3.5 |
Event sans vidéo |
Popup normal sans bloc vidéo |
|
3. SectionWeather
| # |
Action |
Résultat attendu |
✓ |
| 3.1 |
Vérifier les jobs dans Hangfire dashboard |
weather-sync-morning (6h00) et weather-sync-afternoon (13h00) présents et schedulés |
|
| 3.2 |
Déclencher weather-sync-morning manuellement |
Toutes les SectionWeather avec une ville non-vide sont mises à jour |
|
| 3.3 |
SectionWeather avec ville vide |
Pas de crash, section ignorée |
|
4. SectionEvent
4.1 Page dédiée (visitapp)
| # |
Action |
Résultat attendu |
✓ |
| 4.1.1 |
Tapper sur une section de type SectionEvent |
Page SectionEvent s'ouvre (pas de navigation vide) |
|
| 4.1.2 |
Header de la page |
Titre de l'event, dates startDate…endDate, image de fond |
|
| 4.1.3 |
Onglet Programme |
Timeline verticale des ProgrammeBlock — bloc actif mis en évidence |
|
| 4.1.4 |
Tapper sur un bloc programme |
BottomSheet détail + annotations du bloc |
|
| 4.1.5 |
Onglet Carte |
FlutterMap chargé depuis baseSectionMapId, couche annotations globales |
|
| 4.1.6 |
Bloc actif sélectionné sur carte |
Annotations du bloc affichées en couleur distincte |
|
| 4.1.7 |
Onglet Parcours |
Liste des GuidedPath liés à l'event, tap → GuidedPathMapProgressionPage |
|
| 4.1.8 |
Bouton retour |
Cerclé kMainColor, positionné en top: 35, left: 10 |
|
| 4.1.9 |
Titre/description multilingues |
Affichés dans la langue active du visiteur |
|
4.2 Mise en avant home screen
| # |
Action |
Résultat attendu |
✓ |
| 4.2.1 |
Configuration avec sectionEventId renseigné |
Bloc "à la une" affiché en haut de home_3.0 à la place de la photo principale |
|
| 4.2.2 |
Contenu du bloc |
Image de l'event, titre, bouton "Ouvrir" |
|
| 4.2.3 |
Tapper "Ouvrir" |
Navigue vers la page SectionEvent |
|
| 4.2.4 |
Configuration sans sectionEventId |
Home affiche la photo principale normale |
|
5. Game — Escape & Puzzle
5.1 Escape game
⚠️ Obsolète (révisé 2026-08-05) : GameTypes.Escape a été retiré du backend — l'enum ne contient
plus que Puzzle et SlidingPuzzle. Un escape game se configure désormais via SectionParcours
avec IsGameMode = true sur le GuidedPath. Les tests correspondants sont au §19.13, cas D et E.
| # |
Action |
Résultat attendu |
✓ |
| 5.1.1 |
Ouvrir une SectionGame |
Aucun onglet / type « Escape » proposé nulle part dans manager-app |
|
| 5.1.2 |
Escape game |
→ tester via SectionParcours, §19.13 cas D |
|
| 5.1.3 |
GameTypes.Puzzle ou null |
Comportement puzzle inchangé |
|
5.2 Puzzle sliding
| # |
Action |
Résultat attendu |
✓ |
| 5.2.1 |
Ouvrir un jeu de type SlidingPuzzle |
Puzzle affiché avec SlidingPuzzlePiece — coins arrondis, ombres |
|
| 5.2.2 |
Glisser une pièce |
Animation de glissement fluide |
|
| 5.2.3 |
Résoudre le puzzle |
Victoire détectée |
|
| 5.2.4 |
Mélange initial |
Toujours solvable (algorithme validé) |
|
| 5.2.5 |
Aide visuelle |
Numéros sur les tuiles visibles |
|
6. SectionMap / Parcours guidés
6.1 Affichage des GuidedPaths sur la carte
| # |
Action |
Résultat attendu |
✓ |
| 6.1.1 |
SectionMap avec guidedPaths non vides |
Bouton "Parcours" visible dans map_page.dart |
|
| 6.1.2 |
Tapper "Parcours" |
GuidedPathListSheet — bottom sheet avec liste triée par order |
|
| 6.1.3 |
Tapper sur un parcours |
GuidedPathMapProgressionPage — carte plein écran avec pins |
|
| 6.1.4 |
Pins des étapes |
✓ complétée · ◉ courante (animation pulsante) · ○ suivante · 🔒 verrouillée |
|
| 6.1.5 |
hideNextStepsUntilComplete = true |
Pins futurs masqués, badge progression affiché |
|
6.2 Vue liste
| # |
Action |
Résultat attendu |
✓ |
| 6.2.1 |
SectionMap avec isListViewEnabled = true |
Bouton toggle "Liste / Carte" en bas à droite |
|
| 6.2.2 |
Basculer en vue liste |
ListView des geopoints filtrés (titre, description, miniature) |
|
| 6.2.3 |
isListViewEnabled = false |
Bouton absent, carte seule |
|
6.3 Parcours guidés — UI de progression (GuidedPathMapProgressionPage)
| # |
Action |
Résultat attendu |
✓ |
| 6.3.1 |
Position live sur la carte |
Point de position mise à jour en temps réel |
|
| 6.3.2 |
GuidedStepTimer |
Countdown MM:SS en mode peek, barre de progression colorée en mode étendu |
|
| 6.3.3 |
Quiz par étape |
QuestionsListWidget affiché, réponses évaluées |
|
| 6.3.4 |
requireSuccessToAdvance = true |
Bouton Suivant désactivé si quiz raté |
|
| 6.3.5 |
isLinear = true |
Navigation séquentielle, pas de bouton Précédent |
|
| 6.3.6 |
Géodéclenchement |
Indicateur "Approchez-vous (Xm)" → "Vous êtes dans la zone ✓" selon zoneRadiusMeters |
|
| 6.3.7 |
GuidedPathContentProgressionPage (escape) |
Même logique, vue plein écran centrée sur le contenu sans carte |
|
6.4 Annotations carte — SectionEvent
| # |
Action |
Résultat attendu |
✓ |
| 6.4.1 |
globalMapAnnotations |
Affichées en couleur principale dans la preview et EventMapFullPage |
|
| 6.4.2 |
Annotations du bloc actif |
Affichées en orange par-dessus les globales |
|
| 6.4.3 |
MapAnnotation.geometry.coordinates |
Accessibles (fix client généré EventAddressDTOGeometry) |
|
7. Notifications push
| # |
Action |
Résultat attendu |
✓ |
| 7.1 |
Envoyer une notification depuis manager-app |
Notification reçue sur le device visitapp |
|
| 7.2 |
App en foreground |
MaterialBanner affiché dans configuration_page.dart |
|
| 7.3 |
App en background |
Notification locale affichée dans la barre système |
|
| 7.4 |
App terminée |
Notification affichée, tap rouvre l'app |
|
| 7.5 |
Tap sur notification |
Navigue vers le bon écran |
|
| 7.6 |
Subscribe/unsubscribe dans home_3.0 |
Préférence sauvegardée, plus de notifs si unsubscribed |
|
| 7.7 |
⚠️ Manuel |
google-services.json et GoogleService-Info.plist placés dans les bons dossiers |
|
8. Statistiques
| # |
Action |
Résultat attendu |
✓ |
| 8.1 |
Ouvrir une section puis la quitter |
Event sectionView + sectionLeave (avec durée) envoyé |
|
| 8.2 |
Compléter un quiz |
Event quizComplete avec score et totalQuestions |
|
| 8.3 |
Terminer un jeu |
Event gameComplete avec gameType et durée |
|
| 8.4 |
Tapper un POI sur la carte |
Event mapPoiTap avec geoPointId et titre |
|
| 8.5 |
Lire un article |
Event articleRead |
|
| 8.6 |
Vérifier les stats dans manager-app |
Events visibles dans l'écran statistiques |
|
8bis. Écran « Fréquentation » refondu (jamais lancé — livré le 2026-08-07)
Voir v2/stats-screen-plan.md § État d'implémentation.
| # |
Action |
Résultat attendu |
✓ |
| 8.7 |
Ouvrir l'écran sur une instance à un seul canal |
Pas de filtre « Canal », pas de carte « Canaux », 4ᵉ KPI = « Contenus consultés » |
|
| 8.8 |
Ouvrir l'écran sur une instance multi-canal |
Une chip par canal réellement présent, chacune avec son volume ; aucune chip pour un canal que l'instance n'a pas |
|
| 8.9 |
Changer de période (7 / 30 / 90 / Année) |
Les périodes au-delà de StatsHistoryDays sont grisées ; le sous-titre de date suit la sélection |
|
| 8.10 |
Regarder les variations des KPI sur un plan à historique court (30 j) en période 30 j |
Aucune variation affichée — la période précédente sortirait de l'historique et serait tronquée par le backend |
|
| 8.11 |
Contenu au titre long (« La vie quotidienne des soldats ») |
Titre complet sur 2 lignes max, jamais tronqué à 8 caractères |
|
| 8.12 |
Survoler la courbe |
Trait vertical + date et nombre de visites ; le pic est cerclé et nommé sous le graphe |
|
| 8.13 |
Période contenant des jours sans aucun event |
La courbe descend à zéro ces jours-là au lieu de les sauter ; les bandes de week-end tombent bien sur samedi/dimanche |
|
| 8.14 |
Instance sans HasAdvancedStats |
Bloc « plan Premium » à la place des tableaux détaillés, carte « Langues » absente (le backend vide la distribution) |
|
| 8.15 |
Basculer la langue du manager en NL puis EN |
Toutes les chaînes suivent, y compris les deux phrases du bandeau « à retenir » |
|
8ter. Export PDF à la demande (livré le 2026-08-07)
flutter test test/statistics_report_test.dart couvre la génération du document (4 cas). Ce qui suit ne se vérifie que dans un vrai navigateur.
| # |
Action |
Résultat attendu |
✓ |
| 8.16 |
Cliquer sur « Télécharger le PDF » dans le bloc rapport |
Le fichier se télécharge, nommé <instance>-frequentation-<date>.pdf |
|
| 8.17 |
Ouvrir le PDF |
Page de garde au nom de l'instance, filet à sa couleur principale, période lisible ; les chiffres sont identiques à ceux de l'écran |
|
| 8.18 |
Vérifier les accents et les guillemets (« Fréquentation », « Connais-tu… ») |
Rendus correctement — la police OpenSans est embarquée dans le document |
|
| 8.19 |
Instance dont l'ApplicationInstance mobile porte un logo |
Logo en page de garde. S'il manque, c'est le CORS du bucket Firebase : le rapport doit quand même se générer, sans logo |
|
| 8.20 |
Instance sans HasAdvancedStats |
Pas de tableaux POI/quiz/jeux/QR dans le PDF, et la 4ᵉ puce du bloc rapport n'est pas affichée |
|
| 8.21 |
Période sans aucune visite |
Le bouton n'est pas atteignable (l'écran affiche l'état vide) — rien à générer |
|
8quater. Historique unifié à 13 mois (décidé et appliqué le 2026-08-09)
| # |
Action |
Résultat attendu |
✓ |
| 8.22 |
dotnet ef database update puis SELECT "Id","StatsHistoryDays" FROM "SubscriptionPlans" |
395 sur plan-essentiel, plan-standard, plan-premium |
|
| 8.23 |
SELECT "Name","StatsHistoryDays" FROM "Instances" WHERE "HasStats" |
395 partout — c'est cette valeur que lit le contrôleur, pas celle du plan. Si elle est restée à 30, la reprise SQL de la migration n'a pas tourné |
|
| 8.24 |
Ouvrir l'écran sur un plan Essentiel |
Les 4 périodes sont sélectionnables, « Année » comprise — elle était grisée avant |
|
| 8.25 |
Sélectionner « Année » |
La courbe s'affiche, mais aucune variation sur les KPI : comparer 365 jours exigerait 730 jours d'historique. C'est voulu — mieux vaut rien qu'un chiffre faux |
|
| 8.26 |
Hangfire /hangfire → job visit-events-purge |
Job listé, cron 3 h. Le déclencher à la main : il doit logger « purge ignorée : Stats:RetentionDays non défini » et ne rien supprimer |
|
| 8.27 |
⛔ N'activer Stats__RetentionDays=395 qu'après avoir mis en place le pg_dump quotidien |
Suppression définitive — le snapshot OVH ne permet pas de restaurer une table seule |
|
9. Beacons / géofencing
| # |
Action |
Résultat attendu |
✓ |
| 9.1 |
Visitapp en range d'un iBeacon connu |
App au premier plan : popup de suggestion, sans notification. App en arrière-plan / écran verrouillé : notification locale beaconFound (cf. §23) |
|
| 9.2 |
Notification beacon |
Titre/corps dans la langue active (clé beaconFound / beaconFoundBody) |
|
| 9.3 |
Entrée dans une zone geo d'un GuidedStep |
Notification locale geoZone affichée |
|
| 9.4 |
Filtre par minorId + accuracy + cooldown |
Pas de spam de notifications pour le même beacon |
|
| 9.5 |
beacon_scanner: ^0.0.4 |
Scanner opérationnel sur Android et iOS |
|
| 9.6 |
iOS — balise émettant un proximityUUID autre que FDA50693-A4E2-4FB1-AFCF-C6EB07647825 |
Le cas doit échouer : la région iOS est filtrée sur cet UUID en dur (geo_beacon_trigger_service.dart:148-155). Si ça déclenche quand même, le code ne fait pas ce qu'il dit — Android, lui, scanne sans filtre |
|
| 9.7 |
Déclenchement proactif de l'assistant par balise (chemin distinct de 9.1) |
Le prompt part depuis geo_beacon_trigger_service._onBeaconResult, uniquement si le mode proactif est actif |
|
| 9.8 |
Distance réelle de déclenchement, mètre en main |
⚠️ proximity_suggestion_service.dart = beaconMaxDistanceMeters = 100 (accuracy du ranging). Section.meterZoneGPS sert désormais aux zones GPS, pas aux beacons. Noter la distance mesurée, pas celle attendue |
|
| 9.9 |
Stationner 2 min devant un POI |
Une seule notification (cooldown), pas de répétition |
|
| 9.10 |
Balise avec major porteur d'un niveau de batterie |
Aucun effet sur l'identification du POI — l'app ne lit que le minorId. C'est ce qui rend le major utilisable pour la télémétrie |
|
10. AI Assistant
| # |
Action |
Résultat attendu |
✓ |
| 10.1 |
Ouvrir l'assistant depuis home_3.0.dart |
AssistantChatSheet s'ouvre |
|
| 10.2 |
Ouvrir depuis configuration_page.dart |
Idem |
|
| 10.3 |
Envoyer un message |
Réponse de l'assistant affichée en bulle |
|
| 10.4 |
Réponse avec cards |
Cards affichées sous la réponse texte |
|
| 10.5 |
Tap sur une card de navigation |
Navigue vers la bonne section |
|
| 10.6 |
Instance avec isAssistant = false |
Bouton assistant absent |
|
11. Quotas & Plans d'abonnement
11.1 CRUD SubscriptionPlan (SuperAdmin)
| # |
Action |
Résultat attendu |
✓ |
| 11.1.1 |
POST /api/SubscriptionPlan → { "name": "Standard", "storageQuotaBytes": 5368709120, "aiTokensPerMonth": 5000000 } |
200 + id généré |
|
| 11.1.2 |
POST /api/SubscriptionPlan → { "name": "Premium", "storageQuotaBytes": 21474836480, "aiTokensPerMonth": 20000000 } |
200 |
|
| 11.1.3 |
GET /api/SubscriptionPlan |
200 + liste des plans |
|
| 11.1.4 |
PUT /api/SubscriptionPlan — modifier aiTokensPerMonth |
200 + valeur mise à jour |
|
| 11.1.5 |
DELETE d'un plan non assigné |
202 |
|
| 11.1.6 |
DELETE d'un plan assigné |
409 Conflict |
|
| 11.1.7 |
Appel avec token non-SuperAdmin |
401 / 403 |
|
5368709120 = 5 GB · 21474836480 = 20 GB
11.2 Assignation d'un plan
| # |
Action |
Résultat attendu |
✓ |
| 11.2.1 |
PUT /api/Instance avec subscriptionPlanId: "{id}" |
Plan assigné |
|
| 11.2.2 |
GET /api/Instance/{id} |
Champ subscriptionPlan présent dans la réponse |
|
| 11.2.3 |
PUT /api/Instance avec subscriptionPlanId: "" |
Plan effacé (null en base) |
|
| 11.2.4 |
PUT /api/Instance sans le champ subscriptionPlanId |
Plan existant inchangé |
|
11.3 Endpoint quota
| # |
Action |
Résultat attendu |
✓ |
| 11.3.1 |
GET /api/Instance/{id}/quota — instance sans plan |
storageQuotaBytes: 0, aiTokensPerMonth: 0 |
|
| 11.3.2 |
GET /api/Instance/{id}/quota — instance avec plan Standard |
storageQuotaBytes: 5368709120, aiTokensPerMonth: 5000000 |
|
| 11.3.3 |
Appel avec token Viewer |
200 (l'endpoint accepte Viewer) |
|
11.4 Tracking stockage
| # |
Action |
Résultat attendu |
✓ |
| 11.4.1 |
Uploader une image |
Resource.SizeBytes = taille réelle du fichier |
|
| 11.4.2 |
Uploader un PDF |
Idem |
|
| 11.4.3 |
GET /quota après uploads |
storageUsedBytes = somme des SizeBytes |
|
| 11.4.4 |
Supprimer une ressource + rappeler /quota |
storageUsedBytes diminue |
|
| 11.4.5 |
Ajouter une ImageUrl (externe) |
SizeBytes = 0 — pas comptée |
|
11.5 Tracking quota IA (en tokens)
| # |
Action |
Résultat attendu |
✓ |
| 11.5.1 |
Envoyer un message IA (/api/AI/chat) |
AiTokensThisMonth en base = Usage.TotalTokenCount retourné par Gemini pour cet appel (visible dans AiChatResponse.TokensUsed) |
|
| 11.5.2 |
Envoyer un 2e message |
Compteur = somme des TokensUsed des deux appels (pas juste +1) |
|
| 11.5.3 |
GET /quota |
aiTokensUsed = valeur cumulée en base |
|
| 11.5.4 |
Modifier AiUsageMonthKey → "2025-01" en base, envoyer un message |
Compteur repart au TokensUsed du nouvel appel (pas cumulé avec l'ancien mois), AiUsageMonthKey = mois courant |
|
| 11.5.5 |
Instance avec AiTokensThisMonth déjà ≥ AiTokensPerMonth (quota atteint) |
Nouvel appel → 429 "Quota IA mensuel dépassé", AssistantService pas appelé (pas d'incrément supplémentaire) |
|
| 11.5.6 |
Appel /api/AI/translate sur un texte HTML long (plusieurs paragraphes, plusieurs langues cibles) vs un texte court |
TokensUsed nettement plus élevé pour le texte long — vérifie que le comptage reflète bien le coût réel et pas juste "1 appel = 1 unité" |
|
| # |
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.1 bis — Entrée dans le téléchargement depuis la home v3
Ajouté le 2026-09-08 avec le portage du téléchargement dans le bento (home_3.0.dart). Avant ce portage, une visite hors ligne non téléchargée ouvrait un détail vide, sans message.
| # |
Action |
Attendu |
OK |
| 21.1.7 |
Ouvrir la home v3 avec une configuration isOffline jamais téléchargée |
Tuile ternie, pastille ↓ en haut à droite |
|
| 21.1.8 |
Taper la tuile (pas la pastille) |
Le dialogue de téléchargement s'ouvre — pas un détail vide |
|
| 21.1.9 |
Télécharger, fermer, revenir à la home |
La pastille passe au ✓ vert, la tuile n'est plus ternie |
|
| 21.1.10 |
Taper la tuile téléchargée |
Le détail s'ouvre avec ses sections |
|
| 21.1.11 |
Relancer l'app puis regarder les tuiles |
⚠️ Les visites non téléchargées restent en ↓. Le piège : le fetch écrit une ligne par visite dans la table configurations pour cacher order/gridSpan — si l'état se remettait à se déduire de ces lignes, tout passerait en ✓ au 2ᵉ lancement |
|
| 21.1.12 |
Choisir une langue dans le dialogue, puis télécharger |
Seules les ressources de cette langue sont récupérées (sauf mode admin toutes langues) |
|
| 21.1.13 |
Taper la pastille ✓ d'une visite déjà téléchargée |
Dialogue de mise à jour (downloadPromptUpdate), pas de premier téléchargement |
|
| 21.1.14 |
Ouvrir une visite dont les langues ne contiennent pas la langue courante |
Snackbar « langue non supportée », pas de navigation |
|
| 21.1.15 |
Désactiver une configuration dans manager-app (interrupteur du lien de canal), relancer la home |
La tuile disparaît |
|
21.2 — Fraîcheur du contenu
| # |
Action |
Attendu |
OK |
| 21.2.1 |
⛔ Télécharger une visite, puis remplacer une image dans manager-app (même ressource), puis relancer le téléchargement Cas impossible, réécrit le 2026-08-13 : Upload appelle GenerateHexId() à chaque téléversement (ResourceController:276), donc « remplacer une image » crée un nouvel id et une nouvelle URL — aucun flux ne réécrit le blob d'un id existant. Le cas d'origine testait un scénario qui ne peut pas se produire. À jouer à la place : téléverser une nouvelle image, la placer dans une section déjà téléchargée, re-télécharger |
La nouvelle image arrive sur le device ; l'ancienne, si plus référencée, est purgée (cf. 21.2.3) |
|
| 21.2.2 |
Relancer un téléchargement sans aucune modification |
Rien n'est re-téléchargé, message « à jour » |
|
| 21.2.3 |
Supprimer une ressource dans manager-app puis re-télécharger |
Le fichier local correspondant est purgé |
|
21.3 — Types non disponibles hors ligne
| # |
Action |
Attendu |
OK |
| 21.3.1 |
En mode avion, ouvrir une section Weather |
Message explicite « nécessite une connexion », pas d'écran blanc ni de spinner infini |
|
| 21.3.2 |
Idem sur une section Web (webview externe) |
Message explicite |
|
| 21.3.3 |
Idem sur une section Agenda (événements distants) |
Message explicite |
|
| 21.3.4 |
Ouvrir une SectionVideo pointant sur YouTube en mode avion |
Message explicite, distinct d'une vidéo uploadée qui, elle, doit se lire |
|
21.4 — Robustesse
| # |
Action |
Attendu |
OK |
| 21.4.1 |
Couper le réseau pendant un téléchargement |
Le nombre d'échecs est remonté à l'utilisateur, la visite n'est pas annoncée « téléchargée » |
|
| 21.4.2 |
Relancer après un téléchargement partiel |
Seules les ressources manquantes sont récupérées |
|
22. Recette de bascule — l'app d'aujourd'hui vs la base migrée
Ajoutée le 2026-08-11, à la demande. À jouer juste après le POST /api/migration/run du jour J, avant la bascule DNS et pendant que Mongo est encore lisible. C'est la seule fenêtre où les deux bases coexistent : après, la comparaison n'est plus possible.
Le dry run compare des comptages. Cette recette compare le contenu — un compte juste ne dit pas qu'une section a gardé ses questions, ses traductions et ses médias.
Rappel : MigrationController lit un MongoDB vivant, pas les fichiers de migration-data/.
22.1 — Comptages globaux, les deux bases côte à côte
# Mongo (source) — adapter l'URI au Mongo de prod ou au dump restauré
docker run --rm --network container:myim_mongo_dryrun mongo:6 \
mongosh "mongodb://localhost:27017/TabletDb" --quiet --eval '
["Instances","Users","Configurations","Resources","Sections","Devices"]
.forEach(c => print(c.padEnd(16) + db[c].countDocuments()));'
# Postgres (cible)
docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL'
SELECT 'Instances' AS entite, count(*) FROM "Instances"
UNION ALL SELECT 'Users', count(*) FROM "Users"
UNION ALL SELECT 'Configurations', count(*) FROM "Configurations"
UNION ALL SELECT 'Resources', count(*) FROM "Resources"
UNION ALL SELECT 'Sections', count(*) FROM "Sections"
UNION ALL SELECT 'Devices', count(*) FROM "Devices";
SQL
| # |
Contrôle |
Attendu |
OK |
| 22.1.1 |
Instances, Users, Configurations, Resources, Devices |
Identiques des deux côtés |
|
| 22.1.2 |
Sections |
Postgres = Mongo moins les orphelines signalées. Le rapport les liste dans Skipped : leur nombre doit expliquer exactement l'écart |
|
| 22.1.3 |
FatalError et code HTTP |
Vide, et 200. Un 500 veut dire que la transaction a été annulée : rien n'a été écrit, on corrige et on rejoue |
|
22.2 — Sections par type : c'est là que les pertes se cachent
Un compte global juste peut masquer 19 articles perdus compensés ailleurs. À comparer type par type.
# Mongo — Type est un entier : 0=Map 1=Slider 2=Video 3=Web 4=Menu 5=Quiz
# 6=Article 7=PDF 8=Game 9=Agenda 10=Weather
docker run --rm --network container:myim_mongo_dryrun mongo:6 \
mongosh "mongodb://localhost:27017/TabletDb" --quiet --eval '
db.Sections.aggregate([{$group:{_id:"$Type",n:{$sum:1}}},{$sort:{_id:1}}])
.forEach(r => print(r._id + " : " + r.n));'
# Postgres — héritage TPH, le type vit dans Discriminator
docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL'
SELECT "Discriminator", count(*) FROM "Sections" GROUP BY 1 ORDER BY 1;
SQL
| # |
Contrôle |
Attendu |
OK |
| 22.2.1 |
Chaque type de Mongo se retrouve en Postgres |
Écart nul, ou entièrement expliqué par les orphelines de ce type |
|
| 22.2.2 |
Event et Parcours |
0 des deux côtés — ces types n'existaient pas dans Mongo. En voir apparaître signalerait une erreur de mapping |
|
| 22.2.3 |
Game |
Autant que de type 8 dans Mongo, avec GameType = Puzzle |
|
22.3 — Les collections filles, invisibles dans les comptages de sections
C'est l'écart (b) : un quiz peut arriver sans une seule question sans que rien ne le signale.
docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL'
SELECT count(*) AS questions,
count(DISTINCT "SectionQuizId") AS quiz_pourvus
FROM "QuizQuestions";
SQL
| # |
Contrôle |
Attendu |
OK |
| 22.3.1 |
Total QuizQuestions |
41 sur l'export d'avril — à recalculer sur le dump du jour J |
|
| 22.3.2 |
Ouvrir un quiz migré dans manager-app |
Ses questions sont là, avec leurs réponses et la bonne réponse cochée |
|
| 22.3.3 |
Ordre des réponses |
Identique à l'ancienne app |
|
| 22.3.4 |
EventAgendas et GuidedPaths |
Vides — Mongo n'en contient pas. Non vides = un mapping a inventé des données |
|
22.4 — Ce que la migration génère ou décide
Ces colonnes n'existent pas dans Mongo : rien à confronter, tout à vérifier. C'est ici que se logent les pannes silencieuses du §1quinquies.
docker exec -i myim_postgres psql -U mym -d my_info_mate <<'SQL'
SELECT "Name", "WebSlug", left("PublicApiKey", 6) AS cle, "SubscriptionPlanId",
"AiTokensPerMonth", "IsMobile", "IsTablet", "IsWeb", "IsAssistant"
FROM "Instances" ORDER BY "Name";
SQL
| # |
Contrôle |
Attendu |
OK |
| 22.4.1 |
PublicApiKey |
Non nulle pour les 4 instances, préfixe ap_. Nulle = les apps visiteur ne s'authentifient plus |
|
| 22.4.2 |
WebSlug |
Non nul et unique, dérivé du nom |
|
| 22.4.3 |
SubscriptionPlanId |
plan-premium pour MyInfoMate et VisitNamur, plan-pro pour MDLF et le Fort |
|
| 22.4.4 |
AiTokensPerMonth |
> 0 sur les deux Premium. 0 sur les deux Pro, c'est voulu — Pro n'inclut pas l'IA |
|
| 22.4.5 |
IsMobile / IsTablet |
Cohérents avec les apps réellement utilisées par chaque client |
|
| 22.4.6 |
IsWeb |
Faux partout — le canal web n'existait pas avant, il s'active à la main |
|
| 22.4.7 |
Rôles des utilisateurs |
Tous InstanceAdmin. Aucun SuperAdmin : à poser à la main sur ton compte, sinon les écrans SuperAdmin sont inatteignables |
|
| 22.4.8 |
Resources.StoragePath |
Renseigné pour les types fichier, nul pour les types URL (ImageUrl, VideoUrl, JSONUrl) |
|
| 22.4.9 |
Resources.SizeBytes à 0 |
Uniquement celles listées dans les Errors du rapport (HEAD en échec). Le quota les comptera pour rien tant qu'un backfill ne passe pas |
|
22.5 — Comparaison à l'écran, ancienne app vs nouvelle
Les comptages ne disent rien des traductions, des médias liés ni de l'ordre. Prendre une configuration par instance et les ouvrir côte à côte.
| # |
Contrôle |
Attendu |
OK |
| 22.5.1 |
La liste des sections, dans l'ordre |
Même ordre, mêmes libellés |
|
| 22.5.2 |
Une section Article |
Contenu HTML identique, dans chaque langue de la configuration |
|
| 22.5.3 |
Une SectionMap |
Mêmes points, mêmes catégories, même icône, même centrage et zoom |
|
| 22.5.4 |
Un Menu |
Les sections liées sont les mêmes (LinkMenuSectionsAsync relie après coup) |
|
| 22.5.5 |
Un PDF |
Le fichier s'ouvre, et c'est le bon |
|
| 22.5.6 |
Une sous-section |
Toujours rattachée au bon parent |
|
| 22.5.7 |
Couleurs et loader de la configuration |
Identiques |
|
| 22.5.8 |
Une app visiteur avec sa vraie clé API |
Se connecte et affiche le contenu — le seul contrôle qui teste PublicApiKey de bout en bout |
|
22.6 — Le contenu qu'on sait perdu
| # |
Contrôle |
Attendu |
OK |
| 22.6.1 |
Relire les Skipped du rapport |
Chaque ligne est une section orpheline connue et acceptée. Une surprise ici = on arrête et on regarde |
|
| 22.6.2 |
Les 20 sections MDLF orphelines |
Soit récupérées (les 2 configurations recréées dans Mongo avant la bascule), soit leur perte actée avec le client |
|
23. QR codes, nom d'application, page de téléchargement, proximité
Prérequis : migration AddAppNameQrAndStoresToApplicationInstance appliquée ; visitapp buildée après flutter clean (empreinte de libapp.so vérifiée).
23.1 — Réglages dans manager-app (écrans Mobile et Web)
| # |
Action |
Résultat attendu |
✓ |
| 23.1.1 |
Saisir le nom de l'application en FR/EN/NL, recharger |
Valeurs conservées, par app (Mobile ≠ Web) |
|
| 23.1.2 |
Décocher Afficher le scan de QR code, recharger |
Case restée décochée |
|
| 23.1.3 |
En SuperAdmin, saisir les liens App Store / Play Store (écran Mobile) |
Enregistrés |
|
| 23.1.4 |
En InstanceAdmin |
Les champs de liens stores n'apparaissent pas ; un PUT forgé avec d'autres liens ne les modifie pas |
|
| 23.1.5 |
QR d'une section (instance avec app mobile) |
Encode https://app.myinfomate.be/download/{instanceId}/{configId}/{sectionId} |
|
| 23.1.6 |
QR d'une section (instance web seule) |
Encode https://app.myinfomate.be/{slug}/{configId}/sections/{sectionId} |
|
23.2 — Scan dans l'app mobile
| # |
QR scanné |
Depuis |
Résultat attendu |
✓ |
| 23.2.1 |
Id brut d'une section (QR du Fort) |
Accueil |
La visite s'ouvre directement sur la section |
|
| 23.2.2 |
Id brut d'une section de la visite ouverte |
Visite |
La section s'ouvre |
|
| 23.2.3 |
Id brut d'une section d'une autre visite |
Visite |
Popup « autre visite » puis ouverture sur la section |
|
| 23.2.4 |
URL web.mymuseum.be/… d'un vrai QR MDLF imprimé |
Accueil et visite |
Même comportement qu'avant |
|
| 23.2.5 |
Nouvelle URL app.myinfomate.be/download/… |
Accueil et visite |
Ouvre la section |
|
| 23.2.6 |
QR d'une autre instance |
Accueil |
« QR code invalide » |
|
| 23.2.7 |
Visite hors ligne, mode avion, id brut |
Accueil |
Résolu depuis la base locale |
|
| 23.2.8 |
Scan QR désactivé dans le manager |
— |
Bouton absent sur l'accueil, dans la visite et dans un menu |
|
| 23.2.9 |
Nom de l'app renseigné |
— |
En-tête de l'accueil = nom de l'app (langue active) ; sans nom : titre de la 1re visite comme avant |
|
23.3 — Web (visitapp-web)
| # |
Action |
Résultat attendu |
✓ |
| 23.3.1 |
Ouvrir /download/{instanceId}/{c}/{s} (téléphone, appareil photo) |
Image principale, nom de l'app, boutons stores renseignés, textes dans la langue du navigateur |
|
| 23.3.2 |
Même URL sans liens stores |
« Bientôt disponible sur les stores » |
|
| 23.3.3 |
Instance sans app mobile |
404 |
|
| 23.3.4 |
Scanner un QR download depuis l'app web |
Ouvre la section web |
|
| 23.3.5 |
Scan QR désactivé sur l'app Web |
Bouton absent (accueil et visite) |
|
| 23.3.6 |
Nom de l'app renseigné |
Titre sur l'accueil et dans l'onglet |
|
| 23.3.7 |
Visite avec sections géolocalisées, activer le bouton de proximité, forcer la position dans les DevTools |
Carte « À proximité » une fois par section et par session, 20 s minimum entre deux |
|
| 23.3.8 |
⚠️ Prendre l'instanceId d'un vrai QR MDLF imprimé et ouvrir /download/{cet id}/… |
La page de MDLF s'affiche (son nom, son image, ses liens). Si elle ne trouve rien, l'id a changé à la migration Mongo → Postgres : la redirection ne servirait à rien, à traiter avant de la poser |
|
| 23.3.9 |
Instance sans nom d'application renseigné |
Le titre retombe sur le nom de l'instance, jamais sur « Application mobile » |
|
| 23.3.10 |
Ancien QR d'une instance web seule redirigé vers /download/… |
Redirection vers la visite web à la bonne section |
|
23.4 — Proximité et notifications (mobile, device réel)
| # |
Action |
Résultat attendu |
✓ |
| 23.4.1 |
Visite avec zones GPS (sans beacon) |
Le bouton de proximité apparaît une fois les sections chargées |
|
| 23.4.2 |
Activer, entrer dans une zone (mock location Android), app ouverte |
Popup de suggestion, pas de notification |
|
| 23.4.3 |
Même chose écran verrouillé |
Notification « Contenu à proximité » + titre de la section |
|
| 23.4.4 |
Toucher la notification |
L'app s'ouvre sur la section |
|
| 23.4.5 |
Rester dans la zone écran verrouillé |
Une seule notification par section et par visite |
|
| 23.4.6 |
Android : pendant le scan |
Notification persistante « Suggestions de contenu à proximité activées » ; disparaît en coupant le bouton ou en quittant la visite |
|
| 23.4.7 |
iOS : écran verrouillé |
Indicateur de localisation en arrière-plan ; notification reçue (beacon et GPS) |
|
| 23.4.8 |
App tuée |
Aucune notification (hors périmètre, par choix) |
|
| 23.4.9 |
Mode vocal lunettes sans permission de localisation (Android 14+) |
Pas de crash ; le service de premier plan ne démarre pas |
|
24. Onglet XR — flotte de casques (lot XR-2)
Livré le 2026-09-12. Aucun casque ne sait encore s'appairer — E2 de XR-4 n'est pas fait —
donc l'appareil de test se crée à la main par l'API. C'est la seule façon de voir la carte d'un
casque appairé aujourd'hui, et ça teste exactement le chemin que prendra Unity.
24.0 — Prérequis, à faire une fois
Il n'y a pas encore d'écran pour activer le canal VR sur une instance (carte kanban « Écran
SuperAdmin — add-on IA & canaux », planifiée). Tout se fait par l'API, en SuperAdmin.
| # |
Action |
Résultat attendu |
✓ |
| 24.0.1 |
PUT /api/instance avec isVR: true sur l'instance de test |
200, et isVR toujours à true après un GET |
|
| 24.0.2 |
POST /api/applicationinstance avec appType: 3 et l'instanceId |
200 — sans elle, l'onglet affiche « Application VR non configurée » |
|
| 24.0.3 |
Créer un lien de configuration sur cette ApplicationInstance VR (depuis l'onglet Configuration, une fois 24.0.2 fait) |
Le lien existe — il servira de carte « aucun casque appairé » |
|
| 24.0.4 |
POST /api/device avec appType: 3, un identifier bidon, l'instanceId et un configurationId valide |
200. ⚠️ Deux pièges : sans appType, le serveur crée une tablette (elle irait dans l'onglet Kiosk), et l'appel crée son propre AppConfigurationLink — il ne se rattache pas au lien de 24.0.3, on se retrouve donc avec deux cartes |
|
| # |
Action |
Résultat attendu |
✓ |
| 24.1.1 |
Ouvrir le manager sur l'instance de test |
Entrée « VR » sous Applications, à côté de Mobile / Kiosk / Web |
|
| 24.1.2 |
Basculer sur une instance sans canal VR |
L'entrée « VR » disparaît, et l'écran affiché n'est pas resté celui de l'instance précédente |
|
| 24.1.3 |
Instance avec isVR: true mais sans ApplicationInstance VR (sauter 24.0.2) |
« Application VR non configurée », pas d'écran vide ni de crash |
|
24.2 — Sous-onglet « Configuration »
| # |
Action |
Résultat attendu |
✓ |
| 24.2.1 |
Ouvrir l'onglet VR |
Deux sous-onglets, « Configuration » actif, souligné |
|
| 24.2.2 |
Modifier les langues de l'application VR, recharger |
Conservées, et sans effet sur les langues de Mobile / Web / Kiosk |
|
| 24.2.3 |
Ajouter un lien de configuration depuis cet onglet |
Apparaît dans la liste, et dans le sous-onglet « Casques » |
|
| 24.2.4 |
Comparer à l'onglet Mobile |
Mêmes champs, même comportement — c'est le même écran réutilisé |
|
24.3 — Sous-onglet « Casques »
| # |
Action |
Résultat attendu |
✓ |
| 24.3.1 |
Ouvrir « Casques » |
Le code PIN de l'instance en tête, puis la grille — deux cartes, celle du lien de 24.0.3 et celle créée par le POST de 24.0.4 |
|
| 24.3.2 |
La carte du lien de 24.0.3 |
« Aucun casque appairé » + le nom de la configuration |
|
| 24.3.3 |
La carte du casque créé en 24.0.4 |
Son nom (ou son identifiant si sans nom), le nom de la configuration, une pastille rouge (jamais connecté) |
|
| 24.3.4 |
Mettre connected: true sur ce device par l'API, revenir sur l'onglet |
Pastille verte |
|
| 24.3.5 |
Renseigner batteryLevel, appVersion, lastSeen par l'API, recharger |
Les trois lignes s'affichent sous la configuration ; la date est au format jj/mm/aaaa hh:mm |
|
| 24.3.6 |
Device sans lastSeen |
« Jamais vu », pas de ligne vide ni de date à 1970 |
|
| 24.3.7 |
Bouton crayon → changer le nom et la configuration → valider |
Notification de succès, la carte se met à jour sans recharger la page |
|
| 24.3.8 |
Recharger après 24.3.7 |
Le nom et la configuration ont tenu — les deux passent par deux appels distincts (device/mainInfos puis le lien de configuration) |
|
| 24.3.9 |
Revenir sur « Configuration » puis sur « Casques » |
La configuration réassignée en 24.3.7 est bien celle affichée des deux côtés |
|
| 24.3.10 |
Ouvrir l'onglet Kiosk de la même instance |
Le casque de test n'y apparaît pas, et les tablettes existantes y sont toujours toutes |
|
24.4 — Langues et affichage
| # |
Action |
Résultat attendu |
✓ |
| 24.4.1 |
Basculer le manager en EN puis en NL |
Titres d'onglets, « Casques », « Aucun casque appairé », « Batterie / Version / Vu le » traduits — aucun texte français résiduel |
|
| 24.4.2 |
Réduire la fenêtre à une largeur de tablette |
La grille repasse en moins de colonnes, rien n'est tronqué en hauteur dans les cartes |
|
| 24.4.3 |
Un casque au nom très long |
Ellipse, pas de débordement |
|
24.5 — Ce que ce plan ne teste pas
Appairage réel par pincode depuis le casque, remontée automatique de batterie et de version,
rafraîchissement MQTT : tout cela est dans XR-4 (E2) et XR-5, pas encore écrit. Les valeurs
de 24.3.5 sont posées à la main précisément parce que rien ne les alimente encore.
25. App VR — appairage et lecture du contenu (XR-4, items E2-E3)
Écrit le 2026-09-12, jamais exécuté : ni contre un vrai serveur, ni sur le casque. Le code
compile contre les DLL Unity, c'est tout ce qu'on sait. Ce chapitre est donc une première
exécution, pas une non-régression — traiter chaque échec comme une information, pas comme un bug.
Prérequis : le §24.0 joué (canal VR activé sur l'instance de test), un Quest en mode développeur,
et la scène construite par MyInfoMate → Construire la scène Boot (E2-E3 — appairage et export).
Le code PIN, l'URL du serveur et la langue se posent dans l'inspecteur du Bootstrap avant le
build : sans l'Interaction SDK (E1) il n'y a ni pointeur ni clavier dans le casque.
25.1 — Appairage (E2)
| # |
Action |
Résultat attendu |
✓ |
| 25.1.1 |
PIN correct, canal VR activé, Build And Run |
Le casque affiche le nom de la configuration et ses sections. Console : [Pairing] Casque appairé — device … |
|
| 25.1.2 |
Ouvrir l'onglet XR du manager après 25.1.1 |
Le casque apparaît dans la grille, nom = celui du champ headsetName, pastille verte |
|
| 25.1.3 |
Ouvrir l'onglet Kiosk de la même instance |
Le casque n'y est pas — c'est tout l'objet de appType = 3 |
|
| 25.1.4 |
Relancer l'app sans rien changer |
Aucun nouvel appareil dans le manager : l'identifiant matériel est stable et le serveur reconnaît l'appareil |
|
| 25.1.5 |
Relancer avec un PIN vidé |
Toujours appairé (persistance PlayerPrefs), le PIN n'est plus lu |
|
| 25.1.6 |
Cocher forgetPairing, rebuild |
Réappairage complet ; vérifier qu'il n'y a pas de second appareil dans le manager |
|
| 25.1.7 |
PIN inexistant |
« Ce code PIN ne correspond à aucun lieu. » — pas d'écran noir |
|
| 25.1.8 |
PIN d'une instance sans canal VR (sauter 24.0.2) |
« Le canal VR n'est pas activé pour ce lieu. » ⚠️ C'est un 404 traduit : si le message générique « Ce contenu n'existe plus » apparaît, la traduction du 404 est à revoir |
|
| 25.1.9 |
URL de serveur injoignable (couper le wifi avant le lancement) |
« Le serveur est injoignable. Vérifiez la connexion du casque. » au bout de ~20 s, pas un gel |
|
25.2 — Lecture du contenu (E3)
| # |
Action |
Résultat attendu |
✓ |
| 25.2.1 |
Instance avec plusieurs sections actives |
Le message liste jusqu'à 5 sections avec leur type ; la console (adb logcat) les liste toutes |
|
| 25.2.2 |
Comparer à l'écran Configurations du manager |
Même nombre de sections de premier niveau, même ordre. Les sous-sections d'un Menu ne comptent pas |
|
| 25.2.3 |
Une section désactivée dans le manager |
Elle n'apparaît pas dans le casque |
|
| 25.2.4 |
Passer language de FR à EN, rebuild |
Les titres changent de langue. ⚠️ Si une section n'a pas de traduction EN, elle retombe sur la première disponible — c'est voulu, pas un bug |
|
| 25.2.5 |
Une configuration contenant tous les types de section (Game, PDF, Weather…) |
Rien ne plante : les 13 types se lisent, même ceux que la VR ne rendra jamais |
|
| 25.2.6 |
Configuration vide (aucune section) |
« 0 sections », pas d'erreur |
|
| 25.2.7 |
⚠️ Ouvrir le JSON d'export d'une vraie configuration (navigateur, avec la clé) |
Il porte exportVersion: 1, generatedAt, et les champs spécifiques de chaque section — les contents d'un Slider, les points d'une Map, le model3DResourceId d'une maquette. Jusqu'au 12/09 il ne portait que les champs communs, et l'export est aussi ce qui part en visite hors ligne |
|
| 25.2.8 |
Sur ce même JSON, chercher une section multilingue |
Les titres portent toutes les langues, pas seulement une : le casque change de langue sans rappeler le serveur |
|
| 25.2.9 |
⚠️ mymuseum-visitapp : télécharger une visite hors ligne, couper le réseau, l'ouvrir |
Les galeries, cartes et quiz ont leur contenu. C'est le même correctif d'export : à vérifier avant de croire que seul le casque était concerné |
|
25.3 — Cache et mode hors ligne (E4)
Le cache s'écrit dès le premier chargement réussi. C'est l'argument n°1 d'Unity contre le web :
si ces cas-là ne passent pas, le choix de stack perd sa justification.
| # |
Action |
Résultat attendu |
✓ |
| 25.3.1 |
Lancer une première fois avec le wifi |
Contenu affiché, et un fichier apparaît dans /sdcard/Android/data/<package>/files/content/ (adb shell ls) |
|
| 25.3.2 |
Couper le wifi du casque, relancer l'app |
Le contenu s'affiche quand même, avec la ligne « hors ligne — contenu du jj/mm hh:mm » |
|
| 25.3.3 |
Comparer le délai d'affichage avec et sans wifi |
Le départ sur cache doit être plus rapide que le premier lancement : c'est tout l'objet du « cache d'abord » |
|
| 25.3.4 |
Modifier un titre de section dans le manager, relancer l'app (wifi actif) |
L'ancien contenu s'affiche d'abord, puis se remplace par le nouveau sans relancer — console : [Content] Contenu mis à jour depuis le serveur |
|
| 25.3.5 |
Même chose sans modification |
Aucun clignotement : le contenu identique ne doit pas provoquer de réaffichage |
|
| 25.3.6 |
Éteindre brutalement le casque pendant un rafraîchissement, relancer |
Le contenu précédent est intact — l'écriture est atomique, jamais un JSON tronqué |
|
| 25.3.7 |
Wifi coupé et cache vidé (désinstaller/réinstaller) |
« Le serveur est injoignable… », pas un écran noir |
|
25.4 — Télémétrie (E9)
Rien n'émet encore d'événement automatiquement : Telemetry existe mais n'est appelée que
depuis le menu (E5), pas encore écrit. À jouer après E5, ou en déclenchant un appel à la main.
| # |
Action |
Résultat attendu |
✓ |
| 25.4.1 |
Émettre un SectionView depuis le casque |
Écran Statistiques du manager : le canal VR apparaît dans le filtre, sans aucune configuration |
|
| 25.4.2 |
Vérifier le canal de l'événement en base |
AppType = 3. ⚠️ S'il vaut 0 (Mobile), c'est le parsing par nom qui a échoué en silence — le piège documenté du StatsController |
|
| 25.4.3 |
Émettre pendant que le wifi est coupé |
L'app continue normalement, la console note l'échec, rien ne remonte au visiteur |
|
25.5 — Menu flottant : manette, main, tête (E5)
Scène MyInfoMate → Construire la scène Boot (E5 — menu flottant). C'est l'app : elle appaire,
sert le cache, affiche le menu et émet la télémétrie. Les cas 25.1 à 25.4 valent aussi pour elle.
| # |
Action |
Résultat attendu |
✓ |
| 25.5.1 |
Lancer avec une configuration de 3-4 sections |
Un arc de panneaux à ~2,2 m, à hauteur d'yeux, chacun de face |
|
| 25.5.2 |
Tourner la tête |
Le menu reste où il est — un menu qui suit la tête est invisable et donne la nausée |
|
| 25.5.3 |
Manettes posées, mains baissées. Viser un panneau avec la tête |
Il s'éclaircit, et un anneau se remplit en ~1,2 s. Pas de rayon visible — c'est voulu, un trait au centre du champ de vision gêne en permanence |
|
| 25.5.4 |
Détourner la tête avant la fin |
L'anneau se vide, rien ne se déclenche |
|
| 25.5.5 |
Maintenir jusqu'au bout |
Le titre de la section s'affiche. ⚠️ Vérifier que c'est la bonne — c'est tout l'objet du test |
|
| 25.5.6 |
Balayer le menu de la tête, sans s'arrêter |
Aucun déclenchement accidentel. Si ça part trop vite, allonger dwellSeconds |
|
| 25.5.7 |
Prendre une manette. Viser |
Un rayon apparaît, le panneau visé s'éclaircit, et l'anneau ne se remplit pas |
|
| 25.5.8 |
Appuyer sur la gâchette d'index |
Déclenchement immédiat, sans attente |
|
| 25.5.9 |
Reposer la manette, reprendre la tête |
Le rayon disparaît et la temporisation revient — la bascule est automatique, sans réglage |
|
| 25.5.10 |
Mains nues (building block Hand Tracking dans la scène) |
Rayon depuis la main, déclenchement au pincement pouce-index |
|
| 25.5.11 |
Maintenir le pincement |
Un seul déclenchement, pas un par image |
|
| 25.5.12 |
Sans le building block Hand Tracking |
Manette et tête fonctionnent quand même, aucune erreur en console |
|
| 25.5.13 |
Configuration de plus de 5 sections |
Une seconde rangée en dessous, pas un cercle autour du visiteur |
|
| 25.5.14 |
Sections avec image dans le manager |
Les vignettes apparaissent après le menu, sans le bloquer |
|
| 25.5.15 |
Section sans image |
Le panneau reste lisible, titre seul |
|
| 25.5.16 |
Relancer hors ligne après un premier lancement |
Menu et vignettes viennent du cache |
|
| 25.5.17 |
Après 25.5.5, regarder les stats du manager |
MenuItemTap et SectionView sur le canal VR (voir 25.4) |
|
⚠️ Deux réglages à rapporter, ils ne se décident pas sur le papier :
| # |
Réglage |
Question |
Valeur retenue |
| 25.5.18 |
dwellSeconds (1,2 s) |
Pénible, ou trop prompt à déclencher ? |
|
| 25.5.19 |
Distance et hauteur de l'arc (2,2 m / 1,45 m) |
Texte lisible sans effort ? Panneaux hauts atteignables sans lever la tête ? |
|
25.6 — Sections affichables : Slider, Map, Parcours, Event (E8)
Les quatre types qui s'ouvrent, tous dans la même vue paginée — une chose à la fois, devant
soi, avec de quoi passer à la suivante. Prérequis : une configuration de test qui contient un
Slider (3+ images), une Map (3+ points), un Event avec du programme aujourd'hui, et au moins
un type écarté (Article ou Quiz).
| # |
Action |
Résultat attendu |
✓ |
| 25.6.1 |
Choisir un Slider dans le menu |
Le menu disparaît, une grande image apparaît à 2,5 m avec « 1 / N » |
|
| 25.6.2 |
Galerie de 10+ images |
La première s'affiche sans attendre les suivantes ; le compteur monte au fur et à mesure |
|
| 25.6.3 |
Viser ▶ |
Image suivante, compteur à jour, légende à jour |
|
| 25.6.4 |
Arriver à la dernière image |
La flèche ▶ disparaît — un bouton présent qui ne réagit pas se lit comme une panne |
|
| 25.6.5 |
Première image |
Idem pour ◀ |
|
| 25.6.6 |
« Retour au menu » |
La galerie disparaît, le menu revient tel qu'il était (même position, mêmes vignettes) |
|
| 25.6.7 |
Après 25.6.6, stats du manager |
Un SectionLeave avec une durée cohérente avec le temps passé |
|
| 25.6.8 |
Slider dont les contenus ont un ordre défini dans le manager |
Même ordre dans le casque |
|
| 25.6.9 |
Slider sans image, ou dont les images ne se téléchargent pas |
« Cette galerie n'a aucune image affichable. », pas un cadre vide |
|
| 25.6.10 |
Rouvrir le même Slider, wifi coupé |
Images servies depuis le cache |
|
| 25.6.11 |
Choisir une section non gérée (Article, Quiz…) |
Le titre s'affiche avec « pas encore affichable », et on peut revenir |
|
Map et Parcours — rendus en liste de points d'intérêt, un par page :
| # |
Action |
Résultat attendu |
✓ |
| 25.6.12 |
Ouvrir une Map |
Un POI par page : titre, description, horaires s'il y en a, photo s'il y en a |
|
| 25.6.13 |
Comparer à la liste de la Map dans le manager |
Mêmes points, aucun oublié |
|
| 25.6.14 |
POI sans photo |
Le cadre reste sombre et le texte tient tout seul, pas de trou |
|
| 25.6.15 |
Ouvrir un Parcours (Map avec isParcours) |
Même rendu qu'une Map — c'est assumé, voir ci-dessous |
|
Event — le programme, une page par bloc :
| # |
Action |
Résultat attendu |
✓ |
| 25.6.16 |
Event avec du programme aujourd'hui |
Seuls les blocs du jour, triés par heure, avec « 14:00 – 15:30 » |
|
| 25.6.17 |
Event dont le programme du jour est vide |
Les dates suivantes s'affichent, au format « 14/09 · 14:00 » |
|
| 25.6.18 |
Event entièrement passé |
« Rien au programme aujourd'hui. », pas un cadre vide |
|
⚠️ Trois choses à juger sur place, pas sur le papier :
| # |
Question |
Impression |
| 25.6.19 |
L'image fait 1,6 × 0,9 m à 2,5 m. Trop grande, elle oblige à balayer la tête ; trop petite, elle ne vaut pas mieux qu'une tablette |
|
| 25.6.20 |
Une Map en liste de POI est-elle acceptable en démo, ou est-ce que ça se voit trop que la maquette 3D manque ? |
|
| 25.6.21 |
Un Parcours rendu comme une Map passe-t-il, ou faut-il le masquer du menu tant qu'il n'a pas son vrai rendu ? |
|
25.7 — Ressource 360° et lecture immersive (E6)
Deux moitiés : déclarer une 360 dans le manager, puis la voir dans le casque. La première se
teste sans casque — à faire dès maintenant si l'occasion se présente.
Dans manager-app :
| # |
Action |
Résultat attendu |
✓ |
| 25.7.1 |
Médiathèque → ajouter un .jpg |
Pastille « Image », comme avant |
|
| 25.7.2 |
Toucher la pastille |
Elle bascule en « Image 360° » et se colore |
|
| 25.7.3 |
Envoyer, puis rouvrir la fiche du média |
Type conservé après rechargement |
|
| 25.7.4 |
⚠️ Vérifier les dimensions du fichier stocké (fiche du média, ou Firebase) |
La 360 n'est PAS redimensionnée : une 8192×4096 reste 8192×4096. Si elle est ressortie en 2560 de large, l'exclusion de compression n'a pas pris — et dans le casque ce sera une bouillie |
|
| 25.7.5 |
Même chose avec un .jpg laissé en « Image » |
Celui-là est compressé à 2560 : la compression ordinaire n'a pas été cassée au passage |
|
| 25.7.6 |
Un .mp4 basculé en « Vidéo 360° » |
Type conservé, fichier intact |
|
| 25.7.7 |
Déposer un .glb |
Accepté, type « Modèle 3D » déduit tout seul |
|
| 25.7.8 |
Remplacer le fichier d'une ressource 360 existante |
Toujours pas de compression |
|
| 25.7.9 |
Passer le manager en EN puis NL |
Libellés et infobulle traduits |
|
Dans le casque (une SectionVideo dont le média est la ressource 360) :
| # |
Action |
Résultat attendu |
✓ |
| 25.7.10 |
Choisir la section depuis le menu |
Le décor devient la photo : on tourne la tête, on est dedans |
|
| 25.7.11 |
⚠️ Regarder la couleur |
Si le ciel est magenta, Skybox/Panoramic n'est pas dans Always Included Shaders — ça ne se voit que sur le casque, jamais dans l'éditeur |
|
| 25.7.12 |
Chercher la couture verticale du panorama |
Pas de trait net à la jonction |
|
| 25.7.13 |
Juger la définition |
C'est ici que se voit une 360 qui aurait été compressée |
|
| 25.7.14 |
Enchaîner cinq ou six 360 d'affilée, en revenant au menu entre chaque |
L'app ne ralentit pas et ne se fait pas tuer : chaque image est libérée en sortant. Une fuite ne se voit qu'au bout de plusieurs, jamais sur un essai unique |
|
| 25.7.15 |
Section en vidéo 360 |
Elle démarre, tourne en boucle, le son sort |
|
| 25.7.16 |
Attendre 4 s sans rien faire |
Le panneau de retour s'efface : il ne reste rien dans le décor |
|
| 25.7.17 |
Baisser les yeux |
Il réapparaît, posé sous le regard courant |
|
| 25.7.18 |
Se retourner complètement, puis baisser les yeux |
Il est devant soi, pas resté à sa position de départ |
|
| 25.7.19 |
Prendre une manette |
Il réapparaît sans avoir à baisser la tête |
|
| 25.7.20 |
⚠️ Sans rien savoir, sauriez-vous ressortir ? Faire essayer quelqu'un qui n'a pas lu ce plan |
S'il reste coincé dans la photo, 4 s d'affichage initial ne suffisent pas — c'est le réglage à revoir |
|
| 25.7.21 |
Le retirer |
Le menu revient sur le décor d'origine, pas sur la 360 restée affichée |
|
| 25.7.22 |
Rouvrir une autre 360 puis revenir |
Idem, aucun décor résiduel |
|
| 25.7.23 |
Relancer hors ligne |
La 360 vient du cache disque |
|
| 25.7.24 |
Section Video pointant une URL YouTube |
Pas de skybox — elle n'est pas téléchargeable, c'est traité comme un type non géré |
|
25.8 — Maquette 3D et points d'intérêt (E7)
Trois endroits à vérifier : la section dans le manager, le viewer qui sert d'éditeur, et le rendu
dans le casque. ⚠️ Le viewer n'a encore jamais tourné — le lancer seul (npm run dev, glisser un
GLB) avant de juger l'intégration évitera de confondre deux problèmes.
Dans manager-app :
| # |
Action |
Résultat attendu |
✓ |
| 25.8.1 |
Créer une section, groupe « lieux » |
Le type Scène 3D est proposé, avec son icône |
|
| 25.8.1b |
Choisir le mode Objet puis Décor |
L'explication sous le sélecteur change ; le choix tient après enregistrement |
|
| 25.8.2 |
Ouvrir la section |
Le sélecteur de modèle ne propose que des ressources Model3D |
|
| 25.8.3 |
Sans modèle choisi |
« Choisissez d'abord un modèle 3D » — pas d'iframe vide |
|
| 25.8.4 |
Choisir un .glb |
Le viewer apparaît et charge le modèle |
|
| 25.8.5 |
Le viewer ne se charge pas |
Vérifier l'URL : il doit être servi sous le même domaine que le manager (/viewer/index.html), ou lancé avec --dart-define=VIEWER_URL=… |
|
| 25.8.6 |
Ajouter des points d'intérêt à la section |
Ils apparaissent dans le viewer, à l'origine du modèle |
|
| 25.8.7 |
Glisser un point, Alt+glisser pour la hauteur |
Sa position suit |
|
| 25.8.8 |
Enregistrer, recharger l'écran |
Les points sont là où on les a mis |
|
| 25.8.9 |
Vérifier en base |
Model3DPosition renseigné en jsonb, Geometry inchangé — les deux positions coexistent |
|
Dans le casque :
| # |
Action |
Résultat attendu |
✓ |
| 25.8.10 |
Ouvrir une scène en mode Objet |
Le modèle est posé devant le visiteur, à hauteur d'yeux ; on tourne autour sans se déplacer |
|
| 25.8.10b |
Ouvrir une scène en mode Décor |
Le visiteur est dedans et regarde autour. ⚠️ Si en mode Objet on se retrouve dans le modèle, le mode n'est pas lu — c'est lui qui décide si le rig est passé au moteur |
|
| 25.8.11 |
⚠️ Le test qui compte : placer trois points à des endroits franchement asymétriques |
Ils sont exactement là dans le casque. Une maquette symétrique ne prouve rien — c'est le bug que GltfSpace et calibration.glb existent pour attraper |
|
| 25.8.12 |
Viser un point 1,2 s |
Il s'éclaircit et son audio part de lui, pas du centre de la tête |
|
| 25.8.13 |
Maquette dont un point n'a pas de position 3D |
Il est simplement absent, rien ne plante |
|
| 25.8.14 |
Modèle absent ou illisible |
« Cette maquette n'a pas pu être chargée », retour au menu possible |
|
| 25.8.15 |
Mesurer le temps de chargement d'un GLB lourd |
À rapporter : c'est ce qui dira s'il faut un écran de progression |
|
25.9 — Mode borne (E10)
Ce qui sépare une démo d'une borne qu'on laisse tourner 8 h. À jouer à deux, en se passant le
casque : c'est exactement le scénario visé.
| # |
Action |
Résultat attendu |
✓ |
| 25.9.1 |
Retirer le casque, le reposer sur la table |
Retour au menu immédiat, sans attendre le délai |
|
| 25.9.2 |
Le remettre, tourné dans une autre direction |
Le menu est devant soi, pas dans le dos — c'est le cas qui casse une borne en salle |
|
| 25.9.3 |
Retirer le casque alors qu'une galerie était ouverte |
Le visiteur suivant trouve le menu, pas la galerie du précédent |
|
| 25.9.4 |
Idem, puis regarder les stats |
Un SectionLeave a bien été émis pour la galerie abandonnée |
|
| 25.9.5 |
Garder le casque sur la tête, immobile, 90 s |
Retour au menu |
|
| 25.9.6 |
Lire une longue légende sans bouger, moins de 90 s |
Pas de retour au menu intempestif — si ça coupe, allonger idleSeconds |
|
| 25.9.7 |
Feuilleter une galerie pendant plus de 90 s, sans bouger la tête |
Pas de retour au menu : choisir est une activité, même sans mouvement |
|
| 25.9.8 |
Après un retour au menu, stats du manager |
Les événements suivants portent un sessionId différent — sinon une journée de borne compte pour un seul visiteur |
|
| 25.9.9 |
Laisser le casque porté, sans rien faire, 10 min |
L'écran ne s'éteint pas |
|
| 25.9.10 |
Lancer dans le simulateur, ou dans l'éditeur |
La session ne se réinitialise pas toutes les 90 s : sans capteur de présence, on considère le casque porté |
|
⚠️ Ce que E10 ne fait pas : verrouiller le casque sur l'app. Ce n'est pas du code, c'est le mode
appareil de Meta — à régler sur le casque, et à revérifier avant de l'écrire dans une offre.
25.10 — Supervision de flotte (XR-5)
Ce qui remplit enfin les colonnes batterie / version / dernier vu de l'onglet XR.
| # |
Action |
Résultat attendu |
✓ |
| 25.10.1 |
Lancer l'app, puis ouvrir l'onglet XR |
Le casque passe en ligne tout de suite, pas au bout de 3 min |
|
| 25.10.2 |
Recharger l'onglet après quelques minutes |
Batterie cohérente avec celle du casque, version = celle du build, dernier vu qui avance |
|
| 25.10.3 |
Quitter l'app, attendre, recharger |
Le « dernier vu » se fige — c'est ce qui permet de voir qu'un casque est tombé |
|
| 25.10.4 |
Couper le wifi, laisser tourner |
L'app continue normalement, rien ne remonte au visiteur |
|
| 25.10.5 |
Vérifier en base après un battement |
LastSeen et AppVersion renseignés — ils n'avaient aucun chemin d'écriture avant |
|
| 25.10.6 |
⚠️ Forger un battement vers un casque d'une autre instance |
403 — une clé ne parle que de ses appareils |
|
| 25.10.7 |
⚠️ Appairer une vraie nouvelle tablette (pas un casque) |
Elle s'appaire. Ça répondait 403 depuis mars ; les tests ne le prouvent pas, car ils appellent le contrôleur sans passer par les filtres |
|
| 25.10.8 |
⚠️ Redémarrer une tablette déjà appairée |
Elle retrouve sa configuration. Deux causes corrigées ensemble le 12/09 : la route Device/{id}/detail refusait les clés, et l'app reconstruisait son client sans clé au démarrage |
|
| 25.10.9 |
Sur cette tablette, ouvrir une visite complète |
Rien d'autre n'est retombé : c'est le chemin qui traverse le plus de routes fermées le 12/09 |
|
| 25.10.10 |
Assigner une autre configuration au casque depuis l'onglet XR, sans y toucher |
Au bout de 3 min au plus, le casque revient au menu avec le nouveau contenu — sans redémarrage. C'est le « pousser vers le casque » de XR-5, obtenu par le battement plutôt que par MQTT |
|
| 25.10.11 |
Faire ça pendant qu'une galerie est ouverte |
Elle se ferme : le visiteur ne reste pas dans un contenu qui n'est plus le sien |
|
| 25.10.12 |
Éteindre un casque, revenir sur l'onglet XR une heure après |
Bandeau rouge « Muet depuis 1 h » sur sa carte. ⚠️ La pastille reste verte — c'est le dernier battement qui l'a écrite : c'est précisément ce mensonge que le bandeau corrige |
|
| 25.10.13 |
Un casque vu il y a 10 minutes |
Pas de bandeau : le seuil est à 1 h, soit vingt battements manqués |
|
| 25.10.14 |
Un casque muet depuis plus d'un jour |
« Muet depuis N j », pas « depuis 34 h » |
|
| 25.10.15 |
⚠️ mymuseum-visitapp : ouvrir un contenu dont la ressource n'a pas d'URL |
L'image se charge quand même. Repéré dans le code sans pouvoir le déclencher : apiService.dart:111 retombe sur GET /api/Resource/{id} en HTTP brut, sans clé — donc 401 depuis le 12/09. Le domaine y est en dur (api.mymuseum.be), donc ce repli ne marchait déjà plus ailleurs ; à supprimer si le cas n'existe pas |
|
25.11 — Ce qu'on mesure au passage
Trois chiffres à rapporter, ils n'existent nulle part aujourd'hui :
| # |
Mesure |
Pourquoi |
Valeur |
| 25.11.1 |
Temps entre le lancement et l'affichage des sections, avec et sans cache |
Décide si un écran de progression est un détail ou un sujet |
|
| 25.11.2 |
Taille du JSON d'export d'une vraie configuration (adb shell ls -l sur le cache) |
Dimensionne le cache disque, et dit si le stockage de l'add-on immersif tient |
|
| 25.11.3 |
Le casque garde-t-il le wifi en veille ? |
Conditionne le rafraîchissement en tâche de fond et la supervision d'XR-5 |
|
26. Médias immersifs dans le manager — ce que voit le gestionnaire (XR-4)
Ce chapitre ne teste ni le casque ni l'API : seulement qu'un conservateur qui n'a jamais entendu
parler de glTF comprend ce qu'il manipule. Les trois types immersifs existaient en base
depuis le 12/09 mais étaient invisibles côté Médiathèque — pas de facette, pas d'icône, pas
d'aperçu. Corrigé le même jour, jamais rejoué à l'écran.
Prérequis : une instance avec l'add-on immersif, et dans sa médiathèque au moins une image 360,
une vidéo 360 et un .glb.
26.1 — Médiathèque
| # |
Action |
Résultat attendu |
✓ |
| 26.1.1 |
Ouvrir la Médiathèque |
Le rail de facettes propose Image 360°, Vidéo 360° et Modèle 3D en fin de liste, avec leur compteur |
|
| 26.1.2 |
Cliquer la facette « Image 360° » |
Seules les 360 restent ; le chip de filtre actif affiche le libellé traduit, pas un code |
|
| 26.1.3 |
Regarder les vignettes des trois types |
Icônes photosphère / 360 / cube. Aucun point d'exclamation — c'était le cas par défaut, il se lisait comme une erreur de fichier |
|
| 26.1.4 |
Ouvrir une image 360 |
Aperçu visible (image équirectangulaire, donc déformée aux pôles — c'est normal) et non un cadre gris vide |
|
| 26.1.5 |
Ouvrir une vidéo 360 |
Le lecteur vidéo s'ouvre et la vidéo démarre |
|
| 26.1.6 |
Ouvrir un modèle 3D |
Phrase explicite : pas d'aperçu ici, le modèle se visualise dans une section Scène 3D et dans le casque. Pas de cadre vide sans explication |
|
| 26.1.7 |
Sur ces trois-là, lire le bloc « Informations » |
Type traduit, poids réel, date. Le poids d'une vidéo 360 est en Go — c'est l'information qui justifie le quota |
|
| 26.1.8 |
Téléverser un .glb |
Il arrive en Modèle 3D, et n'est pas compressé (une compression le détruirait) |
|
26.2 — Déclarer qu'une image est une 360
| # |
Action |
Résultat attendu |
✓ |
| 26.2.1 |
Dans un sélecteur de médias, regarder la pastille de type d'une image |
Un petit chevron ↔ accolé au libellé : c'est ce qui dit que la pastille est cliquable. Sans lui, le basculement n'existait qu'au survol |
|
| 26.2.2 |
Survoler la pastille |
Infobulle : bascule plat / 360°, l'image n'est pas compressée et part en immersif dans le casque |
|
| 26.2.3 |
Cliquer |
Le libellé passe à « Image 360° » et la pastille change de fond (teinte de marque) |
|
| 26.2.4 |
Enregistrer, recharger |
Le type a tenu, et la ressource n'a pas été recompressée |
|
| 26.2.5 |
Même geste sur une vidéo |
Bascule Vidéo ↔ Vidéo 360° |
|
| 26.2.6 |
Sur un PDF, un JSON, un audio |
Aucun chevron, pastille non cliquable — rien à basculer |
|
26.3 — Nommage de la section
| # |
Action |
Résultat attendu |
✓ |
| 26.3.1 |
Créer une section, lire la liste des types |
« Scène 3D » — plus « Maquette 3D », qui excluait l'épée ou la pièce de collection |
|
| 26.3.2 |
Lire sa description |
Elle annonce objet ou décor, les points d'intérêt et le casque VR |
|
| 26.3.3 |
Passer le manager en EN |
« 3D scene » pour la section, « 3D model » pour la ressource — deux libellés distincts. Ils étaient identiques, on ne savait pas lequel on choisissait |
|
| 26.3.4 |
Passer en NL |
« 3D-scène » / « 3D-model », même distinction |
|
| 26.3.5 |
Ouvrir la section, lire le sélecteur de mode |
Titre « Ce que le visiteur fait de la scène », puis Objet / Décor, chacun avec sa phrase d'exemple (une épée / une salle reconstituée) |
|
| 26.3.6 |
Basculer d'un mode à l'autre, enregistrer, recharger |
Le mode a tenu. Il ne change rien à l'écran du manager : il ne se voit que dans le casque |
|
| 26.3.7 |
Section sans modèle choisi |
« Choisissez d'abord un modèle 3D dans la médiathèque », et pas une zone morte |
|
27. Hors ligne complet et fond immersif (XR-4, E4 et E5)
Écrit le 2026-09-12, jamais exécuté. Deux chantiers indépendants, regroupés parce qu'ils se
testent au même moment — le second n'a de sens que si le premier a rempli le cache.
27.1 — Préchargement des médias
Le cache existait mais ne se remplissait qu'à la demande : une borne branchée sans réseau
avait ses titres et pas une image. C'est ce que ce bloc vérifie, et c'est l'argument qui a fait
choisir Unity contre WebXR.
| # |
Action |
Résultat attendu |
✓ |
| 27.1.1 |
Casque neuf (forgetPairing), appairer, laisser l'app cinq minutes sur le menu sans rien toucher |
Dans adb logcat : [Preload] N médias à mettre en cache, puis [Preload] Terminé — N/N disponibles hors ligne, avec le poids du cache en Mo |
|
| 27.1.2 |
Relancer l'app sans rien effacer |
N déjà en cache, 0 téléchargement : le préchargement ne retélécharge jamais ce qu'il a |
|
| 27.1.3 |
Couper le wifi du casque, relancer |
Le menu s'affiche, et chaque section s'ouvre avec ses images — c'est le seul test qui compte |
|
| 27.1.4 |
Toujours hors ligne : ouvrir une 360, une vidéo 360, une scène 3D |
Les trois se chargent depuis le disque |
|
| 27.1.5 |
Toujours hors ligne : viser un point d'intérêt qui porte un audio |
Le son sort. Il était streamé depuis l'URL : le hotspot était muet dès que le réseau tombait, alors que le fichier pouvait être sur le casque |
|
| 27.1.6 |
Une configuration contenant une section Video YouTube |
Elle n'est pas téléchargée (lien externe), et le journal ne compte pas d'échec pour elle |
|
| 27.1.7 |
Une configuration avec un PDF et un JSON |
Pas téléchargés non plus : le casque ne les affiche pas, les prendre ne ferait que remplir le disque |
|
| 27.1.8 |
Couper le wifi pendant le préchargement, le remettre, relancer l'app |
Les médias déjà pris sont gardés, les autres repartent. Aucun message au visiteur |
|
| 27.1.9 |
⚠️ Mesurer : durée du préchargement complet et poids final du cache, sur une vraie configuration |
C'est le chiffre qui dira si une borne peut être mise en service le matin même ou la veille |
|
| 27.1.10 |
Vérifier l'espace disque du casque après plusieurs configurations |
Rien ne purge aujourd'hui. Si ça monte, c'est un sujet — pas un bug |
|
27.2 — Fond immersif, côté manager
| # |
Action |
Résultat attendu |
✓ |
| 27.2.1 |
Instance sans l'add-on immersif → ouvrir une configuration |
Aucune carte « Fond immersif » : pas de réglage qu'on ne peut ni remplir ni voir |
|
| 27.2.2 |
Activer l'add-on (écran SuperAdmin), rouvrir |
La carte apparaît |
|
| 27.2.3 |
Choisir une image 360 comme fond |
Sous le champ : « Panorama 360° ». Le type n'est jamais demandé — il est déduit du média, donc impossible à renseigner faux |
|
| 27.2.4 |
Choisir une vidéo 360 à la place |
Le libellé devient « Vidéo 360° », avec l'avertissement sur le décodeur d'une borne |
|
| 27.2.5 |
Choisir un modèle 3D |
« Décor 3D : prévu par le contrat, pas encore affiché en fond » — annoncé avant, pas découvert dans le casque |
|
| 27.2.6 |
Ne pas renseigner l'image de repli, enregistrer |
Autorisé, mais la phrase d'aide dit ce que ça coûte : web, mobile et tablettes n'afficheront aucun fond |
|
| 27.2.7 |
Renseigner le repli, enregistrer, recharger |
Les deux ont tenu |
|
| 27.2.8 |
« Retirer le fond », enregistrer, recharger |
Le fond est parti — et pas un fond vide avec un type resté collé |
|
| 27.2.9 |
Onglet XR → sous-onglet Configuration, en bas |
La même carte, pour le fond du menu d'accueil cette fois, enregistré immédiatement |
|
| 27.2.10 |
Le même écran pour le mobile, le web, le kiosk |
Aucune carte de fond : ces canaux n'ont pas de menu à décorer |
|
| 27.2.11 |
Passer en EN et NL |
Les dix libellés traduits |
|
27.3 — Fond immersif, côté casque
| # |
Action |
Résultat attendu |
✓ |
| 27.3.1 |
Configuration sans fond, lancer l'app |
Menu sur fond noir, comme avant. Aucun message, aucune erreur |
|
| 27.3.2 |
Configuration avec un panorama, relancer |
Le menu flotte dans le lieu, pas dans le noir. C'est tout l'objet du chantier |
|
| 27.3.3 |
Ouvrir une section 360 depuis ce menu |
La 360 remplace le fond |
|
| 27.3.4 |
La fermer |
Le fond revient. SkyboxView restaure le ciel précédent, et le ciel précédent, c'est le fond |
|
| 27.3.5 |
Fond en vidéo 360 |
Elle tourne en boucle et sans son — un fond qui parle par-dessus un commentaire serait intenable |
|
| 27.3.6 |
⚠️ Laisser une borne à fond vidéo tourner une heure, relever la batterie |
Le décodeur tourne en permanence. À mesurer avant de vendre ce cas comme normal |
|
| 27.3.7 |
Assigner au casque une configuration avec un autre fond, attendre le battement |
Le nouveau fond remplace l'ancien, sans redémarrage et sans que les deux vidéos tournent |
|
| 27.3.8 |
Fond de type Scène 3D |
Ciel par défaut + une ligne dans le journal. Pas d'écran noir muet |
|
| 27.3.9 |
⚠️ Fond dont la ressource a été supprimée de la médiathèque |
Le menu reste utilisable sur fond noir. Jamais un plantage au démarrage |
|
| 27.3.10 |
Reposer le casque, le reprendre (nouvelle visite) |
Le fond est toujours là : il appartient à la visite, pas à la session |
|
Hors scope (non implémenté)
| Feature |
Statut |
| 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 |
❌ |