# Parité manager-app → apps visiteur > **Question à laquelle ce document répond** : tout ce qu'un client peut configurer dans `manager-app` est-il réellement visible dans `mymuseum-visitapp` (mobile) et `visitapp-web` ? > > Audit du **2026-08-05**, fait sur le code réel (pas sur la doc). Méthode : extraction des champs de tous les `ToDTO()` du backend (`manager-service/ManagerService/Data/` + `Data/SubSection/`), puis vérification de chaque champ dans les 3 bases de code, puis lecture manuelle de chaque écart pour éliminer les faux positifs (renommages `snake_case` → `camelCase`, champs chargés via un endpoint dédié, champs dérivés type `imageResourceId` → `imageUrl`). > > `tablet-app` est hors périmètre de cet audit (kiosk, cycle de release séparé) mais apparaît en colonne indicative quand l'information est utile. --- ## 1. Couverture par type de section Les **13 types de section** du backend (`DTOs/SectionType.cs`) sont dispatchés dans les deux apps visiteur — aucun type n'ouvre un écran vide. | SectionType | Config manager-app | mymuseum-visitapp | visitapp-web | |---|---|---|---| | Map | `Map/map_config.dart` | `Screens/Sections/Map/` | `MapSection.tsx` | | Slider | `Slider/` | `Sections/Slider/` | `SliderSection.tsx` | | Video | `Video/` | `Sections/Video/` | `VideoSection.tsx` | | Web | `Web/` | `Sections/Web/` | `WebSection.tsx` | | Menu | `Menu/` | `Sections/Menu/` | `MenuSection.tsx` | | Quiz | `Quizz/quizz_config.dart` | `Sections/Quiz/` | `QuizSection.tsx` | | Article | `Article/` | `Sections/Article/` | `ArticleSection.tsx` | | PDF | `Pdf/` | `Sections/PDF/` | `PdfSection.tsx` | | Game | `Game/game_config.dart` | `Sections/Game/game_page.dart` | `GameSection.tsx` (+ `game/PuzzleGame`, `game/SlidingPuzzle`) | | Agenda | `Agenda/agenda_config.dart` | `Sections/Agenda/` | `AgendaSection.tsx` | | Weather | `Weather/` | `Sections/Weather/` | `WeatherSection.tsx` | | Event | `Event/event_config.dart` | `Sections/Event/` | `EventSection.tsx` (+ `event/EventMap`) | | Parcours | `SectionParcours/section_parcours_config.dart` | `Sections/Parcours/parcours_page.dart` | `ParcoursSection.tsx` | **Conclusion niveau 1** : la couverture *par type de section* est complète dans les deux apps. Les écarts sont tous au niveau *champ* ou *sous-comportement*, décrits ci-dessous. --- ## 2. Écarts confirmés — mymuseum-visitapp (mobile) Champ configurable dans manager-app, sans effet dans l'app mobile. | # | Champ / comportement | Configuré dans | Réalité mobile | Web | Gravité | |---|---|---|---|---|---| | ~~**M1**~~ ✅ | `SectionMap.centerLatitude` / `centerLongitude` ("Centrer sur") | `map_config.dart:612-622` (picker sur mini-carte) | **Ignoré.** `flutter_map_view.dart:105`, `map_box_view.dart:202`, `google_map_view.dart:127` centrent sur `mapDTO.latitude/longitude` — c'est-à-dire le point GPS *de la section* (champ beacon/geofence), pas le centre choisi | ✅ `MapSection.tsx:62-63` | **Haute** — le client règle le cadrage de sa carte et rien ne bouge sur mobile | | ~~**M2**~~ ✅ | `GeoPoint.schedules` (horaires d'un point d'intérêt) | `showNewOrUpdateGeoPoint.dart` | **Jamais affiché.** `marker_view.dart` affiche `title`, `description`, `contents`, `imageUrl`, `prices`, `phone`, `email`, `site` — pas `schedules` | ✅ `MapSection.tsx:419` (`InfoBlock` "Horaires") | **Haute** — donnée saisie et invisible | | **M3** | `Section.meterZoneGPS` (rayon de déclenchement) | `section_detail_screen.dart:650` | **Ignoré.** `configuration_page.dart:43` utilise une constante en dur : `int meterToBeacon = 100;` | n/a (pas de BLE en web) | **Moyenne** — le rayon configuré par section n'a aucun effet | | ~~**M4**~~ ✅ | `GuidedStep.isHiddenInitially` | `showNewOrUpdateGuidedStep.dart:203` (toggle) | **Aucun consommateur**, dans aucune des deux apps | ❌ idem | **Moyenne** — toggle mensonger dans l'UI client | | **M5** | `ApplicationInstance.mainImageUrl` | manager-app | **Non utilisé** : `home_3.0.dart:117` affiche `config.imageSource` | ✅ web utilise `mainImageUrl` | **Moyenne** — l'image d'accueil diffère entre les deux canaux pour la même config | | ~~**M6**~~ | ~~Watermark "Preview" pendant l'essai gratuit~~ | backend onboarding | **Non applicable** | ✅ `app/[slug]/layout.tsx:29` → `TrialWatermark` | ❌ **Faux positif — écarté le 2026-08-07** : l'essai gratuit ne concerne que le plan **Essentiel**, qui est *web + kiosk sans app native*. Un essai mobile ne peut pas exister, donc rien à afficher | | ~~**M7**~~ ✅ | Mode contenu d'un parcours (`ShowMap = false`) : `isStepLocked` et `hideNextStepsUntilComplete` | `showNewOrUpdateGuidedStep.dart` / `showNewOrUpdateGuidedPath.dart` | **Non implémentés dans `guided_path_content_progression_page.dart`** : pas de verrouillage d'étape, pas de masquage des étapes futures. `guided_path_map_progression_page.dart` les gère (l.139, 409, 257). ✅ En revanche `isLinear` (l.119), `requireSuccessToAdvance` (l.78) et le timer (l.73) **sont** bien gérés en mode contenu | ❌ même trou côté web, plus large (voir W6/W7) | **Haute** — dans un escape indoor, le visiteur atteint les énigmes non résolues | | ~~**M8**~~ ✅ | `isLinear = false` (navigation libre : « le visiteur choisit ses étapes dans l'ordre qu'il veut ») | `showNewOrUpdateGuidedPath.dart` | **N'existe dans aucune app** : `isLinear=false` ne fait qu'ajouter un bouton Précédent. Aucun moyen de sauter à l'étape de son choix — les pins de carte sélectionnent une étape sans y naviguer | ❌ idem | **Moyenne** — l'option est proposée mais ne produit pas le comportement attendu | --- ## 3. Écarts confirmés — visitapp-web | # | Champ / comportement | Réalité web | Mobile | Gravité | |---|---|---|---|---| | **W1** | `SectionMap.mapProvider` / `mapType` / `mapTypeMapbox` | **Ignorés** — `map/LeafletMap.tsx` a un `TileLayer` OSM en dur | ✅ 3 vues séparées (Google / Mapbox / flutter_map) | **Moyenne** — un client qui choisit "Google Hybrid" voit de l'OSM sur le web | | ~~**W2**~~ ✅ | `Categorie.icon` / `Categorie.resourceDTO` (icône de catégorie) | **Ignorés** — les marqueurs sont des pastilles de couleur (`colorOf(p.categorieId)`, `MapSection.tsx:304`) | ✅ icône affichée | **Moyenne** — perte d'identité visuelle de la carte | | **W3** | `AppConfigurationLink.roundedValue` (rayon des coins, theming instance) | **Aucun usage** | ✅ ~47 usages (popups agenda, audio player, cards…) | **Basse** — cosmétique, mais c'est du theming vendu | | **W4** | `AppConfigurationLink.isSectionImageBackground` | **Aucun usage** | ✅ `section_page.dart:133` (MenuPage) | **Basse** | | **W5** | `SectionArticle.isContentTop` | **Ignoré** — `ArticleSection.tsx` place toujours le carrousel avant le HTML | ✅ | **Basse** | | ~~**W6**~~ ✅ | `GuidedPath.isLinear` et `GuidedStep.isStepLocked` | **Zéro occurrence** dans `ParcoursSection.tsx` (2600+ lignes) — `prevStep` (l.86) autorise toujours le retour arrière, même quand l'ordre devrait être imposé | ✅ `isLinear` géré dans les deux modes ; `isStepLocked` en mode carte | **Moyenne** | | ~~**W7**~~ ✅ | Mode contenu d'un parcours : `hideNextStepsUntilComplete` | Géré **uniquement** dans `ParcoursMapProgression` (l.332). `ProgressView` (mode sans carte, l.1275) ne masque rien | ❌ idem (M7) | **Haute** | | ~~**W8**~~ ✅ | Assistant IA (`Instance.isAssistant` / `ApplicationInstance.isAssistant`) | **Implémenté le 2026-08-06** — `components/assistant/AssistantBubble.tsx`, bulle flottante montée par le layout de configuration quand les deux drapeaux sont vrais. Consomme `POST /api/AI/chat` en `AppType.Web` | ✅ `AssistantChatSheet` | **Haute** — débloque l'add-on IA sur le plan Essentiel, qui est *web-only* | | **W9** | `SectionAgenda.agendaMapProvider` | **Ignoré** — `agenda/EventMiniMap.tsx` en Leaflet fixe | ✅ | **Basse** | | **W10** | `SectionMap.iconResourceId` / `SectionEvent.iconResourceId` | **Ignorés** | ⚠️ 2 usages mobile seulement | **Basse** — vérifier d'abord si ce champ a encore un rôle (cf. §4) | --- > ✅ **Corrigé le 2026-08-06 (M4, M7, M8, W6, W7)** — refonte de la progression des parcours : > - `IsStepLocked`, `IsHiddenInitially` et `FactContent` **supprimés** du modèle (migration > `RemoveDeadGuidedStepFlags`). `IsStepLocked` était non seulement redondant avec > `RequireSuccessToAdvance`, mais rendait l'étape *définitivement* infranchissable même après > réussite du défi (`_canAdvance` renvoyait `false`). > - Le verrouillage d'une étape est désormais **dérivé de la progression réelle** du visiteur, dans > les 4 vues (mobile carte/contenu, web carte/contenu). > - `HideNextStepsUntilComplete` appliqué aussi en mode contenu, mobile et web. > - `IsLinear` appliqué côté web (il ne l'était pas du tout), et **navigation libre réellement > implémentée** : pins de carte tapables et segments de progression cliquables quand > `IsLinear = false`. > - Le retour arrière est désormais toujours possible vers une étape déjà atteinte, dans tous les modes. --- > ✅ **Corrigé le 2026-08-06 (M1, M2, W2)** — `Helpers/mapCenter.dart` centralise la résolution du > centre de carte (« Centrer sur » d'abord, puis le point de section, puis un défaut) pour les trois > vues mobile ; les horaires d'un point d'intérêt sont affichés dans `marker_view.dart` ; les icônes > de catégorie web utilisent `categorie.resourceDTO.url` — champ qui n'existait pas du tout dans le > type web. --- ## 3bis. Écart de cohérence entre les deux apps — bascule carte / contenu d'un parcours > ✅ **Corrigé le 2026-08-06** — les deux apps basculent maintenant sur **`ShowMap` seul** > (`parcours_page.dart` `_showMap`, `ParcoursSection.tsx` `useMapView`). `BaseSectionMapId` est > redevenu ce qu'il devait être : un fond de carte optionnel qui ne conditionne rien. > Le constat d'origine est conservé ci-dessous. **X1 — la condition d'affichage du mode carte n'était pas la même dans les deux apps.** | App | Condition | Fichier | |---|---|---| | mymuseum-visitapp | `showMap == true` **et `baseSectionMapId` renseigné** (une SectionMap de référence doit exister et se charger) | `parcours_page.dart:419` (`_showMap`), navigation `l.723-733` | | visitapp-web | `showMap === true` **et au moins une étape géolocalisée** (`stepsHaveGeo`) — `baseSectionMapId` n'entre pas en compte | `ParcoursSection.tsx:100-101` | **Conséquence** : une même configuration peut s'afficher en mode carte sur un canal et en mode contenu sur l'autre — donc avec géodéclenchement d'un côté et sans de l'autre (cf. M7/W7). | Configuration | mymuseum-visitapp | visitapp-web | |---|---|---| | `showMap=true`, étapes géolocalisées, **sans** `baseSectionMapId` | mode **contenu** → aucune géoloc | mode **carte** → géoloc active | | `showMap=true`, `baseSectionMapId` défini, étapes **sans** géométrie | mode **carte** (sans pins d'étape exploitables) | mode **contenu** | | `showMap=true`, `baseSectionMapId` + étapes géolocalisées | carte ✅ | carte ✅ | C'est le premier point à trancher avant même de décider du sort du mode contenu : tant que la règle diverge, un même parcours est incomparable entre les deux canaux et les tests ne sont pas reproductibles. **Piège associé côté manager-app** : `showNewOrUpdateGuidedStep()` reçoit `isEscapeMode` = `path.isGameMode`, **pas** `showMap` (`showNewOrUpdateGuidedPath.dart:265-269`, `l.334-338`). Le formulaire d'étape ne sait donc pas si la carte sera affichée, et propose le toggle « Déclenchement géolocalisé » avec le texte d'aide *« L'étape se débloque automatiquement quand le visiteur entre dans la zone »* — une promesse non tenue dès que le rendu tombe en mode contenu. --- > Note : `AppConfigurationLink.gridColSpan` / `gridRowSpan` **ne sont pas un écart** — le web les mappe en `colSpan`/`rowSpan` (`client.ts:171-172`) et les rend en CSS Grid (`ConfigurationGrid.tsx:38-48`). Idem `SectionQuiz.bad_level`… → `badLevel`, et `SectionParcours.guidedPaths` (chargés par un endpoint dédié côté mobile). --- ## 4. Champs backend morts ou sans propriétaire | Champ | État | |---|---| | `GuidedStep.FactContent` | Persisté (`MyInfoMateDbContext.cs:361`), **jamais édité, jamais affiché** — nulle part dans les 4 apps | | `SectionEvent.iconResource` / `iconResourceId` | Zéro usage réel côté visiteur, rôle indéterminé | | `SectionMap.MapResourceId` / `MapResource` | Déjà signalé "rôle flou" dans todo-features — l'audit confirme : aucun usage visiteur exploitable | | `SectionEvent.ParcoursIds` | Déjà connu comme legacy (remplacé par `GuidedPath.SectionEventId`) | | `GuidedStep.TriggerGeoPointId` | **N'existe pas** dans le modèle (`GuidedStep.cs`) — le géodéclenchement se fait via `Geometry` + `IsGeoTriggered` + `ZoneRadiusMeters`. Voir §5 | --- ## 5. Erreurs constatées dans `todo-features.md` (avant correction du 2026-08-05) | Ce que disait la doc | Réalité du code | |---|---| | "Les 4 repos compilent (`dotnet build`, `flutter analyze`, `npm run build`)" — section onboarding | **Faux.** Voir §6 : `npm run build` échoue, le projet de tests backend ne compile pas, `flutter analyze` remonte 8 erreurs sur `mymuseum-visitapp` | | "Gestion users par instance admin" ❌ | Scoping backend **fait** : `UserController` sous policy `InstanceAdmin`, filtrage par instance sur GET/GET{id}/PUT/DELETE, `InstanceId` forcé à l'instance de l'appelant au POST, protection contre l'escalade de rôle. **Manque uniquement** le plafond de 5 utilisateurs et le compteur "X / 5" dans l'UI | | "`triggerGeoPointId` exposé en saisie texte ✅" + tâche "TriggerGeoPointId — picker intuitif" + tooltip associé | **Le champ n'existe pas.** Tâches fantômes à supprimer | | "`showGeoZoneNotification()` dans `_checkGeoZone` des **deux** pages GuidedPath" | Uniquement dans `guided_path_map_progression_page.dart` | | "`GuidedPathContentProgressionPage` : mode contenu — **même logique**" | Sous-ensemble : ni géo, ni lock, ni linéaire, ni masquage (voir M7) | | Refacto §8 "Ajouter Parcours dans la liste des types de section" ⚠️ | **Fait** : `SectionParcours/section_parcours_config.dart` avec toggle `showMap` (l.63) et picker `baseSectionMapId` (l.81-90). Restent réellement à faire : les **tooltips ℹ️** (1 seul `Tooltip` dans tout le dossier Parcours) | | Escape game : "`game_page.dart` check `gameDTO.gameType == GameTypes.Escape`" (et test-plan §5.1) | **Obsolète** — `GameTypes` backend = `{ Puzzle, SlidingPuzzle }`, aucune référence à `Escape` dans les apps. ⚠️ Le client généré `manager_api_new/lib/model/game_types.dart:28` **contient encore** `Escape = 2` (désynchronisé) | | `isHiddenInitially` "non requis côté serveur" | Mais exposé comme toggle dans manager-app → incohérence à trancher (retirer l'UI ou l'implémenter) | --- ## 6. Blocages de déploiement mesurés le 2026-08-05 > **Au début de cet audit, aucune des 4 apps ne buildait.** > > **État au 2026-08-06** : B0, B1 et B2 sont corrigés — les 3 apps Flutter buildent, `visitapp-web` build en prod, et `dotnet test` passe **124/124**. Seul **B3 (`tablet-app`, Gradle)** reste ouvert. Voir [STATUS.md §1bis](STATUS.md). | # | Constat | Commande | État | |---|---|---|---| | **B0** | **Les 3 apps Flutter ne compilaient plus** — `Error: The language version override has to be the same in the library and its part(s)`. Cause : `manager_api_new/lib/api/onboarding_api.dart`, ajouté à la main pour l'onboarding (fichier *untracked*, avec `part 'api/onboarding_api.dart';` ajouté dans `api.dart`), ne portait pas l'annotation `// @dart=2.18` que porte `api.dart`. Un seul fichier cassait manager-app **et** mymuseum-visitapp **et** tablet-app. ⚠️ `flutter analyze` ne le voit pas — c'est une erreur du compilateur kernel, pas de l'analyzer | `flutter build web` / `flutter build apk` | ✅ **corrigé** (annotation ajoutée) + typage explicite dans `guided_step_challenge.dart:83` (`[...?step.quizQuestions]`). `manager-app` → `√ Built build\web` ; `mymuseum-visitapp` → 3 APK produits | | **B1** | **`visitapp-web` ne buildait pas en production.** 7 erreurs TypeScript sur l'indexation de `geometry.coordinates` (typé `unknown` dans `lib/api/types.ts:191`) : `map/LeafletMap.tsx` (5) et `MapSection.tsx` (2). Ces deux fichiers indexaient les coordonnées sans le `Array.isArray()` qui narrow le `unknown` — narrowing que `AgendaSection.tsx:41-43` faisait déjà correctement | `npm run build` → `Failed to type check` | ✅ **corrigé** : helper `getGeoPointLatLng()` ajouté dans `src/lib/geo.ts` (à côté de `getStepGeometryCenter`) et utilisé sur les 3 sites. Build prod OK. ⚠️ invisible en `next dev` (Turbopack ne type-checke pas) | | **B2** | **La suite de tests backend ne compile plus.** 3 × `CS7036` : `MyInfoMateDbContext` attend `IHttpContextAccessor`, `UserController` attend `IEmailService`, `AuthenticationController` attend `ProfileLogic` — les fakes de `ManagerService.Tests/` n'ont pas suivi | `dotnet test` | ✅ **corrigé le 2026-08-06** — 124/124. La remise en route a révélé un **vrai bug backend** : `BuildAuditEntries()` modifiait le `ChangeTracker` pendant son énumération → `InvalidOperationException` sur toute écriture d'entité auditée. Invisible tant que les tests ne compilaient pas | | **B3** | `tablet-app` : échec Gradle sur `flutter_tools/gradle/build.gradle.kts:7` (pas lié au client API). Le seul APK présent date d'avril | `flutter build apk` | ❌ ouvert, à investiguer | | **B4** | Client généré `manager_api_new/` : reliquats de désynchronisation, dont `model/game_types.dart:28` (`Escape = 2`) absent du backend | `flutter analyze` | Ouvert, non bloquant | > **Règle qui en découle** : `flutter analyze` et `next dev` ne prouvent rien. Seuls `flutter build`, > `npm run build` et `dotnet test` disent la vérité. Reprise en checklist dans [test-plan.md §0](test-plan.md). **Fix suggéré pour B1** : typer explicitement les coordonnées plutôt que de caster au point d'usage — `coordinates?: number[] | number[][] | number[][][]` dans `types.ts:191` (et `:121` pour `MapAnnotation.geometry`), puis vérifier les 7 sites. Alternative rapide : un helper `asPoint(coords): [number, number] | null` (le pattern existe déjà : `extractStepPoint()` dans `ParcoursSection.tsx:2448`) et l'utiliser partout. --- ## 7. Ordre de traitement suggéré avant déploiement **Bloquant (à faire avant toute mise en prod)** 1. B1 — build web 2. Rotation des secrets Git (cf. `security/audit-securite-manager-service.md`, toujours ouvert) 3. Fermeture du port PostgreSQL public 4. M7 / W7 — géodéclenchement en mode contenu, **ou** décision assumée : "le mode escape indoor ne supporte pas la géolocalisation" + retirer les champs géo de l'UI quand `ShowMap = false` **Important (écart client visible)** 5. M1 (centrage carte mobile), M2 (horaires mobile) 6. W8 — assistant IA en web, si l'add-on est vendu sur le plan Essentiel 7. ~~M6 — watermark trial mobile~~ — **écarté** (l'essai est web-only, cf. §2) 8. ~~M4 — trancher `isHiddenInitially`~~ — **résolu**, champ supprimé du modèle (migration `RemoveDeadGuidedStepFlags`) 9. ~~B2 — réparer les tests backend~~ — **fait** le 2026-08-06, 124/124 **Confort / cohérence** 10. W1, W2 (carte web : provider + icônes de catégorie) 11. M3, M5, W3, W4, W5, W6, W9, W10 12. Nettoyage des champs morts (§4) 13. Tooltips ℹ️ manager-app (le seul reliquat réel du refacto SectionParcours §8) --- ## 8. Comment refaire cet audit Le script d'extraction est jetable mais la méthode se reproduit : 1. Extraire les champs de chaque `public XxxDTO ToDTO()` dans `manager-service/ManagerService/Data/**`. 2. Pour chaque champ, chercher le nom **et** sa variante `camelCase` dans : `manager-app/lib` (hors `manager_api_new/` et `lib/api/`), `mymuseum-visitapp/lib`, `visitapp-web/src/lib` (couche API) et `visitapp-web/src/{components,app,hooks}` (couche UI) séparément — c'est la séparation API/UI qui révèle les champs "mappés mais jamais affichés". 3. Ne garder que `manager-app > 0` et (`mobile == 0` ou `web UI == 0`). 4. **Lire chaque candidat** : le taux de faux positifs est élevé (renommage, endpoint dédié, champ dérivé).