DOCS/outputs/mnaha/seed/README.md
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

171 lines
8.5 KiB
Markdown

# 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=<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
```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" = '<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`.