Thomas Fransolet d14f151517 Livrables de presentation, seed de demo MNAHA et wireframes
- outputs/platform, outputs/prospect : scripts de construction des decks
  commerciaux + captures sources et .pptx generes.
- outputs/mnaha : seed complet de l'instance de demo luxembourgeoise
  (scripts par etape, contenus, images generees), le deck en version
  marque blanche et en version brandee, le PDF de presentation.
- outputs/Wireframes : maquettes app mobile, app web et manager.
- interview clients : notes de l'entretien Fourneau St Michel.

state.json et les __pycache__ sont ignores : le premier porte les cles
d'API de l'instance seedee, les scripts le regenerent.
2026-09-11 15:47:57 +02:00
..

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 mnahahttp://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 :

flutter run -d chrome \
  --dart-define=API_BASE_URL=https://localhost:5001 \
  --dart-define=INSTANCE_ID=<state.json → instanceId> \
  --dart-define=API_KEY=<state.json → apiKeys.VisitApp>

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

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 :

DELETE FROM "VisitEvents" WHERE "InstanceId" = '<id de l''instance>';

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<string> 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.