refactor: noms publics dracodev par configuration (dev/prod)
FlowDeck CI / test (push) Failing after 35m34s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 1m41s

Aucun nom public d'une autre app dracodev n'est codé en dur dans le code vivant : nouveau module app/services/draco_urls.py (env > config > défaut officiel, jamais de -dev en défaut). CSP connect-src/frame-src, bouton SSO ObsiGate, révocations SSO, événements agent, preset MCP TikTak, barre commune, panneau Spark (injection hôte DRACO_PANNEAU_CONFIG), refs croisées (hôte configuré + officiel reconnus, URL d'affichage reconstruite, aucune donnée stockée migrée), User-Agent OG, liens Gitea. Variables ajoutées DRACO_AGENT_URL/TIKTAK_URL/KANBANTE_URL/FLOWDECK_URL, réutilisées DRACO_SSO_URL/OAUTH_REDIRECT_URI/AGENT_EVENEMENTS_URL/GITEA_URL/APP_BASE_URL. Version 7.82.0.
This commit is contained in:
Spark
2026-10-25 18:19:30 +00:00
parent eb4c42b0b5
commit f8449409b2
24 changed files with 580 additions and 42 deletions
+21
View File
@@ -20,6 +20,27 @@ GITHUB_OAUTH_CLIENT_SECRET=
# Exemple : OAUTH_REDIRECT_URI=https://flowdeck.dracodev.net/auth/callback # Exemple : OAUTH_REDIRECT_URI=https://flowdeck.dracodev.net/auth/callback
OAUTH_REDIRECT_URI= 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 ── # ── Connecteurs OAuth (v7.56.0) — Google / Microsoft 365 ──
# Vidés = connecteur « non configuré » (le menu + affiche la raison). # Vidés = connecteur « non configuré » (le menu + affiche la raison).
# Le redirect URI à enregistrer est dérivé d'APP_BASE_URL : # Le redirect URI à enregistrer est dérivé d'APP_BASE_URL :
+39
View File
@@ -1,5 +1,44 @@
# Changelog - FlowDeck # 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 ## v7.81.0 (2026-10-22) — Panneau Spark V3 : la console en surimpression pleine page
- **Fini le nouvel onglet** (demande de Bruno) : le bouton - **Fini le nouvel onglet** (demande de Bruno) : le bouton
+19
View File
@@ -86,6 +86,25 @@ APP_PORT=8080
DATABASE_URL=sqlite:////data/flowdeck.db 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 ## Tests
```bash ```bash
+1 -1
View File
@@ -1 +1 @@
7.81.0 7.82.0
+22
View File
@@ -147,6 +147,28 @@ class Settings(BaseSettings):
github_token: str = "" # PAT GitHub : 30 req/min au lieu de 10 github_token: str = "" # PAT GitHub : 30 req/min au lieu de 10
web_fetch_max_chars: int = 20000 # texte renvoyé par fetch_url 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) ── # ── Vues Notion : géocodage + plafonds (§8.2, §13.4, §20) ──
GEOCODER_PROVIDER: str = "none" # none | nominatim | custom GEOCODER_PROVIDER: str = "none" # none | nominatim | custom
GEOCODER_ENDPOINT: str = "https://nominatim.openstreetmap.org" GEOCODER_ENDPOINT: str = "https://nominatim.openstreetmap.org"
+12 -3
View File
@@ -133,9 +133,14 @@ class ContentSecurityPolicyMiddleware(BaseHTTPMiddleware):
# Exception Fusion (2026-10-21) : le panneau Spark parle au # Exception Fusion (2026-10-21) : le panneau Spark parle au
# service d'agents commun (premier parti, même programme) — # service d'agents commun (premier parti, même programme) —
# seul hôte externe nommé, rien d'autre ne s'ouvre. # 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:; " "media-src 'self' blob:; "
"frame-src 'self'; " "frame-src 'self' {agent}; "
"object-src 'none'; " "object-src 'none'; "
"base-uri 'self'; " "base-uri 'self'; "
"form-action '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é. # on retire quand même tout caractère hors base URL par sécurité.
host = re.sub(r"[^0-9A-Za-z.\-:\[\]]", "", host = re.sub(r"[^0-9A-Za-z.\-:\[\]]", "",
request.headers.get("host", "")) 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( response.headers[self.CSP_HEADER] = self.CSP_VALUE.format(
nonce=nonce, host=host nonce=nonce, host=host, agent=agent
) )
return response return response
+22 -2
View File
@@ -32,12 +32,32 @@ def get_redirect_uri(request: Request) -> str:
host = fwd_host.split(",")[0].strip() or request.headers.get("host", "localhost:8080") host = fwd_host.split(",")[0].strip() or request.headers.get("host", "localhost:8080")
return f"{scheme}://{host}/auth/callback" 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: def _with_nonce(html: str) -> str:
"""A20 : injecte le nonce CSP au moment du rendu. """A20 : injecte le nonce CSP au moment du rendu.
`LOCAL_LOGIN_HTML` est une constante de module — le nonce, lui, est par `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("<script>", f'<script nonce="{CSP_NONCE.get()}">', 1) return html.replace("<script>", f'<script nonce="{CSP_NONCE.get()}">', 1)
@@ -103,7 +123,7 @@ body{background:#191919;color:#fff;font-family:-apple-system,BlinkMacSystemFont,
<p style="font-size:13px;color:rgba(255,255,255,.4);margin-bottom:8px;">Or continue with</p> <p style="font-size:13px;color:rgba(255,255,255,.4);margin-bottom:8px;">Or continue with</p>
<button class="oauth-btn" onclick="window.location='/auth/login?provider=gitea'" style="margin-bottom:8px">🔗 <span id="gitea-btn-label">Login</span> with Gitea</button> <button class="oauth-btn" onclick="window.location='/auth/login?provider=gitea'" style="margin-bottom:8px">🔗 <span id="gitea-btn-label">Login</span> with Gitea</button>
<button class="oauth-btn" onclick="window.location='/auth/login?provider=github'" style="margin-bottom:8px">🐙 <span id="github-btn-label">Login</span> with GitHub</button> <button class="oauth-btn" onclick="window.location='/auth/login?provider=github'" style="margin-bottom:8px">🐙 <span id="github-btn-label">Login</span> with GitHub</button>
<button class="oauth-btn" onclick="window.location='https://og.dracodev.net/sso/authorize?return_to=https%3A%2F%2Fflowdeck.dracodev.net%2Fauth%2Fdraco'">🧭 Login with ObsiGate (SSO)</button> <button class="oauth-btn" onclick="window.location='__SSO_LOGIN_URL__'">🧭 Login with ObsiGate (SSO)</button>
</div> </div>
<!-- SSO / SAML + OIDC (v6.7.0) — buttons injected by loadSsoProviders() --> <!-- SSO / SAML + OIDC (v6.7.0) — buttons injected by loadSsoProviders() -->
<div class="oauth-section" id="sso-section" style="display:none"> <div class="oauth-section" id="sso-section" style="display:none">
+14 -2
View File
@@ -38,7 +38,6 @@ import httpx
log = logging.getLogger("flowdeck.evenements") log = logging.getLogger("flowdeck.evenements")
SOURCE_APP = "flowdeck" SOURCE_APP = "flowdeck"
URL_DEFAUT = "https://agent.dracodev.net/api/v1/evenements"
DELAI_S = 2.0 DELAI_S = 2.0
@@ -47,7 +46,20 @@ def _cle() -> str:
def _url() -> str: def _url() -> str:
return os.environ.get("AGENT_EVENEMENTS_URL", "").strip() or URL_DEFAUT # AGENT_EVENEMENTS_URL (en service) prime ; à défaut, l'endpoint
# est dérivé de la base de l'agent résolue par la config
# (DRACO_AGENT_URL, défaut = nom officiel) — aucun nom figé.
from app.services import draco_urls
return draco_urls.agent_evenements_url()
def _url_defaut() -> str:
return _url()
# Compat : ancien nom lu par d'éventuels consommateurs externes.
URL_DEFAUT = _url()
def actif() -> bool: def actif() -> bool:
+48 -14
View File
@@ -28,20 +28,54 @@ CUSTOM_KINDS = ("custom", "discord", "telegram", "mcp", "tiktak")
AUTH_SCHEMES = ("bearer", "bot", "none") AUTH_SCHEMES = ("bearer", "bot", "none")
# Presets proposés par le formulaire du menu + (le jeton reste toujours saisi # Presets proposés par le formulaire du menu + (le jeton reste toujours saisi
# par l'utilisateur — jamais codé en dur ici). # par l'utilisateur — jamais codé en dur ici).
PRESETS = { def _presets() -> dict:
"discord": {"url": "https://discord.com/api/v10", "auth": "bot", from app.services import draco_urls
"hint": "Bot Discord Developers → Token"},
"telegram": {"url": "https://api.telegram.org/bot{secret}", "auth": "none", return {
"hint": "BotFather → Bot Token (jeton dans l'URL)"}, "discord": {"url": "https://discord.com/api/v10", "auth": "bot",
"mcp": {"url": "https://", "auth": "bearer", "hint": "Bot Discord Developers → Token"},
"hint": "URL du endpoint MCP (streamable HTTP)"}, "telegram": {"url": "https://api.telegram.org/bot{secret}", "auth": "none",
# Fusion ACC-2.1 — première paire de fédération : le serveur MCP en "hint": "BotFather → Bot Token (jeton dans l'URL)"},
# lecture de TikTak (POST /mcp, JSON-RPC) ; la clé `tck_` se crée "mcp": {"url": "https://", "auth": "bearer",
# dans TikTak → Réglages → Intégrations. Comportement MCP standard "hint": "URL du endpoint MCP (streamable HTTP)"},
# (probe, cache d'outils, outils dynamiques mcp_tiktak_*). # Fusion ACC-2.1 — première paire de fédération : le serveur MCP en
"tiktak": {"url": "https://tiktak.dracodev.net/mcp", "auth": "bearer", # lecture de TikTak (POST /mcp, JSON-RPC) ; la clé `tck_` se crée
"hint": "TikTak → Réglages → Intégrations → clé API tck_…"}, # dans TikTak → Réglages → Intégrations. Comportement MCP standard
} # (probe, cache d'outils, outils dynamiques mcp_tiktak_*).
# v7.82.0 : URL dérivée de la config (TIKTAK_URL ; défaut officiel).
"tiktak": {"url": f"{draco_urls.tiktak_url()}/mcp", "auth": "bearer",
"hint": "TikTak → Réglages → Intégrations → clé API tck_…"},
}
class _Presets(dict):
"""Vue dict des presets, résolue à chaque accès (config dev/prod).
``PRESETS`` reste un dict pour ses lecteurs historiques
(``PRESETS["tiktak"]["url"]``), mais l'URL TikTak suit la config
courante au lieu d'être figée à l'import.
"""
def __getitem__(self, key):
return _presets()[key]
def __contains__(self, key):
return key in _presets()
def get(self, key, default=None):
return _presets().get(key, default)
def keys(self):
return _presets().keys()
def items(self):
return _presets().items()
def values(self):
return _presets().values()
PRESETS = _Presets()
OAUTH_KINDS = oauth_connectors.OAUTH_KINDS OAUTH_KINDS = oauth_connectors.OAUTH_KINDS
FETCH_LIMIT = 20000 # caractères max renvoyés au LLM (ponytail : borne fixe) FETCH_LIMIT = 20000 # caractères max renvoyés au LLM (ponytail : borne fixe)
+3 -1
View File
@@ -97,7 +97,9 @@ def _refresh_revocations() -> None:
if now - _revoked_fetched_at < _REVOCATIONS_TTL: if now - _revoked_fetched_at < _REVOCATIONS_TTL:
return return
_revoked_fetched_at = now _revoked_fetched_at = now
base = os.environ.get("DRACO_SSO_URL", "https://og.dracodev.net") from app.services import draco_urls
base = draco_urls.obsigate_url()
try: try:
import httpx import httpx
+154
View File
@@ -0,0 +1,154 @@
"""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}
+51 -4
View File
@@ -35,14 +35,22 @@ def parse_deep_link(url: str) -> tuple[str, str, str, str] | None:
except ValueError: except ValueError:
return None return None
host = (u.hostname or "").lower() host = (u.hostname or "").lower()
if host == "tiktak.dracodev.net": # v7.82.0 (dev/prod) : les hôtes reconnus viennent de la config
# (TIKTAK_URL, DRACO_SSO_URL, FLOWDECK_URL/OAUTH_REDIRECT_URI,
# GITEA_URL/KANBANTE_URL) — un dev colle des URL en `-dev`, la prod
# des URL officielles. L'hôte officiel reste TOUJOURS reconnu en
# plus de l'hôte configuré : les références déjà stockées portent
# des URL absolues et aucune donnée n'est migrée pour un renommage.
from app.services import draco_urls
if host in draco_urls.hotes_connus("tiktak"):
q = parse_qs(u.query) q = parse_qs(u.query)
if q.get("task"): if q.get("task"):
return ("tiktak", "task", q["task"][0], f"TikTak · tâche {q['task'][0]}") return ("tiktak", "task", q["task"][0], f"TikTak · tâche {q['task'][0]}")
if q.get("list"): if q.get("list"):
return ("tiktak", "list", q["list"][0], f"TikTak · liste {q['list'][0]}") return ("tiktak", "list", q["list"][0], f"TikTak · liste {q['list'][0]}")
return None return None
if host == "og.dracodev.net": if host in draco_urls.hotes_connus("obsigate"):
frag = u.fragment or "" frag = u.fragment or ""
if frag.startswith("file="): if frag.startswith("file="):
cible = unquote(frag[5:]) cible = unquote(frag[5:])
@@ -50,7 +58,7 @@ def parse_deep_link(url: str) -> tuple[str, str, str, str] | None:
nom = cible.rsplit("/", 1)[-1] nom = cible.rsplit("/", 1)[-1]
return ("obsigate", "note", cible, f"ObsiGate · {nom}") return ("obsigate", "note", cible, f"ObsiGate · {nom}")
return None return None
if host == "flowdeck.dracodev.net": if host in draco_urls.hotes_connus("flowdeck"):
m = re.fullmatch(r"/pages/(\d+)", u.path or "") m = re.fullmatch(r"/pages/(\d+)", u.path or "")
if m: if m:
return ("flowdeck", "page", m.group(1), f"Flowdeck · page {m.group(1)}") return ("flowdeck", "page", m.group(1), f"Flowdeck · page {m.group(1)}")
@@ -58,7 +66,7 @@ def parse_deep_link(url: str) -> tuple[str, str, str, str] | None:
if m: if m:
return ("flowdeck", "project", m.group(1), f"Flowdeck · projet {m.group(1)}") return ("flowdeck", "project", m.group(1), f"Flowdeck · projet {m.group(1)}")
return None return None
if host == "git.dracodev.net": if host in draco_urls.hotes_connus("gitea") | draco_urls.hotes_connus("kanbante"):
m = re.fullmatch(r"/([^/]+)/([^/]+)/issues/(\d+)", u.path or "") m = re.fullmatch(r"/([^/]+)/([^/]+)/issues/(\d+)", u.path or "")
if m: if m:
owner, repo, num = m.groups() owner, repo, num = m.groups()
@@ -67,9 +75,48 @@ def parse_deep_link(url: str) -> tuple[str, str, str, str] | None:
return None return None
def url_affichage(target_url: str) -> str:
"""URL d'affichage d'une référence : l'hôte vient de la config.
Les références **stockées** portent l'URL absolue collée par
l'utilisateur (``target_url``, jamais migrée ni réécrite en base).
À l'affichage, si l'hôte stocké est un hôte dracodev connu
(officiel ou configuré), il est remplacé par l'hôte configuré de
l'environnement courant — chemin, requête et fragment intacts :
une réf créée en prod s'ouvre sur le dev quand on la consulte
depuis le dev, et inversement. Hôte inconnu : URL inchangée.
"""
try:
u = urlparse((target_url or "").strip())
except ValueError:
return target_url
host = (u.hostname or "").lower()
if not host:
return target_url
from app.services import draco_urls
base = None
for app, url in (
("tiktak", draco_urls.tiktak_url()),
("obsigate", draco_urls.obsigate_url()),
("flowdeck", draco_urls.flowdeck_url()),
("gitea", draco_urls.gitea_url()),
("kanbante", draco_urls.kanbante_url()),
):
if host in draco_urls.hotes_connus(app):
base = url
break
if base is None:
return target_url
return u._replace(scheme=urlparse(base).scheme,
netloc=urlparse(base).netloc).geturl()
def _row_to_ref(row) -> dict: def _row_to_ref(row) -> dict:
d = dict(row) d = dict(row)
d["app_color"] = APP_COLORS.get(d["target_app"], "#888") d["app_color"] = APP_COLORS.get(d["target_app"], "#888")
# Affichage seulement : la valeur stockée en base n'est pas touchée.
d["target_url"] = url_affichage(d.get("target_url") or "")
return d return d
+5 -1
View File
@@ -146,8 +146,12 @@ async def fetch_og_metadata(url: str, timeout: float = 6.0, transport=None) -> d
base = {"url": src, "title": "", "description": "", "image": "", "site_name": "", "favicon": ""} base = {"url": src, "title": "", "description": "", "image": "", "site_name": "", "favicon": ""}
try: try:
from app.services import draco_urls
headers = { headers = {
"User-Agent": "FlowDeck/5.5 bookmark-fetcher (+https://flowdeck.dracodev.net)", # URL de contact = URL publique de l'instance, résolue par
# la config (FLOWDECK_URL/OAUTH_REDIRECT_URI ; défaut officiel).
"User-Agent": f"FlowDeck/5.5 bookmark-fetcher (+{draco_urls.flowdeck_url()})",
"Accept": "text/html,application/xhtml+xml", "Accept": "text/html,application/xhtml+xml",
} }
kwargs = {"timeout": timeout} kwargs = {"timeout": timeout}
+8 -5
View File
@@ -2,7 +2,10 @@
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.1.4 Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.1.4
(ui/barre-apps/) : ne pas modifier la structure ici ; toute évolution (ui/barre-apps/) : ne pas modifier la structure ici ; toute évolution
passe par une nouvelle version de commons. passe par une nouvelle version de commons.
CSS/JS : /static/css/draco-barre.css, /static/js/vendor/draco-barre.js. #} CSS/JS : /static/css/draco-barre.css, /static/js/vendor/draco-barre.js.
v7.82.0 (dev/prod) : les href des onglets ne sont plus figés —
l'hôte les injecte depuis sa config (draco_urls(), noms officiels
par défaut, noms en -dev déclarés dans le .env de l'environnement). #}
<header class="draco-bar" data-current="flowdeck"> <header class="draco-bar" data-current="flowdeck">
<div class="draco-bar-zone draco-bar-gauche"> <div class="draco-bar-zone draco-bar-gauche">
<button type="button" class="draco-icon-btn" data-act="rail" aria-label="Barre latérale">☰</button> <button type="button" class="draco-icon-btn" data-act="rail" aria-label="Barre latérale">☰</button>
@@ -10,20 +13,20 @@
<span class="draco-brand-mark">✦</span><span class="draco-brand-name">dracodev</span> <span class="draco-brand-mark">✦</span><span class="draco-brand-name">dracodev</span>
</a> </a>
<nav class="draco-apptabs" aria-label="Applications"> <nav class="draco-apptabs" aria-label="Applications">
<a class="draco-apptab" data-app="obsigate" href="https://og.dracodev.net/"> <a class="draco-apptab" data-app="obsigate" href="{{ draco_urls().obsigate }}/">
<span class="draco-apptab-ico" style="--app:#7a5af5">▤</span> <span class="draco-apptab-ico" style="--app:#7a5af5">▤</span>
<span class="draco-apptab-txt"><strong>ObsiGate</strong><small>Notes</small></span> <span class="draco-apptab-txt"><strong>ObsiGate</strong><small>Notes</small></span>
</a> </a>
<a class="draco-apptab" data-app="flowdeck" href="https://flowdeck.dracodev.net/"> <a class="draco-apptab" data-app="flowdeck" href="{{ draco_urls().flowdeck }}/">
<span class="draco-apptab-ico" style="--app:#2f6fed">▦</span> <span class="draco-apptab-ico" style="--app:#2f6fed">▦</span>
<span class="draco-apptab-txt"><strong>Flowdeck</strong><small>Projets</small></span> <span class="draco-apptab-txt"><strong>Flowdeck</strong><small>Projets</small></span>
</a> </a>
<a class="draco-apptab" data-app="tiktak" href="https://tiktak.dracodev.net/"> <a class="draco-apptab" data-app="tiktak" href="{{ draco_urls().tiktak }}/">
<span class="draco-apptab-ico" style="--app:#e8762c">✓</span> <span class="draco-apptab-ico" style="--app:#e8762c">✓</span>
<span class="draco-apptab-txt"><strong>TikTak</strong><small>Tâches</small></span> <span class="draco-apptab-txt"><strong>TikTak</strong><small>Tâches</small></span>
<span class="draco-badge" data-badge="tiktak" hidden>0</span> <span class="draco-badge" data-badge="tiktak" hidden>0</span>
</a> </a>
<a class="draco-apptab" data-app="kanbante" href="https://kanbante.dracodev.net/"> <a class="draco-apptab" data-app="kanbante" href="{{ draco_urls().kanbante }}/">
<span class="draco-apptab-ico" style="--app:#0d9488">▥</span> <span class="draco-apptab-ico" style="--app:#0d9488">▥</span>
<span class="draco-apptab-txt"><strong>Kanbante</strong><small>Kanban</small></span> <span class="draco-apptab-txt"><strong>Kanbante</strong><small>Kanban</small></span>
<span class="draco-badge" data-badge="kanbante" hidden>0</span> <span class="draco-badge" data-badge="kanbante" hidden>0</span>
+1
View File
@@ -533,4 +533,5 @@ body.fd-ap-resizing *{cursor:col-resize!important}
</div> </div>
</div> </div>
<script nonce="{{ csp_nonce() }}">window.FLOWDECK_TIKTAK_MCP_URL = {{ (draco_urls().tiktak + "/mcp") | tojson }};</script>
<script data-cfasync="false" nonce="{{ csp_nonce() }}" src="/static/js/agent_panel_2.js?v={{ asset_version }}"></script> <script data-cfasync="false" nonce="{{ csp_nonce() }}" src="/static/js/agent_panel_2.js?v={{ asset_version }}"></script>
+5
View File
@@ -2871,6 +2871,11 @@
</script> </script>
<script src="/static/js/app.js?v={{ asset_version }}" defer data-cfasync="false"></script> <script src="/static/js/app.js?v={{ asset_version }}" defer data-cfasync="false"></script>
<script src="/static/js/vendor/draco-barre.js?v={{ asset_version }}" defer data-cfasync="false"></script> <script src="/static/js/vendor/draco-barre.js?v={{ asset_version }}" defer data-cfasync="false"></script>
{# 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. #}
<script nonce="{{ csp_nonce() }}">window.DRACO_PANNEAU_CONFIG = { agent_url: {{ draco_agent_url() | tojson }} };</script>
<script src="/static/js/draco-panneau.js?v={{ asset_version }}" defer data-cfasync="false"></script> <script src="/static/js/draco-panneau.js?v={{ asset_version }}" defer data-cfasync="false"></script>
<style> <style>
+1 -1
View File
@@ -72,7 +72,7 @@
{% if search %} {% if search %}
Aucun résultat pour "{{ search }}". Aucun résultat pour "{{ search }}".
{% else %} {% else %}
Créez un repo sur <a href="https://git.dracodev.net" style="color:var(--blue);">Gitea</a>. Créez un repo sur <a href="{{ gitea_url() }}" style="color:var(--blue);">Gitea</a>.
{% endif %} {% endif %}
</p> </p>
</div> </div>
+1 -1
View File
@@ -549,7 +549,7 @@
<div> <div>
<div class="setting-label">Gitea <span x-show="authMethod==='gitea'" style="font-size:10px;background:var(--accent);color:#fff;padding:1px 6px;border-radius:3px;margin-left:6px;">Primary</span></div> <div class="setting-label">Gitea <span x-show="authMethod==='gitea'" style="font-size:10px;background:var(--accent);color:#fff;padding:1px 6px;border-radius:3px;margin-left:6px;">Primary</span></div>
<div class="setting-desc"> <div class="setting-desc">
<span x-show="giteaLinked">Connected to git.dracodev.net</span> <span x-show="giteaLinked">Connected to {{ gitea_url() }}</span>
<span x-show="!giteaLinked">Connect your Gitea account to access projects</span> <span x-show="!giteaLinked">Connect your Gitea account to access projects</span>
</div> </div>
</div> </div>
+12
View File
@@ -85,6 +85,12 @@ ENV.globals["plugin_enabled"] = lambda slug: _plugin_enabled(slug)
# liens de partage de pages (collaboration.py) : si elle est mal renseignée, # liens de partage de pages (collaboration.py) : si elle est mal renseignée,
# ces liens le sont déjà. # ces liens le sont déjà.
ENV.globals["app_base_url"] = lambda: _app_base_url() ENV.globals["app_base_url"] = lambda: _app_base_url()
# URLs publiques dracodev résolues par la config (dev/prod, v7.82.0) :
# l'hôte fournit les URL aux composants vendangés (barre commune,
# panneau Spark) — aucun nom public figé dans les templates/JS.
ENV.globals["draco_urls"] = lambda: _draco_urls()
ENV.globals["draco_agent_url"] = lambda: _draco_urls()["agent"]
ENV.globals["gitea_url"] = lambda: _draco_urls()["gitea"]
def _app_base_url() -> str: def _app_base_url() -> str:
@@ -93,6 +99,12 @@ def _app_base_url() -> str:
return (settings.app_base_url or "").rstrip("/") return (settings.app_base_url or "").rstrip("/")
def _draco_urls() -> dict:
from app.services import draco_urls
return draco_urls.toutes()
def _plugin_enabled(slug: str) -> bool: def _plugin_enabled(slug: str) -> bool:
from app.services.plugins import is_enabled from app.services.plugins import is_enabled
+1 -1
View File
@@ -4,7 +4,7 @@
"description": "Capturez du contenu web directement dans FlowDeck", "description": "Capturez du contenu web directement dans FlowDeck",
"version": "1.0.0", "version": "1.0.0",
"permissions": ["activeTab", "storage", "scripting", "contextMenus"], "permissions": ["activeTab", "storage", "scripting", "contextMenus"],
"host_permissions": ["<all_urls>", "https://flowdeck.local/*", "https://flowdeck.dracodev.net/*", "http://localhost:8080/*", "http://localhost:3000/*"], "host_permissions": ["<all_urls>", "https://flowdeck.local/*", "https://flowdeck.dracodev.net/*", "https://flowdeck-dev.dracodev.net/*", "http://localhost:8080/*", "http://localhost:3000/*"],
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js"
}, },
+1 -1
View File
@@ -4,7 +4,7 @@
"description": "Capturez du contenu web directement dans FlowDeck", "description": "Capturez du contenu web directement dans FlowDeck",
"version": "1.0.0", "version": "1.0.0",
"permissions": ["activeTab", "storage", "scripting", "contextMenus"], "permissions": ["activeTab", "storage", "scripting", "contextMenus"],
"host_permissions": ["<all_urls>", "https://flowdeck.local/*", "https://flowdeck.dracodev.net/*", "http://localhost:8080/*", "http://localhost:3000/*"], "host_permissions": ["<all_urls>", "https://flowdeck.local/*", "https://flowdeck.dracodev.net/*", "https://flowdeck-dev.dracodev.net/*", "http://localhost:8080/*", "http://localhost:3000/*"],
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js"
}, },
+1 -1
View File
@@ -1424,7 +1424,7 @@
telegram:{url:'https://api.telegram.org/bot{secret}', auth:'none', telegram:{url:'https://api.telegram.org/bot{secret}', auth:'none',
hint:'BotFather → Bot Token ({secret} complète l\u2019URL)'}, hint:'BotFather → Bot Token ({secret} complète l\u2019URL)'},
mcp: {url:'', auth:'bearer', hint:'URL du endpoint MCP (streamable HTTP)'}, mcp: {url:'', auth:'bearer', hint:'URL du endpoint MCP (streamable HTTP)'},
tiktak: {url:'https://tiktak.dracodev.net/mcp', auth:'bearer', name:'TikTak', tiktak: {url:(window.FLOWDECK_TIKTAK_MCP_URL || 'https://tiktak.dracodev.net/mcp'), auth:'bearer', name:'TikTak',
hint:'TikTak → Réglages → Intégrations → clé API tck_…'} hint:'TikTak → Réglages → Intégrations → clé API tck_…'}
}; };
var p = presets[f.kind] || presets.custom; var p = presets[f.kind] || presets.custom;
+14 -4
View File
@@ -19,7 +19,7 @@
* (persistée), thème clair/sombre aligné sur l'app hôte * (persistée), thème clair/sombre aligné sur l'app hôte
* (html[data-theme], repli prefers-color-scheme, suivi en direct). * (html[data-theme], repli prefers-color-scheme, suivi en direct).
* Bouton « Console » (barre des discussions) : ouvre la console web * Bouton « Console » (barre des discussions) : ouvre la console web
* du même service (agent.dracodev.net) DANS la page, en surimpression * du même service (URL injectée par l'hôte, voir plus bas) DANS la page, en surimpression
* pleine page (iframe) qui naît à la géométrie du panneau puis * pleine page (iframe) qui naît à la géométrie du panneau puis
* s'étend à toute la fenêtre ; l'URL profonde (?disc= discussion * s'étend à toute la fenêtre ; l'URL profonde (?disc= discussion
* courante, ?app= app d'origine déduite du sous-domaine) est celle * courante, ?app= app d'origine déduite du sous-domaine) est celle
@@ -32,7 +32,14 @@
* configuré / exécuteur non configuré sont affichés tels quels. */ * configuré / exécuteur non configuré sont affichés tels quels. */
(function () { (function () {
"use strict"; "use strict";
var API = "https://agent.dracodev.net"; /* Contrat d'injection dracodev-commons (dev/prod) : l'app hôte
* fournit l'URL du service via window.DRACO_PANNEAU_CONFIG.agent_url
* (rendue depuis sa config serveur : DRACO_AGENT_URL, défaut = nom
* officiel ; un dev déclare son agent-dev dans le .env). Le repli
* ci-dessous n'est que le nom OFFICIEL, jamais un nom -dev. */
var API = ((window.DRACO_PANNEAU_CONFIG && window.DRACO_PANNEAU_CONFIG.agent_url) ||
"https://agent.dracodev.net").replace(/\/+$/, "");
var API_HOST = API.replace(/^https?:\/\//, "");
var convId = null; var convId = null;
var convKind = null; // 'principale' | 'parallele' var convKind = null; // 'principale' | 'parallele'
var convTitre = ""; var convTitre = "";
@@ -57,6 +64,9 @@
/* ── Console web (même service, surimpression pleine page) ─── */ /* ── Console web (même service, surimpression pleine page) ─── */
function appHote() { function appHote() {
var h = (window.location.hostname || "").split(".")[0]; var h = (window.location.hostname || "").split(".")[0];
/* Dev/prod : les hôtes dev portent un suffixe -dev
* (flowdeck-dev, og-dev…) — on le retire avant correspondance. */
h = h.replace(/-dev$/, "");
var correspondance = { tiktak: "tiktak", flowdeck: "flowdeck", var correspondance = { tiktak: "tiktak", flowdeck: "flowdeck",
kanbante: "kanbante", og: "obsigate" }; kanbante: "kanbante", og: "obsigate" };
return correspondance[h] || null; return correspondance[h] || null;
@@ -251,7 +261,7 @@
' <span class="d-titre">Discussion principale</span>' + ' <span class="d-titre">Discussion principale</span>' +
' <button type="button" class="d-btn" data-disc="liste">☰ Discussions</button>' + ' <button type="button" class="d-btn" data-disc="liste">☰ Discussions</button>' +
' <button type="button" class="d-btn" data-disc="nouvelle">+ Nouvelle</button>' + ' <button type="button" class="d-btn" data-disc="nouvelle">+ Nouvelle</button>' +
' <button type="button" class="d-btn" data-disc="console" title="Ouvrir la console Spark (agent.dracodev.net) en pleine page par-dessus l\'application, sur cette discussion">Console ⤢</button>' + ' <button type="button" class="d-btn" data-disc="console" title="Ouvrir la console Spark (" + API_HOST + ") en pleine page par-dessus l\'application, sur cette discussion">Console ⤢</button>' +
'</div>' + '</div>' +
'<div class="draco-discussions" hidden></div>' + '<div class="draco-discussions" hidden></div>' +
'<div class="draco-panneau-outils">' + '<div class="draco-panneau-outils">' +
@@ -346,7 +356,7 @@
}) })
.catch(function () { .catch(function () {
majEtat("err", "Service d'agents injoignable"); majEtat("err", "Service d'agents injoignable");
ajouterMsg("sys", "Le service d'agents (agent.dracodev.net) ne répond pas. Vérifie que le conteneur dracodev-agent est démarré."); ajouterMsg("sys", "Le service d'agents (" + API_HOST + ") ne répond pas. Vérifie que le conteneur dracodev-agent est démarré.");
}); });
} }
+124
View File
@@ -0,0 +1,124 @@
"""URLs publiques dracodev par configuration — chantier dev/prod (v7.82.0).
Règle vérifiée : aucun nom public codé en dur dans le code vivant ;
les défauts sont les noms OFFICIELS (jamais de ``-dev`` en défaut) et
chaque environnement déclare ses noms en ``-dev`` par l'environnement
(``DRACO_SSO_URL``, ``DRACO_AGENT_URL``, ``TIKTAK_URL``,
``KANBANTE_URL``, ``FLOWDECK_URL``/``OAUTH_REDIRECT_URI``…).
"""
import pytest
from app.services import draco_urls
VARS = ("DRACO_SSO_URL", "DRACO_AGENT_URL", "TIKTAK_URL", "KANBANTE_URL",
"FLOWDECK_URL", "OAUTH_REDIRECT_URI", "AGENT_EVENEMENTS_URL")
@pytest.fixture(autouse=True)
def _env_propre(monkeypatch):
for v in VARS:
monkeypatch.delenv(v, raising=False)
def test_defauts_officiels_sans_dev():
urls = draco_urls.toutes()
assert urls["obsigate"] == "https://og.dracodev.net"
assert urls["agent"] == "https://agent.dracodev.net"
assert urls["tiktak"] == "https://tiktak.dracodev.net"
assert urls["kanbante"] == "https://kanbante.dracodev.net"
assert urls["flowdeck"] == "https://flowdeck.dracodev.net"
assert all("-dev" not in u for u in urls.values())
def test_surcharge_dev_par_env(monkeypatch):
monkeypatch.setenv("DRACO_SSO_URL", "https://og-dev.dracodev.net")
monkeypatch.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
monkeypatch.setenv("TIKTAK_URL", "https://tiktak-dev.dracodev.net")
monkeypatch.setenv("KANBANTE_URL", "https://kanbante-dev.dracodev.net")
monkeypatch.setenv("FLOWDECK_URL", "https://flowdeck-dev.dracodev.net")
urls = draco_urls.toutes()
assert urls["obsigate"] == "https://og-dev.dracodev.net"
assert urls["agent"] == "https://agent-dev.dracodev.net"
assert urls["tiktak"] == "https://tiktak-dev.dracodev.net"
assert urls["kanbante"] == "https://kanbante-dev.dracodev.net"
assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net"
def test_flowdeck_url_derivée_oauth_redirect(monkeypatch):
monkeypatch.setenv(
"OAUTH_REDIRECT_URI", "https://flowdeck-dev.dracodev.net/auth/callback")
assert draco_urls.flowdeck_url() == "https://flowdeck-dev.dracodev.net"
def test_evenements_url_derivée_agent(monkeypatch):
from app.services import agent_evenements
monkeypatch.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
assert agent_evenements._url() == \
"https://agent-dev.dracodev.net/api/v1/evenements"
monkeypatch.setenv("AGENT_EVENEMENTS_URL", "http://127.0.0.1:9/api/v1/evenements")
assert agent_evenements._url() == "http://127.0.0.1:9/api/v1/evenements"
def test_csp_agent_depuis_config(client, monkeypatch):
monkeypatch.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
r = client.get("/")
csp = r.headers.get("content-security-policy", "")
assert "https://agent-dev.dracodev.net" in csp
assert "connect-src 'self' https://agent-dev.dracodev.net" in csp
assert "frame-src 'self' https://agent-dev.dracodev.net" in csp
assert "https://agent.dracodev.net " not in csp.replace(
"https://agent-dev.dracodev.net", "")
def test_parse_deep_link_dev_et_officiel(monkeypatch):
from app.services.external_refs import parse_deep_link
monkeypatch.setenv("TIKTAK_URL", "https://tiktak-dev.dracodev.net")
monkeypatch.setenv("DRACO_SSO_URL", "https://og-dev.dracodev.net")
# Hôte configuré (dev)…
assert parse_deep_link("https://tiktak-dev.dracodev.net/?task=7")[:3] == \
("tiktak", "task", "7")
assert parse_deep_link("https://og-dev.dracodev.net/#file=P:A.md")[:3] == \
("obsigate", "note", "P:A.md")
# …et hôte officiel toujours reconnu (références déjà stockées).
assert parse_deep_link("https://tiktak.dracodev.net/?task=7")[:3] == \
("tiktak", "task", "7")
assert parse_deep_link("https://og.dracodev.net/#file=P:A.md")[:3] == \
("obsigate", "note", "P:A.md")
def test_url_affichage_reecrit_hote_sans_toucher_stockage(monkeypatch):
from app.services.external_refs import url_affichage
monkeypatch.setenv("TIKTAK_URL", "https://tiktak-dev.dracodev.net")
stockee = "https://tiktak.dracodev.net/?task=42"
assert url_affichage(stockee) == "https://tiktak-dev.dracodev.net/?task=42"
# La fonction est pure : la valeur d'entrée (celle en base) ne bouge pas.
assert stockee == "https://tiktak.dracodev.net/?task=42"
assert url_affichage("https://example.com/x") == "https://example.com/x"
def test_preset_tiktak_depuis_config(monkeypatch):
from app.services import connectors
assert connectors.PRESETS["tiktak"]["url"] == \
"https://tiktak.dracodev.net/mcp"
monkeypatch.setenv("TIKTAK_URL", "https://tiktak-dev.dracodev.net")
assert connectors.PRESETS["tiktak"]["url"] == \
"https://tiktak-dev.dracodev.net/mcp"
def test_bouton_sso_login_depuis_config(monkeypatch):
from app.routers.auth import _sso_login_url
assert _sso_login_url() == (
"https://og.dracodev.net/sso/authorize?return_to="
"https%3A%2F%2Fflowdeck.dracodev.net%2Fauth%2Fdraco")
monkeypatch.setenv("DRACO_SSO_URL", "https://og-dev.dracodev.net")
monkeypatch.setenv(
"OAUTH_REDIRECT_URI", "https://flowdeck-dev.dracodev.net/auth/callback")
assert _sso_login_url() == (
"https://og-dev.dracodev.net/sso/authorize?return_to="
"https%3A%2F%2Fflowdeck-dev.dracodev.net%2Fauth%2Fdraco")