198 lines
7.1 KiB
Python
198 lines
7.1 KiB
Python
"""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<SUFFIXE>.dracodev.net`` (``-dev`` en dev, vide en
|
|
prod), l'URL sœur est ``https://<slug><SUFFIXE>.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_<APP>_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 ``<slug>.dracodev.net``
|
|
# (et de ``<slug>-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<SUFFIXE>.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_<APP>_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}
|