diff --git a/CHANGELOG.md b/CHANGELOG.md index 98bf380..2c1fafd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog - FlowDeck +## v7.82.3 (2026-10-25) — URLs sœurs dérivées de l'environnement de l'agent + +- **Bug corrigé (dev)** : les URL sœurs servies par `draco_urls()` + (menu inter-apps, SSO, preset TikTak, références croisées) + restaient aux noms **prod** quand le `.env` ne déclarait pas les + quatre URL une à une — seul l'agent était en `-dev`, via + `AGENT_EVENEMENTS_URL`. Le menu inter-apps de la dev envoyait + donc vers la prod. +- **Dérivation d'environnement** (`app/services/draco_urls.py`, + résolution uniforme des quatre sœurs — ObsiGate, Flowdeck, + TikTak, Kanbante) : 1) URL explicite prioritaire, contrat + inchangé (`DRACO_SSO_URL`, `TIKTAK_URL`, `KANBANTE_URL`, + `FLOWDECK_URL`/`OAUTH_REDIRECT_URI`…) ; 2) sinon, si l'hôte de + l'agent tel que l'app le connaît déjà (`AGENT_EVENEMENTS_URL`, + via `agent_evenements_url()`) est de la forme + `agent.dracodev.net` (SUFFIXE éventuellement vide, + typiquement `-dev`), l'URL sœur est dérivée : + `https://.dracodev.net` (slugs `og`, `flowdeck`, + `tiktak`, `kanbante`) ; hôte hors motif (autre domaine, LAN, + IP) = **aucune dérivation**, jamais un lien deviné ; 3) sinon + défaut officiel, comme avant. L'URL **Gitea ne change pas** ; + les gabarits et le JS de la barre, qui consomment `draco_urls()`, + sont inchangés. +- Tests : `tests/test_draco_urls_devprod.py` +4 — seul + `AGENT_EVENEMENTS_URL=…agent-dev…` posé → sœurs en `-dev` ; + explicite gagne sur la dérivation ; hôtes d'agent hors motif + → défauts officiels ; agent officiel (suffixe vide) → sœurs + officielles. Non-régression « aucun env » déjà couverte. + ## v7.82.2 (2026-10-25) — CI test entièrement verte : pages-supports de templates exclues des listes Cause racine commune à 7 des 8 échecs restants : la refonte templates diff --git a/VERSION b/VERSION index e4d7aa7..eeb49e3 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.82.2 +7.82.3 diff --git a/app/main.py b/app/main.py index c0846ae..ab0262f 100644 --- a/app/main.py +++ b/app/main.py @@ -216,7 +216,7 @@ def _lire_version() -> str: # Littéral de version synchronisé avec le fichier VERSION : -# version="7.82.2" +# version="7.82.3" # Patron historique des bumps (voir scripts/bump_version.py, qui cherche # ce littéral) et contrôle de tests/test_pwa_offline.py. La version # runtime reste lue dynamiquement depuis VERSION par _lire_version() diff --git a/app/services/draco_urls.py b/app/services/draco_urls.py index d8add09..299142e 100644 --- a/app/services/draco_urls.py +++ b/app/services/draco_urls.py @@ -18,11 +18,26 @@ Priorité de résolution, à chaque appel (les tests et les modules qui lisaient déjà ``os.environ`` au moment de l'appel gardent ce comportement) : variable d'environnement > ``app.config.settings`` > défaut officiel. Aucun défaut ne contient ``-dev``. + +**Dérivation d'environnement (v7.82.3)** — pour les quatre URL +sœurs (ObsiGate, Flowdeck, TikTak, Kanbante), entre la configuration +explicite et le défaut officiel : si l'origine de l'agent telle que +l'app la connaît déjà (``AGENT_EVENEMENTS_URL``, via +``agent_evenements_url()``) a un hôte de la forme +``agent.dracodev.net`` (SUFFIXE éventuellement vide, +typiquement ``-dev``), l'URL sœur est dérivée : +``https://.dracodev.net`` (slugs : ``og``, +``flowdeck``, ``tiktak``, ``kanbante``). Ainsi une installation dev +qui ne déclare que l'agent pointe ses liens inter-apps en ``-dev`` +sans quatre variables de plus. Si l'hôte de l'agent ne correspond +pas au motif (autre domaine, LAN, IP), **aucune dérivation** : on +ne fabrique jamais un lien deviné. L'URL Gitea ne change pas. """ from __future__ import annotations import os +import re from urllib.parse import urlparse from app.config import settings @@ -51,9 +66,70 @@ def _resoudre(env_name: str, settings_attr: str, app: str) -> str: return OFFICIELLES[app] +# Slugs d'hôte des applications sœurs (dérivation d'environnement). +_SLUGS_SOEURS = { + "obsigate": "og", + "flowdeck": "flowdeck", + "tiktak": "tiktak", + "kanbante": "kanbante", +} + +# Hôte de l'agent : agent.dracodev.net (SUFFIXE = ``-dev``…). +_MOTIF_HOTE_AGENT = re.compile(r"^agent(-[a-z0-9-]+)?\.dracodev\.net$") + + +def _suffixe_environnement() -> str | None: + """Suffixe d'environnement porté par l'hôte de l'agent, ou None. + + Source : l'origine de l'agent telle que l'app la connaît déjà — + ``agent_evenements_url()`` (``AGENT_EVENEMENTS_URL`` explicite, + sinon dérivée de la base de l'agent). Retourne ``""`` pour + l'hôte officiel, ``"-dev"`` pour ``agent-dev.dracodev.net``, et + None si l'hôte ne correspond pas au motif (aucune dérivation). + """ + hote_agent = (urlparse(agent_evenements_url()).hostname or "").lower() + m = _MOTIF_HOTE_AGENT.match(hote_agent) + if not m: + return None + return m.group(1) or "" + + +def _url_derivee(app: str) -> str: + """URL sœur dérivée de l'environnement de l'agent ("" si aucune).""" + slug = _SLUGS_SOEURS.get(app) + if not slug: + return "" + suffixe = _suffixe_environnement() + if suffixe is None: + return "" + return f"https://{slug}{suffixe}.dracodev.net" + + +def _resoudre_soeur(env_name: str, settings_attr: str, app: str) -> str: + """Résolution d'une URL sœur : explicite > dérivation > officiel. + + La valeur ``settings`` égale au nom officiel est le *défaut* de + ``app/config.py``, pas un choix explicite : elle ne doit pas + bloquer la dérivation d'environnement. Une valeur settings + différente (dev, LAN, domaine propre) reste prioritaire. + """ + env = _nettoyer(os.environ.get(env_name, "")) + if env: + return env + cfg = _nettoyer(getattr(settings, settings_attr, "") or "") + if cfg and cfg != OFFICIELLES[app]: + return cfg + derivee = _url_derivee(app) + if derivee: + return derivee + if cfg: + return cfg + return OFFICIELLES[app] + + def obsigate_url() -> str: """ObsiGate — aussi l'émettrice SSO (DRACO_SSO_URL, déjà en service).""" - return _resoudre("DRACO_SSO_URL", "draco_sso_url", "obsigate") + return _resoudre_soeur("DRACO_SSO_URL", "draco_sso_url", "obsigate") def agent_url() -> str: @@ -62,11 +138,11 @@ def agent_url() -> str: def tiktak_url() -> str: - return _resoudre("TIKTAK_URL", "tiktak_url", "tiktak") + return _resoudre_soeur("TIKTAK_URL", "tiktak_url", "tiktak") def kanbante_url() -> str: - return _resoudre("KANBANTE_URL", "kanbante_url", "kanbante") + return _resoudre_soeur("KANBANTE_URL", "kanbante_url", "kanbante") def gitea_url() -> str: @@ -98,6 +174,9 @@ def flowdeck_url() -> str: base = _nettoyer(getattr(settings, "app_base_url", "") or "") if base and "localhost" not in base and "127.0.0.1" not in base: return base + derivee = _url_derivee("flowdeck") + if derivee: + return derivee return OFFICIELLES["flowdeck"] diff --git a/tests/test_draco_urls_devprod.py b/tests/test_draco_urls_devprod.py index bdd6b28..781f734 100644 --- a/tests/test_draco_urls_devprod.py +++ b/tests/test_draco_urls_devprod.py @@ -110,6 +110,72 @@ def test_preset_tiktak_depuis_config(monkeypatch): "https://tiktak-dev.dracodev.net/mcp" +def test_derivation_dev_depuis_agent_evenements_seul(monkeypatch): + # Cas réel de la dev : seul AGENT_EVENEMENTS_URL est posé — + # les quatre URLs sœurs servies suivent en ``-dev``, sans + # déclarer quatre variables de plus. Gitea ne change pas. + monkeypatch.setenv( + "AGENT_EVENEMENTS_URL", + "https://agent-dev.dracodev.net/api/v1/evenements") + urls = draco_urls.toutes() + assert urls["obsigate"] == "https://og-dev.dracodev.net" + assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net" + assert urls["tiktak"] == "https://tiktak-dev.dracodev.net" + assert urls["kanbante"] == "https://kanbante-dev.dracodev.net" + assert urls["gitea"] == "https://git.dracodev.net" + # Les accesseurs individuels partagent la même résolution. + assert draco_urls.obsigate_url() == "https://og-dev.dracodev.net" + assert draco_urls.flowdeck_url() == "https://flowdeck-dev.dracodev.net" + assert draco_urls.tiktak_url() == "https://tiktak-dev.dracodev.net" + assert draco_urls.kanbante_url() == "https://kanbante-dev.dracodev.net" + + +def test_derivation_explicite_gagne_sur_derivation(monkeypatch): + monkeypatch.setenv( + "AGENT_EVENEMENTS_URL", + "https://agent-dev.dracodev.net/api/v1/evenements") + # Explicites (contrat inchangé) : elles priment sur la dérivation, + # y compris quand elles pointent le nom officiel. + monkeypatch.setenv("DRACO_SSO_URL", "https://og.dracodev.net") + monkeypatch.setenv("TIKTAK_URL", "https://tiktak.example.net") + urls = draco_urls.toutes() + assert urls["obsigate"] == "https://og.dracodev.net" + assert urls["tiktak"] == "https://tiktak.example.net" + # Non déclarées : dérivées. + assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net" + assert urls["kanbante"] == "https://kanbante-dev.dracodev.net" + + +def test_derivation_hote_agent_hors_motif_defauts_officiels(monkeypatch): + # Autre domaine, LAN, IP : aucune dérivation, jamais un lien deviné. + for url_agent in ( + "https://agent.example.com/api/v1/evenements", + "https://agent-dev.example.net/api/v1/evenements", + "https://monagent-dev.dracodev.net/api/v1/evenements", + "https://agent-dev.dracodev.net.evil.com/api/v1/evenements", + "http://192.168.30.101:8790/api/v1/evenements", + "http://127.0.0.1:8790/api/v1/evenements", + "http://agent-dev.local:8790/api/v1/evenements", + ): + monkeypatch.setenv("AGENT_EVENEMENTS_URL", url_agent) + urls = draco_urls.toutes() + assert urls["obsigate"] == "https://og.dracodev.net", url_agent + assert urls["flowdeck"] == "https://flowdeck.dracodev.net", url_agent + assert urls["tiktak"] == "https://tiktak.dracodev.net", url_agent + assert urls["kanbante"] == "https://kanbante.dracodev.net", url_agent + + +def test_derivation_agent_officiel_suffixe_vide(monkeypatch): + # Hôte agent officiel : suffixe vide, les sœurs restent officielles. + monkeypatch.setenv( + "AGENT_EVENEMENTS_URL", "https://agent.dracodev.net/api/v1/evenements") + urls = draco_urls.toutes() + assert urls["obsigate"] == "https://og.dracodev.net" + assert urls["flowdeck"] == "https://flowdeck.dracodev.net" + assert urls["tiktak"] == "https://tiktak.dracodev.net" + assert urls["kanbante"] == "https://kanbante.dracodev.net" + + def test_bouton_sso_login_depuis_config(monkeypatch): from app.routers.auth import _sso_login_url