# Instance de démonstration MNAHA Jeu de données local pour les captures d'écran des decks MNAHA. Créé via l'API (`https://localhost:5001`), pas en SQL direct : l'indexation de l'assistant se déclenche alors toute seule à chaque publication. ## Ce qui a été créé | | | |---|---| | Instance | **MNAHA**, slug `mnaha`, plan Premium, assistant activé (guide « Melusina ») | | Langues | FR, DE, EN, NL — le luxembourgeois n'est pas géré par le code | | Sites | Nationalmusée um Fëschmaart · Musée Dräi Eechelen · Réimervilla Echternach | | Applications | mobile, borne, web — les trois liées aux trois sites, tuiles bento réglées | | Contenus | 9 articles en 4 langues, 1 sommaire, 1 quiz (3 questions), 1 agenda (4 événements) | | Carte | Echternach, 5 points d'intérêt géolocalisés, 3 catégories | | Parcours | « Sur les pas du maître de la villa » — 5 étapes déclenchées par zone GPS | | Escape game | « Le secret de la villa de Vichten » — 4 étapes, 4 énigmes bloquantes | | Balises | un identifiant de balise sur chacun des 9 articles | | Fréquentation | 23 980 événements, 2 503 sessions sur 90 jours, visite moyenne 8,1 min | | Assistant | 14 questions de visiteurs, dont 5 restées sans réponse | Répartition de la fréquentation : web 47 %, mobile 33 %, borne 20 % ; FR 44 %, DE 24 %, EN 21 %, NL 10 %. Entre 29 et 254 visites par jour, avec des pics de week-end et une légère montée sur la période. ## Accès | | | |---|---| | Back-office | `test@email.be` — SuperAdmin, voit toutes les entrées de menu | | Code PIN de l'instance | **1867** — sert à l'appairage des applications visiteur | | Adresse web | slug `mnaha` → `http://localhost:3000/mnaha` avec `visitapp-web` | | Clés d'application | `state.json`, champs `apiKeys.VisitApp` et `apiKeys.TabletApp` | Lancer une application visiteur sur cette instance : ```bash flutter run -d chrome \ --dart-define=API_BASE_URL=https://localhost:5001 \ --dart-define=INSTANCE_ID= \ --dart-define=API_KEY= ``` L'instance est en plan Premium complet : mobile + borne + web, assistant IA (20 M de jetons/mois), notifications push, statistiques avancées sur 395 jours, 50 Go de stockage, collecte des questions visiteurs activée. Seul `isVR` reste à faux — l'écran VR du back-office n'affiche qu'un « TODO ». ## Rejouer ```bash cd outputs/mnaha/seed python step1_instance.py # instance, sites, applications python step2_sections.py # articles, sommaire, quiz python step3_map_parcours.py # carte, parcours, escape game, agenda python step4_stats.py # 90 jours de fréquentation + ré-indexation python step5_assistant.py # conversations visiteurs (consomme du quota IA) python step6_order.py # ordre d'affichage des sections python step7_images.py # ressources ImageUrl + rattachement aux sections python step7b_finish.py # points d'intérêt et habillage des applications python step8_devices.py # parc de trois bornes, rattachées aux liens kiosk ``` Les étapes 1 à 3 et 6 sont idempotentes. **L'étape 4 ne l'est pas** : la relancer ajoute 24 000 événements de plus. Pour repartir à zéro : ```sql DELETE FROM "VisitEvents" WHERE "InstanceId" = ''; ``` `state.json` garde les identifiants créés ; le supprimer force une nouvelle instance (le nom « MNAHA » étant unique, la création échouerait alors en 409). ## Deux pièges rencontrés, notés pour la prochaine fois **Dates.** Une date sérialisée en `+00:00` est désérialisée par .NET en `Kind=Local`, et Npgsql refuse alors de l'écrire dans une colonne `timestamptz` — erreur 500 opaque. Il faut le suffixe `Z`. **Durée de visite.** Elle se calcule en sommant les `DurationSeconds` des événements `SectionLeave`, pas des `SectionView`. Sans `SectionLeave`, le tableau de bord affiche zéro minute. ## Visuels 21 images générées par `gen_images.py` : 3 sites, 9 articles, 2 parcours, 5 points d'intérêt, l'image d'accueil et l'écran de chargement des trois applications. > **Lancer `serve_images.py` avant toute capture.** Les visuels sont servis sur > `http://localhost:8099` et les ressources y pointent. Serveur éteint, images > absentes. Pourquoi un serveur local plutôt qu'un téléversement : **`POST /api/Resource/upload` ne stocke aucun octet.** Il lit le fichier, l'encode en base64, jette le résultat, et n'enregistre que le libellé, le chemin de bucket et la taille. Le blob est déposé dans Firebase Storage par `manager-app` depuis le navigateur — un script qui parle à l'API n'a aucun moyen de l'y écrire. Les ressources créées par cet endpoint ressortent donc sans `url`, et `manager-app` plante dessus (`NetworkImage(resource.url!)`, `resource_input_container.dart:170`). Les ressources de démonstration sont donc créées en type `ImageUrl`, qui pointe hors bucket et ne pèse pas sur le quota. Le jour où les visuels du dossier de presse arrivent, le chemin normal est de les déposer par le CMS. Ce sont des compositions abstraites — trame de tesselles, plan orthogonal, courbes de niveau, éventails Art déco, monnaies — déclinées dans la palette de chaque site. **Aucune photographie de collection** : les droits n'appartiennent pas au projet. Elles se remplacent par les visuels du dossier de presse du MNAHA le jour où il est disponible. Quatre autres motifs (`bastion`, `pillars`, `arcs`, `strata`) restent dans le fichier mais ne sont pas utilisés : réduits à une vignette, ils se lisent comme un aplat vide ou comme une frise trop littérale. ## Liens de borne dupliqués — artefact du script, pas bug produit `POST /api/Device` crée lui-même un `AppConfigurationLink` pour chaque nouvel appareil (`DeviceController.cs:169-177`), sans se rapprocher d'un lien déjà présent pour ce couple application/configuration. `step1_instance.py` liait les trois sites aux trois applications, borne comprise : les trois appareils créés ensuite ont donc ajouté trois liens de plus. `step1_instance.py` ne crée plus de lien pour la borne — ils naissent de l'appairage, ce qui est le comportement réel du produit. `step9_dedupe_links.py` nettoie ceux qui existent déjà. **Ce n'est pas atteignable depuis le CMS** : `showAddConfigurationLink` n'est utilisé que par l'écran des applications mobile et web, jamais par `kiosk_screen.dart`. Un gestionnaire ne peut pas créer un lien de borne à la main. La fragilité reste (la création d'appareil ne vérifie rien) mais elle dort derrière un chemin que seule l'API expose. Vérifié au passage, contrairement à ce que j'avais d'abord écrit : un `PUT /api/ApplicationInstance` portant ses `configurations` **ne duplique pas**, il échoue en 500 sur la clé primaire — et le `GET` ne renvoie de toute façon pas ce tableau. Un `PUT` de lien isolé ne duplique pas non plus. ## Quatre correctifs apportés au produit en passant **`DELETE /api/Resource/{id}` échouait sur toute carte ayant des catégories.** Le nettoyage des références faisait `categorie.resourceDTO.id == id` sans vérifier que `resourceDTO` existe ; une catégorie sans icône le laisse nul. Passé en `?.`. **L'écran des bornes plantait sur un lien sans appareil.** `kiosk_screen.dart` faisait `deviceLink.device!` alors qu'un contenu peut être attribué à une borne avant qu'une tablette ne s'y appaire. La carte affiche maintenant « aucune tablette appairée » au lieu de lever une exception. **`GET /api/Ai/insights/{instanceId}` renvoyait 500**, sur toutes les instances, y compris sans aucune question. `AiController` faisait un `SelectMany` sur `CitedContentIds`, une `List` stockée en jsonb via un convertisseur de valeur : EF ne sait pas traduire cette expression en SQL. Les listes sont maintenant rapatriées avant d'être aplaties côté client. `manager-app` avalait l'erreur en silence, donc l'onglet « questions des visiteurs » restait désespérément vide sans que rien ne le signale. **Le changement d'instance plantait `manager-app`** (`Bad state: No element`). La bascule vidait `menu.sections` puis naviguait vers la même route : Flutter réutilisait le `State`, `initState` ne rejouait pas, et le menu perdait définitivement Applications / Configurations / Ressources / Statistiques / Guide IA. Construction du menu extraite dans `_buildMenu`, appelée aussi depuis `didUpdateWidget`, et repli au lieu d'une exception quand la position courante ne correspond à aucune entrée. Les deux exigent un redémarrage : `dotnet run` pour le backend, **hot restart** (pas hot reload) pour `manager-app`.