- 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.
171 lines
8.5 KiB
Markdown
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`.
|