"""URLs publiques des applications dracodev — résolution dev/prod. Source unique pour tout le code vivant qui doit nommer une autre application (CSP, panneau Spark, barre commune, SSO, presets MCP, références croisées…). Règle du chantier dev/prod : **aucun nom public codé en dur ailleurs** ; les défauts ci-dessous sont les noms OFFICIELS (production) et un environnement dev déclare ses noms en ``-dev`` dans le ``.env`` (qui survit aux déploiements par release, car il est passé par ``environment:``/``env_file:``) : DRACO_SSO_URL=https://og-dev.dracodev.net DRACO_AGENT_URL=https://agent-dev.dracodev.net TIKTAK_URL=https://tiktak-dev.dracodev.net KANBANTE_URL=https://kanbante-dev.dracodev.net FLOWDECK_URL=https://flowdeck-dev.dracodev.net (ou OAUTH_REDIRECT_URI) 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``. """ from __future__ import annotations import os from urllib.parse import urlparse from app.config import settings OFFICIELLES = { "obsigate": "https://og.dracodev.net", "agent": "https://agent.dracodev.net", "tiktak": "https://tiktak.dracodev.net", "kanbante": "https://kanbante.dracodev.net", "flowdeck": "https://flowdeck.dracodev.net", "gitea": "https://git.dracodev.net", } def _nettoyer(url: str) -> str: return (url or "").strip().rstrip("/") def _resoudre(env_name: str, settings_attr: str, app: str) -> str: env = _nettoyer(os.environ.get(env_name, "")) if env: return env cfg = _nettoyer(getattr(settings, settings_attr, "") or "") 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") def agent_url() -> str: """Service d'agents (Spark) — base sans chemin.""" return _resoudre("DRACO_AGENT_URL", "draco_agent_url", "agent") def tiktak_url() -> str: return _resoudre("TIKTAK_URL", "tiktak_url", "tiktak") def kanbante_url() -> str: return _resoudre("KANBANTE_URL", "kanbante_url", "kanbante") def gitea_url() -> str: return _resoudre("GITEA_URL", "gitea_url", "gitea") def flowdeck_url() -> str: """URL publique de cette instance de Flowdeck. FLOWDECK_URL > origine de OAUTH_REDIRECT_URI (déjà déclarée en dev : ``https://flowdeck-dev.dracodev.net/auth/callback``) > APP_BASE_URL si elle n'est pas locale > nom officiel. """ env = _nettoyer(os.environ.get("FLOWDECK_URL", "")) if env: return env cfg = _nettoyer(getattr(settings, "flowdeck_url", "") or "") if cfg: return cfg for candidate in ( os.environ.get("OAUTH_REDIRECT_URI", ""), getattr(settings, "oauth_redirect_uri", "") or "", ): candidate = _nettoyer(candidate) if candidate: p = urlparse(candidate) if p.scheme and p.netloc: return f"{p.scheme}://{p.netloc}" 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 return OFFICIELLES["flowdeck"] def agent_evenements_url() -> str: """Endpoint du journal d'événements du service d'agents. AGENT_EVENEMENTS_URL (déjà en service) prime ; sinon dérivée de la base de l'agent — jamais un nom figé. """ env = _nettoyer(os.environ.get("AGENT_EVENEMENTS_URL", "")) if env: return env cfg = _nettoyer(getattr(settings, "agent_evenements_url", "") or "") if cfg: return cfg return f"{agent_url()}/api/v1/evenements" def origine(url: str) -> str: """``scheme://netloc`` d'une URL (pour la CSP).""" p = urlparse(url or "") if p.scheme and p.netloc: return f"{p.scheme}://{p.netloc}" return _nettoyer(url) def hote(url: str) -> str: return (urlparse(url or "").hostname or "").lower() def toutes() -> dict[str, str]: """Toutes les URL résolues — injectées aux templates (hôte = config).""" return { "obsigate": obsigate_url(), "agent": agent_url(), "tiktak": tiktak_url(), "kanbante": kanbante_url(), "flowdeck": flowdeck_url(), "gitea": gitea_url(), } def hotes_connus(app: str) -> set[str]: """Hôtes reconnus pour ``app`` : l'hôte configuré **et** l'hôte officiel. Les références croisées déjà stockées portent des URL absolues (officielles aujourd'hui) : elles doivent rester reconnaissables dans un environnement dev, et inversement — aucune migration de données n'est faite pour un changement de nom. """ resolues = toutes() hotes = {hote(OFFICIELLES[app])} if app in resolues: hotes.add(hote(resolues[app])) return {h for h in hotes if h}