"""URLs publiques de l'écosystème dracodev — par configuration (#223). Règle dev/prod (Bruno, 2026-10-25) : **aucun nom public de l'écosystème n'est figé dans le code vivant**. Chaque URL publique vient de l'environnement ; les valeurs par défaut sont les noms **officiels** (sans suffixe) de la future production. L'environnement dev déclare ses noms en ``-dev`` dans son seul ``.env`` — le code n'en connaît aucun. Variables (voir ``.env.example``) : - ``DRACO_AGENT_URL`` — base publique du service d'agents ; à défaut, l'origine est **dérivée** de ``AGENT_EVENEMENTS_URL`` (la configuration réelle de l'émission d'événements, #220), puis le défaut officiel. - ``DRACO_OBSIGATE_URL`` — base publique d'ObsiGate (liens profonds). - ``DRACO_FLOWDECK_URL`` — base publique de Flowdeck. - ``DRACO_TIKTAK_URL`` — base publique de TikTak. - ``DRACO_KANBANTE_URL`` — base publique de Kanbante. - ``DRACO_GITEA_URL`` — base publique de Gitea (les cartes Kanbante *sont* des issues Gitea, contrat liens-profonds) ; distincte de ``OBSIGATE_GITEA_URL`` (source connectée de l'assistant, réglable depuis l'UI, pas une URL d'affichage). Résolution d'une URL sœur (BUG-120) : la variable explicite prime ; à défaut, l'**environnement est dérivé de l'hôte de l'agent** tel qu'ObsiGate le connaît déjà (:func:`agent_url`) — si cet hôte est de la forme ``agent.dracodev.net`` (``-dev`` en dev, vide en prod), l'URL sœur est ``https://.dracodev.net``. Un hôte hors motif (autre domaine, LAN, IP, port) ne dérive **rien** : on ne fabrique jamais un lien deviné, le défaut officiel s'applique. L'installation pointe donc les bons liens selon son environnement sans déclarer les quatre ``DRACO__URL``. Ce module ne lit l'environnement qu'à l'appel (jamais à l'import) : les tests et un changement de ``.env`` suivi d'un redémarrage prennent effet sans état figé. """ from __future__ import annotations import os import re from urllib.parse import urlparse # Défauts officiels (production future) — jamais de nom en ``-dev`` ici. DEFAUTS: dict[str, str] = { "agent": "https://agent.dracodev.net", "obsigate": "https://og.dracodev.net", "flowdeck": "https://flowdeck.dracodev.net", "tiktak": "https://tiktak.dracodev.net", "kanbante": "https://kanbante.dracodev.net", "gitea": "https://git.dracodev.net", } _VARIABLES: dict[str, str] = { "agent": "DRACO_AGENT_URL", "obsigate": "DRACO_OBSIGATE_URL", "flowdeck": "DRACO_FLOWDECK_URL", "tiktak": "DRACO_TIKTAK_URL", "kanbante": "DRACO_KANBANTE_URL", "gitea": "DRACO_GITEA_URL", } # Slugs d'hôte des applications sœurs pour la dérivation # d'environnement (BUG-120) : premier label de ``.dracodev.net`` # (et de ``-dev.dracodev.net`` en dev). Gitea est **exclue** : # son URL ne se dérive jamais. _SLUGS_HOTE: dict[str, str] = { "obsigate": "og", "flowdeck": "flowdeck", "tiktak": "tiktak", "kanbante": "kanbante", } # Premier label de l'hôte de l'agent : ``agent`` suivi d'un suffixe # d'environnement éventuel (``-dev`` en dev, vide en prod). _MOTIF_AGENT = re.compile(r"^agent(-[a-z0-9-]*)?$") def _nettoyer(valeur: str) -> str: return valeur.strip().rstrip("/") def origine(url: str) -> str: """Origine ``schéma://hôte[:port]`` d'une URL (``""`` si illisible).""" u = urlparse(url or "") if u.scheme and u.netloc: return f"{u.scheme}://{u.netloc}" return "" def agent_url() -> str: """Base publique du service d'agents. Priorité : ``DRACO_AGENT_URL``, puis l'origine de ``AGENT_EVENEMENTS_URL`` (déjà la configuration réelle du poste), puis le défaut officiel. """ explicite = _nettoyer(os.environ.get(_VARIABLES["agent"], "")) if explicite: return explicite evenements = _nettoyer(os.environ.get("AGENT_EVENEMENTS_URL", "")) if evenements: derivee = origine(evenements) if derivee: return derivee return DEFAUTS["agent"] def agent_origine() -> str: """Origine du service d'agents, pour la CSP (``connect-src``/``frame-src``).""" return origine(agent_url()) or DEFAUTS["agent"] def _suffixe_environnement() -> str | None: """Suffixe d'environnement déduit de l'hôte de l'agent (BUG-120). Lit l'hôte de :func:`agent_url` — l'origine de l'agent telle qu'ObsiGate la connaît déjà (``DRACO_AGENT_URL``, à défaut ``AGENT_EVENEMENTS_URL``). Si cet hôte est exactement de la forme ``agent.dracodev.net`` sans port, retourne ``SUFFIXE`` (``-dev`` en dev, ``""`` en prod). Sinon (autre domaine, LAN, IP, port exotique) retourne ``None`` : aucune dérivation, on ne fabrique jamais un lien deviné. """ u = urlparse(agent_url()) hote = (u.hostname or "").lower() labels = hote.split(".") if len(labels) != 3 or labels[1:] != ["dracodev", "net"]: return None try: if u.port is not None: return None except ValueError: return None motif = _MOTIF_AGENT.match(labels[0]) if motif is None: return None return motif.group(1) or "" def app_url(app: str) -> str: """Base publique d'une application de l'écosystème (sans ``/`` final). Résolution d'une URL sœur (BUG-120) : ``DRACO__URL`` explicite d'abord ; à défaut, dérivation d'environnement depuis l'hôte de l'agent (:func:`_suffixe_environnement`) ; à défaut, le défaut officiel. L'URL Gitea ne se dérive jamais. """ if app == "agent": return agent_url() variable = _VARIABLES[app] explicite = _nettoyer(os.environ.get(variable, "")) if explicite: return explicite if app in _SLUGS_HOTE: suffixe = _suffixe_environnement() if suffixe is not None: return f"https://{_SLUGS_HOTE[app]}{suffixe}.dracodev.net" return DEFAUTS[app] def app_hote(app: str) -> str: """Nom d'hôte (minuscules) de l'URL publique configurée de *app*.""" return (urlparse(app_url(app)).hostname or "").lower() def hotes_reconnus() -> dict[str, str]: """``{hôte: app}`` pour le parseur de liens profonds. Contient **toujours** les hôtes officiels (un lien collé depuis la production reste reconnaissable en dev, et réciproquement) **plus** l'hôte résolu de chaque application (les noms en ``-dev`` du poste, déclarés dans son ``.env`` ou dérivés de l'hôte de l'agent). """ hotes: dict[str, str] = {} for app, defaut in DEFAUTS.items(): hote = (urlparse(defaut).hostname or "").lower() if hote: hotes[hote] = app for app in DEFAUTS: hote = app_hote(app) if hote: hotes[hote] = app return hotes def urls_publiques() -> dict[str, str]: """Carte des URL publiques injectée à l'hôte web (``window.DRACO_URLS``). C'est le contrat d'injection de dracodev-commons : l'application hôte (ici ObsiGate, qui sert ``index.html``) fournit les URL aux assets partagés (panneau Spark, barre, liens) — les assets ne portent que les défauts officiels en repli. """ return {app: app_url(app) for app in DEFAUTS}