Import initial de la documentation : statut, roadmap, plans V1/V2, specs verticales (creche, sport), audits securite, plan de test, analyse concurrentielle et maquettes de design. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
211 lines
20 KiB
Markdown
211 lines
20 KiB
Markdown
# 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` (`<QuizQuestion>[...?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é).
|