diff --git a/.env.example b/.env.example
index a8a157a..7460b37 100644
--- a/.env.example
+++ b/.env.example
@@ -20,6 +20,27 @@ GITHUB_OAUTH_CLIENT_SECRET=
# Exemple : OAUTH_REDIRECT_URI=https://flowdeck.dracodev.net/auth/callback
OAUTH_REDIRECT_URI=
+# ── URLs publiques dracodev — dev/prod (v7.82.0) ──
+# AUCUN nom public n'est codé en dur dans le code vivant : tout passe
+# par ces variables (résolution : env > config > défaut). Les DÉFAUTS
+# sont les noms OFFICIELS (production, sans suffixe). Un environnement
+# DEV déclare ses noms en -dev ici — le .env survit aux déploiements
+# par release (passé par environment:/env_file: au conteneur).
+# Laisser vide = défaut officiel indiqué en commentaire.
+DRACO_SSO_URL= # ObsiGate, émettrice SSO — défaut https://og.dracodev.net ; dev : https://og-dev.dracodev.net
+DRACO_AGENT_URL= # Service d'agents Spark — défaut https://agent.dracodev.net ; dev : https://agent-dev.dracodev.net
+TIKTAK_URL= # TikTak (preset MCP, barre, refs) — défaut https://tiktak.dracodev.net ; dev : https://tiktak-dev.dracodev.net
+KANBANTE_URL= # Kanbante (barre, refs) — défaut https://kanbante.dracodev.net ; dev : https://kanbante-dev.dracodev.net
+FLOWDECK_URL= # URL publique de CETTE instance — défaut : dérivée d'OAUTH_REDIRECT_URI, sinon https://flowdeck.dracodev.net ; dev : https://flowdeck-dev.dracodev.net
+# SSO draco (Fusion ACC-3.2) — secret partagé avec l'émettrice ObsiGate.
+DRACO_SESSION_SECRET=
+DRACO_SESSION_SECRET_PREV=
+DRACO_SSO_MACHINE_KEY=
+# Événements vers le journal du service d'agents (Fusion, Lot 2).
+# AGENT_EVENEMENTS_URL vide = dérivée de DRACO_AGENT_URL (+ /api/v1/evenements).
+AGENT_EVENEMENTS_URL=
+AGENT_EVENEMENTS_CLE=
+
# ── Connecteurs OAuth (v7.56.0) — Google / Microsoft 365 ──
# Vidés = connecteur « non configuré » (le menu + affiche la raison).
# Le redirect URI à enregistrer est dérivé d'APP_BASE_URL :
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 36986a4..abeff56 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,44 @@
# Changelog - FlowDeck
+## v7.82.0 (2026-10-25) — Noms publics dracodev par configuration (dev/prod)
+
+- **Aucun nom public d'une autre application n'est plus codé en dur**
+ dans le code vivant : nouveau module `app/services/draco_urls.py`,
+ source unique qui résout chaque URL depuis l'environnement
+ (env > `app/config.py` > défaut). Les **défauts sont les noms
+ officiels** (production) ; un environnement dev déclare ses noms
+ en `-dev` dans son `.env` (qui survit aux déploiements par release).
+ Variables **ajoutées** : `DRACO_AGENT_URL`, `TIKTAK_URL`,
+ `KANBANTE_URL`, `FLOWDECK_URL`, `AGENT_EVENEMENTS_URL` (documentée).
+ Variables **réutilisées** : `DRACO_SSO_URL` (ObsiGate/SSO, déjà en
+ service), `OAUTH_REDIRECT_URI` (son origine sert d'URL publique à
+ l'instance si `FLOWDECK_URL` est vide), `AGENT_EVENEMENTS_URL` +
+ `AGENT_EVENEMENTS_CLE`, `GITEA_URL`, `APP_BASE_URL`.
+- **Surfaces corrigées** : CSP (`connect-src`/`frame-src` du service
+ d'agents dérivés de `DRACO_AGENT_URL`) ; bouton « Login with
+ ObsiGate (SSO) » de la page de connexion (émettrice + retour
+ dérivés de la config) ; émission d'événements (endpoint dérivé de
+ la base de l'agent si `AGENT_EVENEMENTS_URL` est vide) ; révocations
+ SSO (`DRACO_SSO_URL`) ; preset MCP TikTak des connecteurs et du
+ panneau agent (URL injectée par l'hôte) ; User-Agent du fetcher OG
+ (URL publique de l'instance) ; barre commune `_draco_bar.html`
+ (href injectés par l'hôte via `draco_urls()`) ; panneau Spark
+ vendangé (contrat d'injection `window.DRACO_PANNEAU_CONFIG.agent_url`
+ rendu par `base.html`, repli = nom officiel seulement ; déduction
+ de l'app hôte insensible au suffixe `-dev`) ; liens Gitea du
+ dashboard et des réglages (`GITEA_URL`).
+- **Références croisées** : le parseur de liens profonds reconnaît
+ l'hôte **configuré** de chaque app **et** l'hôte officiel, et l'URL
+ d'affichage est reconstruite avec l'hôte configuré (chemin, requête
+ et fragment intacts). **Aucune donnée migrée** : `target_url` reste
+ stockée telle que collée par l'utilisateur ; seule l'affichage est
+ recalculée.
+- **Extension Web Clipper** : les manifests autorisent aussi
+ l'hôte dev (`flowdeck-dev.dracodev.net`) à côté de l'officiel.
+- Tests : nouvelle suite `tests/test_draco_urls_devprod.py`
+ (défauts officiels sans `-dev`, surcharges dev, CSP, liens profonds
+ dev + officiels, URL d'affichage, preset TikTak, bouton SSO).
+
## v7.81.0 (2026-10-22) — Panneau Spark V3 : la console en surimpression pleine page
- **Fini le nouvel onglet** (demande de Bruno) : le bouton
diff --git a/README.md b/README.md
index 7595806..b39c0f9 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,25 @@ APP_PORT=8080
DATABASE_URL=sqlite:////data/flowdeck.db
```
+### Environnements dev/prod (v7.82.0)
+
+Aucun nom public d'une application dracodev n'est codé en dur : les
+URL des autres apps (ObsiGate/SSO, service d'agents, TikTak, Kanbante)
+et l'URL publique de Flowdeck se résolvent depuis le `.env` (voir la
+section « URLs publiques dracodev » de `.env.example` et
+`app/services/draco_urls.py`). Les valeurs par défaut sont les noms
+**officiels** (production) ; un environnement de développement déclare
+les noms en `-dev` dans son `.env`, par exemple
+`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` et
+`OAUTH_REDIRECT_URI=https://flowdeck-dev.dracodev.net/auth/callback`
+(dont l'origine sert d'URL publique à l'instance, sauf si
+`FLOWDECK_URL` est posée). La CSP, la barre commune, le panneau Spark,
+le bouton SSO, le preset MCP TikTak et les références croisées
+suivent automatiquement.
+
## Tests
```bash
diff --git a/VERSION b/VERSION
index 80af335..de7c343 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-7.81.0
+7.82.0
diff --git a/app/config.py b/app/config.py
index 9c498b9..872e68d 100644
--- a/app/config.py
+++ b/app/config.py
@@ -147,6 +147,28 @@ class Settings(BaseSettings):
github_token: str = "" # PAT GitHub : 30 req/min au lieu de 10
web_fetch_max_chars: int = 20000 # texte renvoyé par fetch_url
+ # ── URLs publiques dracodev (dev/prod, v7.82.0) ──
+ # AUCUN nom public d'une autre app n'est codé en dur dans le code
+ # vivant : chaque URL se résout ici (voir app/services/draco_urls.py).
+ # Les défauts sont les noms OFFICIELS (production) ; un environnement
+ # dev déclare les noms en `-dev` dans son .env, par exemple :
+ # 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
+ draco_sso_url: str = "https://og.dracodev.net" # ObsiGate (émettrice SSO)
+ draco_agent_url: str = "https://agent.dracodev.net" # service d'agents (Spark)
+ tiktak_url: str = "https://tiktak.dracodev.net"
+ kanbante_url: str = "https://kanbante.dracodev.net"
+ # URL publique de CETTE instance ; vide = dérivée de
+ # OAUTH_REDIRECT_URI, puis d'APP_BASE_URL (si non locale), puis du
+ # nom officiel.
+ flowdeck_url: str = ""
+ # AGENT_EVENEMENTS_URL (déjà en service) : vide = dérivée de
+ # DRACO_AGENT_URL + /api/v1/evenements.
+ agent_evenements_url: str = ""
+
# ── Vues Notion : géocodage + plafonds (§8.2, §13.4, §20) ──
GEOCODER_PROVIDER: str = "none" # none | nominatim | custom
GEOCODER_ENDPOINT: str = "https://nominatim.openstreetmap.org"
diff --git a/app/middleware/security.py b/app/middleware/security.py
index 6e67bcc..4724959 100644
--- a/app/middleware/security.py
+++ b/app/middleware/security.py
@@ -133,9 +133,14 @@ class ContentSecurityPolicyMiddleware(BaseHTTPMiddleware):
# Exception Fusion (2026-10-21) : le panneau Spark parle au
# service d'agents commun (premier parti, même programme) —
# seul hôte externe nommé, rien d'autre ne s'ouvre.
- "connect-src 'self' https://agent.dracodev.net ws://{host} wss://{host}; "
+ # v7.82.0 (dev/prod) : cet hôte n'est plus figé — {agent} est
+ # l'origine résolue par la config (DRACO_AGENT_URL ; défaut =
+ # nom officiel, un dev déclare son agent-dev dans le .env).
+ # Même origine pour frame-src : la console du service est
+ # encadrée en iframe par le panneau (surimpression v3).
+ "connect-src 'self' {agent} ws://{host} wss://{host}; "
"media-src 'self' blob:; "
- "frame-src 'self'; "
+ "frame-src 'self' {agent}; "
"object-src 'none'; "
"base-uri 'self'; "
"form-action 'self'; "
@@ -155,8 +160,12 @@ class ContentSecurityPolicyMiddleware(BaseHTTPMiddleware):
# on retire quand même tout caractère hors base URL par sécurité.
host = re.sub(r"[^0-9A-Za-z.\-:\[\]]", "",
request.headers.get("host", ""))
+ from app.services import draco_urls
+
+ agent = re.sub(r"[^0-9A-Za-z.\-:/]", "",
+ draco_urls.origine(draco_urls.agent_url()))
response.headers[self.CSP_HEADER] = self.CSP_VALUE.format(
- nonce=nonce, host=host
+ nonce=nonce, host=host, agent=agent
)
return response
diff --git a/app/routers/auth.py b/app/routers/auth.py
index d9755e6..252d35a 100644
--- a/app/routers/auth.py
+++ b/app/routers/auth.py
@@ -32,12 +32,32 @@ def get_redirect_uri(request: Request) -> str:
host = fwd_host.split(",")[0].strip() or request.headers.get("host", "localhost:8080")
return f"{scheme}://{host}/auth/callback"
+def _sso_login_url() -> str:
+ """URL du bouton « Login with ObsiGate (SSO) », résolue par la config.
+
+ Émettrice = DRACO_SSO_URL (défaut : nom officiel) ; retour =
+ URL publique de cette instance (FLOWDECK_URL / OAUTH_REDIRECT_URI /
+ APP_BASE_URL, défaut : nom officiel) + ``/auth/draco``. Aucun nom
+ public figé : un dev déclare ses noms en ``-dev`` dans le .env.
+ """
+ from urllib.parse import quote
+
+ from app.services import draco_urls
+
+ retour = f"{draco_urls.flowdeck_url()}/auth/draco"
+ return (f"{draco_urls.obsigate_url()}/sso/authorize"
+ f"?return_to={quote(retour, safe='')}")
+
+
def _with_nonce(html: str) -> str:
"""A20 : injecte le nonce CSP au moment du rendu.
`LOCAL_LOGIN_HTML` est une constante de module — le nonce, lui, est par
- requête, donc il ne peut être figé qu'ici.
+ requête, donc il ne peut être figé qu'ici. Même traitement pour l'URL
+ du bouton SSO ObsiGate (placeholder ``__SSO_LOGIN_URL__``), qui
+ dépend de la config dev/prod courante.
"""
+ html = html.replace("__SSO_LOGIN_URL__", _sso_login_url())
return html.replace("
diff --git a/app/templates/base.html b/app/templates/base.html
index 54c3dac..360d1d2 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -2871,6 +2871,11 @@
+{# Panneau Spark vendangé (dracodev-commons) : contrat d'injection —
+ l'hôte fournit l'URL du service d'agents depuis SA config
+ (DRACO_AGENT_URL ; défaut = nom officiel, -dev via le .env).
+ Le JS ne contient plus aucun nom public en dur. #}
+