Cartes 600, 610 et 620 closes (back-office XR, ressource 360, appairage tablette), 250 et 320 retirées du planifié, 015 et 330 à jour ; STATUS, roadmap, test-plan, todo-features et plans VR / frontière immersif alignés ; kanban.html regénéré. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011VxSQeGQYUvPmSEoGdnidA
1435 lines
105 KiB
Markdown
1435 lines
105 KiB
Markdown
# 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
|
||
|
||
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**
|
||
23. [QR codes, nom d'application, page de téléchargement, proximité](#23-qr-codes-nom-dapplication-page-de-téléchargement-proximité)
|
||
24. [Onglet XR — flotte de casques (lot XR-2)](#24-onglet-xr--flotte-de-casques-lot-xr-2)
|
||
25. [App VR — appairage et lecture du contenu (XR-4, items E2-E3)](#25-app-vr--appairage-et-lecture-du-contenu-xr-4-items-e2-e3) ⚠️ **jamais exécuté**
|
||
26. [Médias immersifs dans le manager — ce que voit le gestionnaire (XR-4)](#26-médias-immersifs-dans-le-manager--ce-que-voit-le-gestionnaire-xr-4)
|
||
27. [Hors ligne complet et fond immersif (XR-4, E4 et E5)](#27-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](v2/stats-screen-plan.md) § État d'implémentation.
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 8.7 | Ouvrir l'écran sur une instance **à un seul canal** | Pas de filtre « Canal », pas de carte « Canaux », 4ᵉ KPI = « Contenus consultés » | |
|
||
| 8.8 | Ouvrir l'écran sur une instance **multi-canal** | Une chip par canal réellement présent, chacune avec son volume ; aucune chip pour un canal que l'instance n'a pas | |
|
||
| 8.9 | Changer de période (7 / 30 / 90 / Année) | Les périodes au-delà de `StatsHistoryDays` sont grisées ; le sous-titre de date suit la sélection | |
|
||
| 8.10 | Regarder les variations des KPI sur un plan à historique court (30 j) en période 30 j | **Aucune variation affichée** — la période précédente sortirait de l'historique et serait tronquée par le backend | |
|
||
| 8.11 | Contenu au titre long (« La vie quotidienne des soldats ») | Titre complet sur 2 lignes max, jamais tronqué à 8 caractères | |
|
||
| 8.12 | Survoler la courbe | Trait vertical + date et nombre de visites ; le pic est cerclé et nommé sous le graphe | |
|
||
| 8.13 | Période contenant des jours sans aucun event | La courbe descend à zéro ces jours-là au lieu de les sauter ; les bandes de week-end tombent bien sur samedi/dimanche | |
|
||
| 8.14 | Instance **sans** `HasAdvancedStats` | Bloc « plan Premium » à la place des tableaux détaillés, carte « Langues » absente (le backend vide la distribution) | |
|
||
| 8.15 | Basculer la langue du manager en NL puis EN | Toutes les chaînes suivent, y compris les deux phrases du bandeau « à retenir » | |
|
||
|
||
### 8ter. Export PDF à la demande (livré le 2026-08-07)
|
||
|
||
`flutter test test/statistics_report_test.dart` couvre la génération du document (4 cas). Ce qui suit ne se vérifie que dans un vrai navigateur.
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 8.16 | Cliquer sur « Télécharger le PDF » dans le bloc rapport | Le fichier se télécharge, nommé `<instance>-frequentation-<date>.pdf` | |
|
||
| 8.17 | Ouvrir le PDF | Page de garde au nom de l'instance, filet à sa couleur principale, période lisible ; les chiffres sont **identiques** à ceux de l'écran | |
|
||
| 8.18 | Vérifier les accents et les guillemets (« Fréquentation », « Connais-tu… ») | Rendus correctement — la police OpenSans est embarquée dans le document | |
|
||
| 8.19 | Instance dont l'`ApplicationInstance` mobile porte un logo | Logo en page de garde. **S'il manque**, c'est le CORS du bucket Firebase : le rapport doit quand même se générer, sans logo | |
|
||
| 8.20 | Instance **sans** `HasAdvancedStats` | Pas de tableaux POI/quiz/jeux/QR dans le PDF, et la 4ᵉ puce du bloc rapport n'est pas affichée | |
|
||
| 8.21 | Période sans aucune visite | Le bouton n'est pas atteignable (l'écran affiche l'état vide) — rien à générer | |
|
||
|
||
### 8quater. Historique unifié à 13 mois (décidé et appliqué le 2026-08-09)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 8.22 | `dotnet ef database update` puis `SELECT "Id","StatsHistoryDays" FROM "SubscriptionPlans"` | 395 sur `plan-essentiel`, `plan-standard`, `plan-premium` | |
|
||
| 8.23 | `SELECT "Name","StatsHistoryDays" FROM "Instances" WHERE "HasStats"` | 395 partout — **c'est cette valeur que lit le contrôleur**, pas celle du plan. Si elle est restée à 30, la reprise SQL de la migration n'a pas tourné | |
|
||
| 8.24 | Ouvrir l'écran sur un plan Essentiel | Les 4 périodes sont sélectionnables, **« Année » comprise** — elle était grisée avant | |
|
||
| 8.25 | Sélectionner « Année » | La courbe s'affiche, mais **aucune variation sur les KPI** : comparer 365 jours exigerait 730 jours d'historique. C'est voulu — mieux vaut rien qu'un chiffre faux | |
|
||
| 8.26 | Hangfire `/hangfire` → job `visit-events-purge` | Job listé, cron 3 h. **Le déclencher à la main** : il doit logger « purge ignorée : Stats:RetentionDays non défini » et **ne rien supprimer** | |
|
||
| 8.27 | ⛔ N'activer `Stats__RetentionDays=395` **qu'après** avoir mis en place le `pg_dump` quotidien | Suppression définitive — le snapshot OVH ne permet pas de restaurer une table seule | |
|
||
|
||
---
|
||
|
||
## 9. Beacons / géofencing
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 9.1 | Visitapp en range d'un iBeacon connu | App au premier plan : popup de suggestion, **sans** notification. App en arrière-plan / écran verrouillé : notification locale `beaconFound` (cf. §23) | |
|
||
| 9.2 | Notification beacon | Titre/corps dans la langue active (clé `beaconFound` / `beaconFoundBody`) | |
|
||
| 9.3 | Entrée dans une zone geo d'un `GuidedStep` | Notification locale `geoZone` affichée | |
|
||
| 9.4 | Filtre par `minorId` + `accuracy` + cooldown | Pas de spam de notifications pour le même beacon | |
|
||
| 9.5 | `beacon_scanner: ^0.0.4` | Scanner opérationnel sur Android et iOS | |
|
||
| 9.6 | **iOS** — balise émettant un `proximityUUID` **autre** que `FDA50693-A4E2-4FB1-AFCF-C6EB07647825` | Le cas doit **échouer** : la région iOS est filtrée sur cet UUID en dur (`geo_beacon_trigger_service.dart:148-155`). Si ça déclenche quand même, le code ne fait pas ce qu'il dit — Android, lui, scanne sans filtre | |
|
||
| 9.7 | Déclenchement **proactif de l'assistant** par balise (chemin distinct de 9.1) | Le prompt part depuis `geo_beacon_trigger_service._onBeaconResult`, uniquement si le mode proactif est actif | |
|
||
| 9.8 | Distance réelle de déclenchement, mètre en main | ⚠️ `proximity_suggestion_service.dart` = `beaconMaxDistanceMeters = 100` (accuracy du ranging). `Section.meterZoneGPS` sert désormais aux zones **GPS**, pas aux beacons. Noter la distance **mesurée**, pas celle attendue | |
|
||
| 9.9 | Stationner 2 min devant un POI | Une seule notification (cooldown), pas de répétition | |
|
||
| 9.10 | Balise avec `major` porteur d'un niveau de batterie | Aucun effet sur l'identification du POI — l'app ne lit que le `minorId`. C'est ce qui rend le `major` utilisable pour la télémétrie | |
|
||
|
||
---
|
||
|
||
## 10. AI Assistant
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 10.1 | Ouvrir l'assistant depuis `home_3.0.dart` | `AssistantChatSheet` s'ouvre | |
|
||
| 10.2 | Ouvrir depuis `configuration_page.dart` | Idem | |
|
||
| 10.3 | Envoyer un message | Réponse de l'assistant affichée en bulle | |
|
||
| 10.4 | Réponse avec cards | Cards affichées sous la réponse texte | |
|
||
| 10.5 | Tap sur une card de navigation | Navigue vers la bonne section | |
|
||
| 10.6 | Instance avec `isAssistant = false` | Bouton assistant absent | |
|
||
|
||
---
|
||
|
||
## 11. Quotas & Plans d'abonnement
|
||
|
||
### 11.1 CRUD SubscriptionPlan (SuperAdmin)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.1.1 | `POST /api/SubscriptionPlan` → `{ "name": "Standard", "storageQuotaBytes": 5368709120, "aiTokensPerMonth": 5000000 }` | 200 + `id` généré | |
|
||
| 11.1.2 | `POST /api/SubscriptionPlan` → `{ "name": "Premium", "storageQuotaBytes": 21474836480, "aiTokensPerMonth": 20000000 }` | 200 | |
|
||
| 11.1.3 | `GET /api/SubscriptionPlan` | 200 + liste des plans | |
|
||
| 11.1.4 | `PUT /api/SubscriptionPlan` — modifier `aiTokensPerMonth` | 200 + valeur mise à jour | |
|
||
| 11.1.5 | `DELETE` d'un plan **non assigné** | 202 | |
|
||
| 11.1.6 | `DELETE` d'un plan **assigné** | 409 Conflict | |
|
||
| 11.1.7 | Appel avec token non-SuperAdmin | 401 / 403 | |
|
||
|
||
> `5368709120` = 5 GB · `21474836480` = 20 GB
|
||
|
||
### 11.2 Assignation d'un plan
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.2.1 | `PUT /api/Instance` avec `subscriptionPlanId: "{id}"` | Plan assigné | |
|
||
| 11.2.2 | `GET /api/Instance/{id}` | Champ `subscriptionPlan` présent dans la réponse | |
|
||
| 11.2.3 | `PUT /api/Instance` avec `subscriptionPlanId: ""` | Plan effacé (null en base) | |
|
||
| 11.2.4 | `PUT /api/Instance` sans le champ `subscriptionPlanId` | Plan existant inchangé | |
|
||
|
||
### 11.3 Endpoint quota
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.3.1 | `GET /api/Instance/{id}/quota` — instance sans plan | `storageQuotaBytes: 0`, `aiTokensPerMonth: 0` | |
|
||
| 11.3.2 | `GET /api/Instance/{id}/quota` — instance avec plan Standard | `storageQuotaBytes: 5368709120`, `aiTokensPerMonth: 5000000` | |
|
||
| 11.3.3 | Appel avec token Viewer | 200 (l'endpoint accepte Viewer) | |
|
||
|
||
### 11.4 Tracking stockage
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.4.1 | Uploader une image | `Resource.SizeBytes` = taille réelle du fichier | |
|
||
| 11.4.2 | Uploader un PDF | Idem | |
|
||
| 11.4.3 | `GET /quota` après uploads | `storageUsedBytes` = somme des `SizeBytes` | |
|
||
| 11.4.4 | Supprimer une ressource + rappeler `/quota` | `storageUsedBytes` diminue | |
|
||
| 11.4.5 | Ajouter une `ImageUrl` (externe) | `SizeBytes = 0` — pas comptée | |
|
||
|
||
### 11.5 Tracking quota IA (en tokens)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.5.1 | Envoyer un message IA (`/api/AI/chat`) | `AiTokensThisMonth` en base = `Usage.TotalTokenCount` retourné par Gemini pour cet appel (visible dans `AiChatResponse.TokensUsed`) | |
|
||
| 11.5.2 | Envoyer un 2e message | Compteur = somme des `TokensUsed` des deux appels (pas juste `+1`) | |
|
||
| 11.5.3 | `GET /quota` | `aiTokensUsed` = valeur cumulée en base | |
|
||
| 11.5.4 | Modifier `AiUsageMonthKey` → `"2025-01"` en base, envoyer un message | Compteur repart au `TokensUsed` du nouvel appel (pas cumulé avec l'ancien mois), `AiUsageMonthKey` = mois courant | |
|
||
| 11.5.5 | Instance avec `AiTokensThisMonth` déjà ≥ `AiTokensPerMonth` (quota atteint) | Nouvel appel → `429 "Quota IA mensuel dépassé"`, `AssistantService` **pas appelé** (pas d'incrément supplémentaire) | |
|
||
| 11.5.6 | Appel `/api/AI/translate` sur un texte HTML long (plusieurs paragraphes, plusieurs langues cibles) vs un texte court | `TokensUsed` nettement plus élevé pour le texte long — vérifie que le comptage reflète bien le coût réel et pas juste "1 appel = 1 unité" | |
|
||
|
||
### 11.6 Widget sidebar (Flutter)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.6.1 | Se connecter à manager-app | Widget quota visible **au-dessus** de l'email dans la sidebar | |
|
||
| 11.6.2 | Instance sans plan | "Illimité" affiché | |
|
||
| 11.6.3 | Instance avec plan | "X MB / 5 GB" · tokens IA formatés en K/M, ex. "1.2M / 5M tokens" | |
|
||
| 11.6.4 | Stockage < 80% | Barre bleue | |
|
||
| 11.6.5 | Stockage ~85% (modifier `SizeBytes` en base) | Barre **orange** | |
|
||
| 11.6.6 | Stockage ~97% | Barre **rouge** | |
|
||
| 11.6.7 | `isAssistant = false` | Bloc IA absent | |
|
||
| 11.6.8 | `isAssistant = true` | Les deux barres visibles | |
|
||
|
||
### 11.7 Dialog config plan (SuperAdmin)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 11.7.1 | Cliquer sur le bouton swap | Dialog avec ✏️ sur chaque instance | |
|
||
| 11.7.2 | Cliquer sur ✏️ | Dialog "Plan — NomInstance" avec radio buttons | |
|
||
| 11.7.3 | Sélectionner un plan + "Enregistrer" | Plan assigné via `PUT /api/Instance` | |
|
||
| 11.7.4 | Sélectionner "Aucun plan" + "Enregistrer" | Plan effacé | |
|
||
| 11.7.5 | Connexion en non-SuperAdmin | Bouton swap invisible | |
|
||
|
||
---
|
||
|
||
## 12. API Keys — date d'expiration
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 12.1 | Créer une API key dans manager-app | Date picker d'expiration disponible | |
|
||
| 12.2 | Créer une key avec date d'expiration | Date sauvegardée, colonne "Expiration" affichée dans le tableau | |
|
||
| 12.3 | Key expirée | Colonne "Expiration" rouge dans le tableau | |
|
||
| 12.4 | Appel API avec une key expirée | 401 / rejeté | |
|
||
| 12.5 | Révoquer une key (`IsActive = false`) | Appels suivants avec cette key refusés | |
|
||
|
||
---
|
||
|
||
## 13. Manager-app — GuidedStep champs avancés
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 13.1 | Ouvrir le formulaire d'un GuidedStep | Toggles `isHiddenInitially`, `isStepLocked`, `isStepTimer` visibles | |
|
||
| 13.2 | Activer `isStepTimer` | Champs `timerSeconds` et `timerExpiredMessage` (multilingue) apparaissent | |
|
||
| 13.3 | Sauvegarder avec timer | Valeurs persistées en base | |
|
||
| 13.4 | Géodéclenchement | Zone posée via le `GeometryInputContainer` (mini-carte) + toggle `isGeoTriggered` + `zoneRadiusMeters`. ⚠️ **révisé 2026-08-05** : il n'y a **pas** de champ `triggerGeoPointId`, l'ancienne ligne de test portait sur un champ inexistant | |
|
||
| 13.5 | `isHiddenInitially` activé puis sauvegardé | Valeur persistée — mais ⚠️ **aucun effet visiteur** (écart M4, voir §20) | |
|
||
|
||
---
|
||
|
||
## 14. Manager-app — SectionEvent annotations par bloc
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 14.1 | Ouvrir le formulaire d'un `ProgrammeBlock` | Section "Annotations" présente | |
|
||
| 14.2 | Ajouter une annotation | Formulaire `showNewOrUpdateMapAnnotation` (label, géométrie, couleur, icône) | |
|
||
| 14.3 | Modifier une annotation existante | Mise à jour sauvegardée | |
|
||
| 14.4 | Supprimer une annotation | Supprimée de la liste | |
|
||
|
||
---
|
||
|
||
## 15. Backend — Bug GuidedPath SectionGameId
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 15.1 | Créer un `GuidedPath` avec un `SectionGameId` | `SectionGameId` sauvegardé | |
|
||
| 15.2 | `PUT` pour mettre à jour le `GuidedPath` | `SectionGameId` **conservé** après update (pas remis à null) | |
|
||
|
||
---
|
||
|
||
## 16. Non-régression
|
||
|
||
| # | Vérification | Résultat attendu | ✓ |
|
||
|---|-------------|-----------------|---|
|
||
| 16.1 | Navigation dans toutes les sections de manager-app | Aucune régression | |
|
||
| 16.2 | Upload de ressources | Fonctionne, `SizeBytes` renseigné | |
|
||
| 16.3 | `GET /api/Instance/{id}` existant | Retourne les anciens champs + les nouveaux | |
|
||
| 16.4 | Authentification JWT manager-app | Login/logout fonctionnels | |
|
||
| 16.5 | tablet-app — sélection par pincode | Inchangé | |
|
||
| 16.6 | mymuseum-visitapp — boot et chargement config | Inchangé | |
|
||
| 16.7 | Swagger / OpenAPI | Tous les nouveaux endpoints documentés | |
|
||
|
||
---
|
||
|
||
## 17. SectionVideo — multi-sources
|
||
|
||
### 17.1 Configuration (manager-app)
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 17.1.1 | Ouvrir la config d'une SectionVideo existante avec une URL YouTube | Sélecteur positionné sur "YouTube", URL pré-remplie | |
|
||
| 17.1.2 | Ouvrir une SectionVideo avec une URL Vimeo | Sélecteur positionné sur "Vimeo" | |
|
||
| 17.1.3 | Ouvrir une SectionVideo avec une URL Firebase Storage | Sélecteur positionné sur "Ressource" | |
|
||
| 17.1.4 | Changer de mode | `source_` remis à null, champ vidé | |
|
||
| 17.1.5 | Mode Ressource → sélectionner une vidéo | Modal filtré sur `Video` et `VideoUrl` uniquement, label de la ressource affiché après sélection | |
|
||
| 17.1.6 | Sauvegarder dans chaque mode | `source_` contient l'URL correcte en base | |
|
||
|
||
### 17.2 Lecture tablet-app
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 17.2.1 | SectionVideo avec URL YouTube | Player YouTube affiché, lecture sans erreur 152 | |
|
||
| 17.2.2 | SectionVideo avec URL Vimeo | Player Vimeo affiché via WebView | |
|
||
| 17.2.3 | SectionVideo avec URL Firebase Storage (mp4) | `VideoViewer` affiché, lecture directe | |
|
||
| 17.2.4 | `source_` vide | Message "La vidéo ne peut pas être affichée, l'url est incorrecte" | |
|
||
| 17.2.5 | URL Vimeo malformée (sans ID numérique) | Message "Impossible d'extraire l'identifiant Vimeo depuis l'URL" | |
|
||
|
||
### 17.3 Lecture mymuseum-visitapp
|
||
|
||
| # | Action | Résultat attendu | ✓ |
|
||
|---|--------|-----------------|---|
|
||
| 17.3.1 | SectionVideo avec URL YouTube | Player YouTube affiché, lecture sans erreur 152 | |
|
||
| 17.3.2 | SectionVideo avec URL Vimeo | Player Vimeo affiché via WebView | |
|
||
| 17.3.3 | SectionVideo avec URL Firebase Storage (mp4) | `VideoViewer` affiché, lecture directe | |
|
||
| 17.3.4 | Bouton retour en mode normal | Retour fonctionnel | |
|
||
| 17.3.5 | Bouton retour en mode fullscreen YouTube | `exitFullScreen()` appelé puis retour | |
|
||
|
||
---
|
||
|
||
## 18. Onboarding self-service (plan Essentiel)
|
||
|
||
Implémenté le 2026-08-05, **jamais exécuté**. La checklist détaillée (prérequis Stripe CLI,
|
||
inscription, conversion payante, watermark, auth par token, job Hangfire, quota IA) vit dans
|
||
`todo-features.md` → section **« 🧪 Checklist de test end-to-end »** du chapitre
|
||
*Onboarding & Acquisition*, pour rester à côté des specs correspondantes.
|
||
|
||
Points de vigilance à ne pas oublier au moment de tester :
|
||
- Le `whsec_` de `stripe listen` est **différent** de celui du dashboard → à mettre dans
|
||
`appsettings.Development.json`, sinon la vérification de signature échoue en local.
|
||
- `IsAssistant` vaut `false` par défaut sur une instance neuve → à passer à `true` en base pour
|
||
pouvoir tester quoi que ce soit sur le quota IA.
|
||
- Ne pas tester la désactivation d'essai sur une instance réelle : `IsActive = false` coupe
|
||
immédiatement l'accès visiteur via la `PublicApiKey`.
|
||
|
||
---
|
||
|
||
## 19. Parité manager-app → apps visiteur (par type de section)
|
||
|
||
> **But de ce chapitre** : répondre à « tout ce que je peux configurer est-il visible côté visiteur ? ».
|
||
> Un bloc par type de section, à jouer indépendamment des autres. Analyse de référence :
|
||
> [parity-manager-visitapp.md](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. **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.<br>**À 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
|
||
|
||
```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** | |
|
||
|
||
---
|
||
|
||
## 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 | |
|
||
|
||
### 24.1 — Le menu
|
||
|
||
| # | 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 | ❌ |
|