- 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.
201 lines
7.8 KiB
Python
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()
|