DOCS/outputs/mnaha/seed/step1_instance.py
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

201 lines
7.8 KiB
Python

# -*- coding: utf-8 -*-
"""Étape 1 — l'instance MNAHA, ses trois sites et ses trois applications.
Idempotent : relancer le script ne crée pas de doublon, il réutilise ce qui existe.
"""
import json
from pathlib import Path
from api import Api, LANGS, tr
STATE = Path(__file__).resolve().parent / "state.json"
INSTANCE_NAME = "MNAHA"
# Les couleurs se stockent comme Flutter les sérialise — « Color(0xaarrggbb) ».
# C'est la seule forme que `visitapp-web` dénormalise et que les apps Flutter
# savent relire ; un « 0xFF… » nu donne une couleur invalide, donc du gris.
SITES = [
{
"key": "feschmaart",
"label": "Nationalmusée um Fëschmaart",
"title": {"FR": "Nationalmusée um Fëschmaart",
"DE": "Nationalmuseum am Fischmarkt",
"EN": "National Museum on the Fish Market",
"NL": "Nationaal Museum aan de Vismarkt"},
"primary": "Color(0xff1f3a5f)",
"secondary": "Color(0xffb08d57)",
},
{
"key": "draieechelen",
"label": "Musée Dräi Eechelen",
"title": {"FR": "Musée Dräi Eechelen",
"DE": "Museum Dräi Eechelen",
"EN": "Dräi Eechelen Museum",
"NL": "Dräi Eechelen Museum"},
"primary": "Color(0xff4a5c3d)",
"secondary": "Color(0xff9c7a4a)",
},
{
"key": "echternach",
"label": "Réimervilla Echternach",
"title": {"FR": "Villa romaine d'Echternach",
"DE": "Römische Villa Echternach",
"EN": "Roman Villa of Echternach",
"NL": "Romeinse villa van Echternach"},
"primary": "Color(0xff7a4a2f)",
"secondary": "Color(0xffc0a16b)",
},
]
APP_TYPES = {"mobile": 0, "kiosk": 1, "web": 2}
# Tuiles de l'accueil mobile : le Fëschmaart occupe deux colonnes, c'est le site
# principal ; les deux autres se partagent la ligne suivante.
BENTO = {"feschmaart": (2, 1), "draieechelen": (1, 1), "echternach": (1, 1)}
def load_state():
return json.loads(STATE.read_text(encoding="utf-8")) if STATE.exists() else {}
def save_state(state):
STATE.write_text(json.dumps(state, indent=2, ensure_ascii=False), encoding="utf-8")
def main():
api = Api()
state = load_state()
instances = api.get("/api/Instance")
existing = next((i for i in instances if i["name"] == INSTANCE_NAME), None)
if existing:
instance = existing
print(f"instance existante : {instance['id']}")
else:
instance = api.post("/api/Instance", {
"name": INSTANCE_NAME,
"isMobile": True,
"isTablet": True,
"isWeb": True,
"isPushNotification": True,
"isAssistant": True,
"isVisitorQuestionCollectionEnabled": True,
"guideName": "Melusina",
"guidePersonaPrompt": (
"Tu es le guide numérique du Musée national d'archéologie, d'histoire "
"et d'art du Luxembourg. Tu réponds avec précision et sobriété, en "
"t'appuyant uniquement sur les contenus du musée. Quand une "
"information ne figure pas dans ces contenus, tu le dis clairement "
"et tu invites le visiteur à s'adresser à l'accueil."),
"guideFallbackMessages": tr(
FR="Cette information ne figure pas encore dans nos contenus. "
"L'équipe d'accueil pourra vous répondre.",
DE="Diese Information ist in unseren Inhalten noch nicht enthalten. "
"Unser Empfangsteam hilft Ihnen gerne weiter.",
EN="That information is not in our content yet. Our front desk team "
"will be able to help you.",
NL="Die informatie staat nog niet in onze inhoud. Ons onthaalteam "
"helpt u graag verder."),
"subscriptionPlanId": "plan-premium",
})
print(f"instance créée : {instance['id']} (slug {instance.get('webSlug')})")
instance_id = instance["id"]
state["instanceId"] = instance_id
state["webSlug"] = instance.get("webSlug")
state["pinCode"] = instance.get("pinCode")
# --- les trois sites, un par configuration
configs = api.get(f"/api/Configuration?instanceId={instance_id}") or []
by_label = {c["label"]: c for c in configs}
state.setdefault("configs", {})
for site in SITES:
found = by_label.get(site["label"])
if found:
cfg = found
print(f" config existante : {site['label']}")
else:
cfg = api.post("/api/Configuration", {
"instanceId": instance_id,
"label": site["label"],
"languages": LANGS,
"primaryColor": site["primary"],
"secondaryColor": site["secondary"],
"isOffline": site["key"] == "echternach",
})
print(f" config créée : {site['label']} -> {cfg['id']}")
# La création écrase le titre par « FR - Title », « DE - Title »… :
# le vrai titre ne peut être posé qu'ensuite.
if [t.get("value") for t in (cfg.get("title") or [])] != [t["value"] for t in tr(**site["title"])]:
cfg["title"] = tr(**site["title"])
cfg = api.put("/api/Configuration", cfg)
print(f" titre posé : {site['label']}")
state["configs"][site["key"]] = cfg["id"]
# --- les trois applications
app_instances = api.get(f"/api/ApplicationInstance?instanceId={instance_id}") or []
# Le GET renvoie `appType` en chaîne (« Mobile »), le POST l'attend en entier.
names = {0: "Mobile", 1: "Tablet", 2: "Web", 3: "VR", 4: "Voice"}
by_type = {}
for ai in app_instances:
raw = ai["appType"]
by_type[names.get(raw, raw) if isinstance(raw, int) else raw] = ai
state.setdefault("apps", {})
for name, app_type in APP_TYPES.items():
found = by_type.get(names[app_type])
if found:
app = found
print(f" application existante : {name}")
else:
app = api.post("/api/ApplicationInstance", {
"instanceId": instance_id,
"appType": app_type,
"languages": LANGS,
"primaryColor": "Color(0xff1f3a5f)",
"secondaryColor": "Color(0xffb08d57)",
"isAssistant": True,
"hasStats": True,
})
print(f" application créée : {name} -> {app['id']}")
state["apps"][name] = app["id"]
# Les liens de la borne naissent de l'appairage d'une tablette :
# `POST /api/Device` en crée un par appareil. En créer ici en plus
# donnerait deux cartes pour le même site (cf. step9_dedupe_links.py).
if name == "kiosk":
continue
# Les liens se lisent sur leur propre route : le DTO d'application
# renvoie un tableau vide, et s'y fier ferait recréer les liens à
# chaque exécution.
existing_links = api.get(
f"/api/ApplicationInstance/{app['id']}/application-link") or []
linked = {l["configurationId"] for l in existing_links}
for order, site in enumerate(SITES):
cfg_id = state["configs"][site["key"]]
if cfg_id in linked:
continue
col, row = BENTO[site["key"]]
api.post(f"/api/ApplicationInstance/{app['id']}/application-link", {
"applicationInstanceId": app["id"],
"configurationId": cfg_id,
"order": order,
"isActive": True,
"gridColSpan": col if app_type != 1 else 1,
"gridRowSpan": row,
})
print(f" lien {name} <- {site['label']}")
save_state(state)
print(f"\nétat écrit dans {STATE}")
if __name__ == "__main__":
main()