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
+14 -2
View File
@@ -38,7 +38,6 @@ import httpx
log = logging.getLogger("flowdeck.evenements")
SOURCE_APP = "flowdeck"
URL_DEFAUT = "https://agent.dracodev.net/api/v1/evenements"
DELAI_S = 2.0
@@ -47,7 +46,20 @@ def _cle() -> 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:
+48 -14
View File
@@ -28,20 +28,54 @@ CUSTOM_KINDS = ("custom", "discord", "telegram", "mcp", "tiktak")
AUTH_SCHEMES = ("bearer", "bot", "none")
# Presets proposés par le formulaire du menu + (le jeton reste toujours saisi
# par l'utilisateur — jamais codé en dur ici).
PRESETS = {
"discord": {"url": "https://discord.com/api/v10", "auth": "bot",
"hint": "Bot Discord Developers → Token"},
"telegram": {"url": "https://api.telegram.org/bot{secret}", "auth": "none",
"hint": "BotFather → Bot Token (jeton dans l'URL)"},
"mcp": {"url": "https://", "auth": "bearer",
"hint": "URL du endpoint MCP (streamable HTTP)"},
# Fusion ACC-2.1 — première paire de fédération : le serveur MCP en
# lecture de TikTak (POST /mcp, JSON-RPC) ; la clé `tck_` se crée
# dans TikTak → Réglages → Intégrations. Comportement MCP standard
# (probe, cache d'outils, outils dynamiques mcp_tiktak_*).
"tiktak": {"url": "https://tiktak.dracodev.net/mcp", "auth": "bearer",
"hint": "TikTak → Réglages → Intégrations → clé API tck_…"},
}
def _presets() -> dict:
from app.services import draco_urls
return {
"discord": {"url": "https://discord.com/api/v10", "auth": "bot",
"hint": "Bot Discord Developers → Token"},
"telegram": {"url": "https://api.telegram.org/bot{secret}", "auth": "none",
"hint": "BotFather → Bot Token (jeton dans l'URL)"},
"mcp": {"url": "https://", "auth": "bearer",
"hint": "URL du endpoint MCP (streamable HTTP)"},
# Fusion ACC-2.1 — première paire de fédération : le serveur MCP en
# lecture de TikTak (POST /mcp, JSON-RPC) ; la clé `tck_` se crée
# 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
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:
return
_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:
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:
return None
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)
if q.get("task"):
return ("tiktak", "task", q["task"][0], f"TikTak · tâche {q['task'][0]}")
if q.get("list"):
return ("tiktak", "list", q["list"][0], f"TikTak · liste {q['list'][0]}")
return None
if host == "og.dracodev.net":
if host in draco_urls.hotes_connus("obsigate"):
frag = u.fragment or ""
if frag.startswith("file="):
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]
return ("obsigate", "note", cible, f"ObsiGate · {nom}")
return None
if host == "flowdeck.dracodev.net":
if host in draco_urls.hotes_connus("flowdeck"):
m = re.fullmatch(r"/pages/(\d+)", u.path or "")
if m:
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:
return ("flowdeck", "project", m.group(1), f"Flowdeck · projet {m.group(1)}")
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 "")
if m:
owner, repo, num = m.groups()
@@ -67,9 +75,48 @@ def parse_deep_link(url: str) -> tuple[str, str, str, str] | 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:
d = dict(row)
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
+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": ""}
try:
from app.services import draco_urls
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",
}
kwargs = {"timeout": timeout}