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