- 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.
8.5 KiB
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 :
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.pyavant toute capture. Les visuels sont servis surhttp://localhost:8099et 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.