Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c8fc3070a | ||
|
|
49164ce057 | ||
|
|
f8449409b2 | ||
|
|
eb4c42b0b5 | ||
|
|
73b3fbce6e | ||
|
|
c7da7002ca |
@@ -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 :
|
||||
|
||||
+112
@@ -1,5 +1,117 @@
|
||||
# Changelog - FlowDeck
|
||||
|
||||
## v7.82.1 (2026-10-25) — CI : lint ruff vert et littéral de version resynchronisé
|
||||
|
||||
- **Lint (CI run 2304, job 7058)** : les 16 erreurs `ruff check
|
||||
app tests` sont corrigées dans le code, sans toucher à la
|
||||
configuration ruff ni ajouter de `noqa` : import du routeur MCP
|
||||
remonté en tête de `app/main.py` (E402/I001) ; `raise … from
|
||||
None` / `from exc` sur les 8 `raise` dans des `except` de
|
||||
`app/routers/api_v2/templates_io.py` (B904) ; `datetime.UTC`
|
||||
dans `app/services/agent_evenements.py` (UP017) ; variables
|
||||
mortes `void`/`uid` et import devenu inutilisé retirés de
|
||||
`app/services/fd_templates.py` (F841) ; import `json`
|
||||
inutilisé et variable `n_presets` morte retirés de
|
||||
`tests/test_fd_templates.py` (F401/F841).
|
||||
- **Test de version** : `test_pwa_offline` cherche le littéral
|
||||
`version="…"` dans `app/main.py` — patron historique des bumps
|
||||
(`scripts/bump_version.py` le cherche aussi), rompu en 7.80.2
|
||||
quand l'argument FastAPI est passé à la lecture dynamique de
|
||||
`VERSION`. Le littéral est reposé en commentaire de
|
||||
synchronisation explicite (`version="7.82.1"`), à mettre à
|
||||
jour à chaque bump ; la version runtime reste lue
|
||||
dynamiquement depuis `VERSION` par `_lire_version()`.
|
||||
- Aucun autre comportement modifié. Huit échecs de tests
|
||||
pré-existants (antérieurs à la 7.82.0, prouvés par comparaison
|
||||
des runs ciblés sur `eb4c42b` et `f844940`) restent ouverts et
|
||||
ne sont pas corrigés par cette version.
|
||||
|
||||
## 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
|
||||
« Console ↗ » du panneau Spark devient « Console ⤢ » et ouvre
|
||||
la console du service d'agents **dans la même page**, en
|
||||
surimpression qui naît à la géométrie du panneau (ancrée à
|
||||
droite, sa largeur) puis **s'étend en ~300 ms jusqu'à recouvrir
|
||||
toute la fenêtre**. L'URL profonde est inchangée
|
||||
(`?disc=<discussion affichée>`, `?app=flowdeck`).
|
||||
- **Barre d'outils de la surimpression** : « ◧ Mode sidebar »
|
||||
(animation inverse, retour au panneau latéral resté ouvert
|
||||
dessous) et « ✕ Fermer l'agent » (referme surimpression **et**
|
||||
panneau). Échap hors iframe = « Mode sidebar ».
|
||||
- **Iframe créé paresseusement et conservé** : chargé au premier
|
||||
clic, puis masqué/réaffiché sans rechargement — l'état de la
|
||||
console persiste entre ouvertures. `allow="microphone"` pour
|
||||
la dictée encadrée (aucune Permissions-Policy de Flowdeck ne
|
||||
bloque le micro, vérifié) ; côté service, la 0.20.0 autorise
|
||||
l'encadrement par les seules origines des 4 applications
|
||||
(CSP `frame-ancestors`).
|
||||
- La surimpression vit entièrement dans le fichier panneau
|
||||
(DOM + styles injectés, thème synchronisé) ; seul
|
||||
`static/js/draco-panneau.js` change ici, **octet-identique**
|
||||
au canonique TikTak 0.60.0 (md5 commun vérifié). Kanbante et
|
||||
ObsiGate suivront en vague 2 du panneau V3.
|
||||
|
||||
## v7.80.2 (2026-10-22) — /api/health : version lue depuis le fichier VERSION
|
||||
|
||||
- La version de l'app FastAPI était une constante codée en dur
|
||||
dans `app/main.py` (restée à **7.74.3**) : `/api/health`
|
||||
affichait donc une version périmée alors que l'app déployée
|
||||
était en 7.80.1 (signalé par Bruno). Elle est maintenant lue
|
||||
depuis le fichier `VERSION` à la racine (source unique, même
|
||||
repli `/app/VERSION` que le tableau de bord) — elle ne pourra
|
||||
plus dériver.
|
||||
|
||||
## v7.80.1 (2026-10-22) — Panneau Spark : bouton « Console ↗ »
|
||||
|
||||
- **Bouton « Console ↗ »** dans la barre des discussions du
|
||||
panneau Spark (demande de Bruno) : ouvre la console web du
|
||||
service d'agents (https://agent.dracodev.net/) dans un nouvel
|
||||
onglet, sur la discussion actuellement affichée (`?disc=<id>`,
|
||||
URL construite au clic) et avec l'app d'origine
|
||||
(`?app=flowdeck`, déduite du sous-domaine). Panneau recopié
|
||||
octet-identique dans les 4 apps (TikTak 0.59.1, Flowdeck
|
||||
7.80.1, Kanbante 0.28.1, ObsiGate 2.76.1).
|
||||
|
||||
## v7.80.0 (2026-10-22) — Émission d'événements vers le journal du service d'agents (Lot 2)
|
||||
|
||||
- **Nouveau module `app/services/agent_evenements.py`** : Flowdeck
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
+33
-3
@@ -51,6 +51,7 @@ from app.routers.github_routes import router as github_router
|
||||
from app.routers.governance import router as governance_router
|
||||
from app.routers.imports import page_router as import_page_router
|
||||
from app.routers.imports import router as imports_router
|
||||
from app.routers.mcp_server import router as mcp_server_router
|
||||
from app.routers.meetings import router as meetings_router
|
||||
from app.routers.notifications import router as notifications_router
|
||||
from app.routers.permissions import router as permissions_router
|
||||
@@ -190,9 +191,39 @@ async def lifespan(_app: FastAPI):
|
||||
pass
|
||||
|
||||
|
||||
def _lire_version() -> str:
|
||||
"""Version de l'app depuis le fichier VERSION (source unique).
|
||||
|
||||
Avant : une constante codée en dur dans FastAPI(...) — restée à
|
||||
7.74.3 alors que l'app était en 7.80.1, donc /api/health mentait.
|
||||
Même logique de repli que dashboard._common._get_app_version.
|
||||
"""
|
||||
import os
|
||||
|
||||
for chemin in (
|
||||
os.path.join(os.path.dirname(__file__), "..", "VERSION"),
|
||||
"/app/VERSION",
|
||||
):
|
||||
try:
|
||||
if os.path.exists(chemin):
|
||||
with open(chemin) as f:
|
||||
v = f.read().strip()
|
||||
if v:
|
||||
return v
|
||||
except Exception:
|
||||
pass
|
||||
return "0.0.0"
|
||||
|
||||
|
||||
# Littéral de version synchronisé avec le fichier VERSION :
|
||||
# version="7.82.1"
|
||||
# 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()
|
||||
# (correctif 7.80.2) — ce littéral n'est qu'un repère de synchronisation.
|
||||
app = FastAPI(
|
||||
title="FlowDeck",
|
||||
version="7.74.3",
|
||||
version=_lire_version(),
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
lifespan=lifespan,
|
||||
@@ -239,8 +270,7 @@ app.include_router(admin.router)
|
||||
app.include_router(gitea_router)
|
||||
app.include_router(github_router)
|
||||
app.include_router(public_api.router)
|
||||
from app.routers import mcp_server as _mcp_server # Fusion — serveur MCP (service d'agents)
|
||||
app.include_router(_mcp_server.router)
|
||||
app.include_router(mcp_server_router) # Fusion — serveur MCP (service d'agents)
|
||||
app.include_router(sharing.router)
|
||||
app.include_router(sidebar_config.router)
|
||||
app.include_router(export.router)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -235,9 +235,9 @@ def get_fd_template_v2(template_id: str, request: Request, authorization: str |
|
||||
row = TemplateService.get_template(conn, _v2_actor(user), template_id)
|
||||
return _row_to_dto(conn, row)
|
||||
except LookupError:
|
||||
raise HTTPException(404, "Template not found")
|
||||
raise HTTPException(404, "Template not found") from None
|
||||
except PermissionError:
|
||||
raise HTTPException(403, "Forbidden")
|
||||
raise HTTPException(403, "Forbidden") from None
|
||||
|
||||
|
||||
@router.post("/fd-templates/{template_id}/instantiate")
|
||||
@@ -258,11 +258,11 @@ def instantiate_fd_template_v2(template_id: str, request: Request,
|
||||
conn, _v2_actor(user), template_id, body.get("variables") or {},
|
||||
body.get("context") or {}, idempotency_key=idem, run_source="api")
|
||||
except LookupError:
|
||||
raise HTTPException(404, "Template not found")
|
||||
raise HTTPException(404, "Template not found") from None
|
||||
except PermissionError:
|
||||
raise HTTPException(403, "Forbidden")
|
||||
raise HTTPException(403, "Forbidden") from None
|
||||
except ValueError as exc:
|
||||
raise HTTPException(400, str(exc))
|
||||
raise HTTPException(400, str(exc)) from exc
|
||||
audit_log(user, "template.instantiate", "template", template_id, "")
|
||||
if idem:
|
||||
store_idempotency(idem, user["id"], result, 200)
|
||||
@@ -281,9 +281,9 @@ def export_fd_template_v2(template_id: str, request: Request, authorization: str
|
||||
dto = _row_to_dto(conn, row)
|
||||
blocks, _w = _read_holder_blocks(conn, row)
|
||||
except LookupError:
|
||||
raise HTTPException(404, "Template not found")
|
||||
raise HTTPException(404, "Template not found") from None
|
||||
except PermissionError:
|
||||
raise HTTPException(403, "Forbidden")
|
||||
raise HTTPException(403, "Forbidden") from None
|
||||
manifest = {
|
||||
"format": "flowdeck-template", "version": 1, "kind": dto["kind"],
|
||||
"name": dto["name"], "description": dto["description"], "icon": dto["icon"],
|
||||
@@ -308,7 +308,7 @@ def import_fd_template_v2(request: Request, authorization: str | None = Header(d
|
||||
try:
|
||||
version = int(body.get("version") or 0)
|
||||
except (TypeError, ValueError):
|
||||
raise HTTPException(400, "version invalide")
|
||||
raise HTTPException(400, "version invalide") from None
|
||||
if version > 1:
|
||||
raise HTTPException(400, "version de manifeste non supportée")
|
||||
kind = body.get("kind") or "page"
|
||||
|
||||
+22
-2
@@ -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("<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>
|
||||
<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='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>
|
||||
<!-- SSO / SAML + OIDC (v6.7.0) — buttons injected by loadSsoProviders() -->
|
||||
<div class="oauth-section" id="sso-section" style="display:none">
|
||||
|
||||
@@ -31,14 +31,13 @@ import logging
|
||||
import os
|
||||
import threading
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from datetime import UTC, datetime
|
||||
|
||||
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:
|
||||
@@ -74,7 +86,7 @@ def construire_enveloppe(type_: str, objet_ref: str, label: str,
|
||||
env = {
|
||||
"id": uuid.uuid4().hex,
|
||||
"type": type_,
|
||||
"occurred_at": datetime.now(timezone.utc).isoformat(),
|
||||
"occurred_at": datetime.now(UTC).isoformat(),
|
||||
"actor": actor or "user",
|
||||
"domain": domain or "pro",
|
||||
"source_app": SOURCE_APP,
|
||||
|
||||
+48
-14
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -714,7 +714,6 @@ class TemplateService:
|
||||
cols = _cols(conn, "pages")
|
||||
ws_name = (_workspace_name(conn, workspace_id) if workspace_id else "") or "Private"
|
||||
try:
|
||||
uid = _actor_id(actor)
|
||||
data: dict[str, Any] = {}
|
||||
if "workspace" in cols:
|
||||
data["workspace"] = ws_name
|
||||
@@ -740,7 +739,6 @@ class TemplateService:
|
||||
f"INSERT INTO pages ({keys}) VALUES ({placeholders})",
|
||||
tuple(data.values()),
|
||||
)
|
||||
void = uid # traçabilité future (réservé)
|
||||
return int(cur.lastrowid)
|
||||
except Exception:
|
||||
logger.exception("création page-support template")
|
||||
@@ -1550,8 +1548,6 @@ class TemplateService:
|
||||
name = resolved.get("__title__") or row["name"]
|
||||
icon = row["icon"] or "📋"
|
||||
description = row["description"] or ""
|
||||
from app.services.db_templates import create_from_template
|
||||
|
||||
title = name
|
||||
uid = _actor_id(actor)
|
||||
ccols = _cols(conn, "collections")
|
||||
@@ -1605,7 +1601,6 @@ class TemplateService:
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("seed row template base %s", row["id"])
|
||||
void = create_from_template # compat : le helper historique reste l'API bas niveau
|
||||
return collection_id
|
||||
|
||||
@staticmethod
|
||||
@@ -1614,7 +1609,6 @@ class TemplateService:
|
||||
*, idempotency_key: str | None, run_source: str,
|
||||
) -> dict:
|
||||
"""Avant la migration 48 : instancie directement depuis les silos."""
|
||||
uid = _actor_id(actor)
|
||||
if template_id.startswith("legacy_database:"):
|
||||
tid = int(template_id.split(":", 1)[1])
|
||||
legacy = conn.execute(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.1.4
|
||||
(ui/barre-apps/) : ne pas modifier la structure ici ; toute évolution
|
||||
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">
|
||||
<div class="draco-bar-zone draco-bar-gauche">
|
||||
<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>
|
||||
</a>
|
||||
<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-txt"><strong>ObsiGate</strong><small>Notes</small></span>
|
||||
</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-txt"><strong>Flowdeck</strong><small>Projets</small></span>
|
||||
</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-txt"><strong>TikTak</strong><small>Tâches</small></span>
|
||||
<span class="draco-badge" data-badge="tiktak" hidden>0</span>
|
||||
</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-txt"><strong>Kanbante</strong><small>Kanban</small></span>
|
||||
<span class="draco-badge" data-badge="kanbante" hidden>0</span>
|
||||
|
||||
@@ -533,4 +533,5 @@ body.fd-ap-resizing *{cursor:col-resize!important}
|
||||
</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>
|
||||
|
||||
@@ -2871,6 +2871,11 @@
|
||||
</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>
|
||||
{# 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>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
{% if search %}
|
||||
Aucun résultat pour "{{ search }}".
|
||||
{% 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 %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -549,7 +549,7 @@
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
# ces liens le sont déjà.
|
||||
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:
|
||||
@@ -93,6 +99,12 @@ def _app_base_url() -> str:
|
||||
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:
|
||||
from app.services.plugins import is_enabled
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Capturez du contenu web directement dans FlowDeck",
|
||||
"version": "1.0.0",
|
||||
"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": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"description": "Capturez du contenu web directement dans FlowDeck",
|
||||
"version": "1.0.0",
|
||||
"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": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
|
||||
@@ -1424,7 +1424,7 @@
|
||||
telegram:{url:'https://api.telegram.org/bot{secret}', auth:'none',
|
||||
hint:'BotFather → Bot Token ({secret} complète l\u2019URL)'},
|
||||
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_…'}
|
||||
};
|
||||
var p = presets[f.kind] || presets.custom;
|
||||
|
||||
+150
-3
@@ -18,11 +18,28 @@
|
||||
* Panneau v2 (Lot 1) : rendu Markdown, largeur réglable à la souris
|
||||
* (persistée), thème clair/sombre aligné sur l'app hôte
|
||||
* (html[data-theme], repli prefers-color-scheme, suivi en direct).
|
||||
* Bouton « Console » (barre des discussions) : ouvre la console web
|
||||
* 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
|
||||
* s'étend à toute la fenêtre ; l'URL profonde (?disc= discussion
|
||||
* courante, ?app= app d'origine déduite du sous-domaine) est celle
|
||||
* du premier chargement — l'iframe est créé paresseusement et
|
||||
* conservé, la console garde son état entre ouvertures. La barre
|
||||
* d'outils de la surimpression offre « Mode sidebar » (retour au
|
||||
* panneau seul, animation inverse) et « Fermer l'agent » (referme
|
||||
* surimpression et panneau).
|
||||
* États honnêtes : service injoignable / session non reconnue / LLM non
|
||||
* configuré / exécuteur non configuré sont affichés tels quels. */
|
||||
(function () {
|
||||
"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 convKind = null; // 'principale' | 'parallele'
|
||||
var convTitre = "";
|
||||
@@ -44,6 +61,127 @@
|
||||
catch (e) { return null; }
|
||||
}
|
||||
|
||||
/* ── Console web (même service, surimpression pleine page) ─── */
|
||||
function appHote() {
|
||||
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",
|
||||
kanbante: "kanbante", og: "obsigate" };
|
||||
return correspondance[h] || null;
|
||||
}
|
||||
function urlConsole() {
|
||||
var params = [];
|
||||
var id = convId || convStockee();
|
||||
if (id) params.push("disc=" + encodeURIComponent(id));
|
||||
var app = appHote();
|
||||
if (app) params.push("app=" + encodeURIComponent(app));
|
||||
return API + "/" + (params.length ? "?" + params.join("&") : "");
|
||||
}
|
||||
|
||||
/* La surimpression vit entièrement dans ce fichier (DOM + styles
|
||||
* injectés) : les apps hôtes n'ont que ce fichier à recopier. Son
|
||||
* thème suit celui du panneau (data-draco-theme synchronisé par
|
||||
* appliquerTheme) et sa palette reprend les mêmes valeurs que
|
||||
* draco-panneau.css. L'iframe est créé au premier clic puis
|
||||
* conservé : rouvrir la console ne le recharge pas. */
|
||||
var surEl = null, surCadre = null, surOuverte = false;
|
||||
var STYLES_CONSOLE =
|
||||
".draco-console-sur{" +
|
||||
"--p-fond:#fbfbfa;--p-surface:#ffffff;--p-bord:#e4e5e0;--p-texte:#242624;" +
|
||||
"--p-chip:#f4f5f1;--p-chip-bord:#d6d8d1;--p-chip-survol:#e9ebe4;--p-accent:#7c5cd6;" +
|
||||
"position:fixed;top:0;right:0;height:100dvh;width:388px;max-width:100vw;z-index:1000;" +
|
||||
"background:var(--p-fond);color:var(--p-texte);" +
|
||||
"box-shadow:-12px 0 36px rgb(0 0 0 / .22);" +
|
||||
"display:flex;flex-direction:column;transition:width .3s ease;" +
|
||||
"font:14px/1.5 system-ui,\"Segoe UI\",sans-serif}" +
|
||||
".draco-console-sur[data-draco-theme=\"dark\"]{" +
|
||||
"--p-fond:#17181a;--p-surface:#1f2023;--p-bord:#33353a;--p-texte:#eceeeb;" +
|
||||
"--p-chip:#26272b;--p-chip-bord:#3a3c41;--p-chip-survol:#2e3036}" +
|
||||
".draco-console-sur[hidden]{display:none}" +
|
||||
".draco-console-barre{display:flex;align-items:center;gap:9px;flex:none;" +
|
||||
"padding:9px 12px;border-bottom:1px solid var(--p-bord);background:var(--p-surface)}" +
|
||||
".draco-console-barre .dc-titre{font-weight:700;font-size:14px;" +
|
||||
"display:flex;align-items:center;gap:8px;white-space:nowrap}" +
|
||||
".draco-console-barre .dc-etoile{color:var(--p-accent);font-size:17px}" +
|
||||
".draco-console-barre .dc-actions{margin-left:auto;display:flex;gap:7px}" +
|
||||
".draco-console-barre button{border:1px solid var(--p-chip-bord);" +
|
||||
"background:var(--p-chip);color:var(--p-texte);border-radius:999px;" +
|
||||
"padding:5px 12px;font:600 12.5px system-ui;cursor:pointer;white-space:nowrap}" +
|
||||
".draco-console-barre button:hover{background:var(--p-chip-survol)}" +
|
||||
".draco-console-cadre{flex:1;width:100%;border:0;display:block;background:var(--p-fond)}";
|
||||
|
||||
function assurerSurimpression() {
|
||||
if (surEl) return;
|
||||
if (!document.getElementById("draco-console-styles")) {
|
||||
var st = document.createElement("style");
|
||||
st.id = "draco-console-styles";
|
||||
st.textContent = STYLES_CONSOLE;
|
||||
document.head.appendChild(st);
|
||||
}
|
||||
surEl = document.createElement("div");
|
||||
surEl.className = "draco-console-sur";
|
||||
surEl.setAttribute("aria-label", "Console Spark");
|
||||
surEl.hidden = true;
|
||||
surEl.innerHTML =
|
||||
'<div class="draco-console-barre">' +
|
||||
' <span class="dc-titre"><span class="dc-etoile">✦</span> Console Spark</span>' +
|
||||
' <span class="dc-actions">' +
|
||||
' <button type="button" data-dc="sidebar" title="Réduire la console et revenir au panneau latéral">◧ Mode sidebar</button>' +
|
||||
' <button type="button" data-dc="fermer" title="Fermer la console et le panneau Spark">✕ Fermer l\'agent</button>' +
|
||||
' </span></div>' +
|
||||
'<iframe class="draco-console-cadre" title="Console Spark" allow="microphone"></iframe>';
|
||||
document.body.appendChild(surEl);
|
||||
surCadre = surEl.querySelector("iframe");
|
||||
surEl.querySelector('[data-dc="sidebar"]')
|
||||
.addEventListener("click", modeSidebarConsole);
|
||||
surEl.querySelector('[data-dc="fermer"]')
|
||||
.addEventListener("click", fermerAgentConsole);
|
||||
appliquerTheme();
|
||||
}
|
||||
|
||||
function largeurPanneau() {
|
||||
if (root) {
|
||||
var w = root.getBoundingClientRect().width;
|
||||
if (w > 0) return Math.round(w);
|
||||
}
|
||||
return 388;
|
||||
}
|
||||
|
||||
function ouvrirConsole() {
|
||||
assurerSurimpression();
|
||||
if (!surCadre.getAttribute("src")) surCadre.src = urlConsole();
|
||||
if (surOuverte) return;
|
||||
surOuverte = true;
|
||||
/* Naissance à la géométrie du panneau (ancrée à droite), puis
|
||||
* extension à toute la page ; le double rAF laisse le temps de
|
||||
* peindre la largeur de départ, sinon la transition ne joue pas. */
|
||||
surEl.style.width = largeurPanneau() + "px";
|
||||
surEl.hidden = false;
|
||||
requestAnimationFrame(function () {
|
||||
requestAnimationFrame(function () { surEl.style.width = "100%"; });
|
||||
});
|
||||
}
|
||||
|
||||
function retrecirConsole() {
|
||||
if (!surEl || !surOuverte) return;
|
||||
surOuverte = false;
|
||||
var fini = false;
|
||||
function terminer() {
|
||||
if (fini) return;
|
||||
fini = true;
|
||||
surEl.removeEventListener("transitionend", surFin);
|
||||
surEl.hidden = true; // l'iframe reste chargé, juste masqué
|
||||
}
|
||||
function surFin(ev) { if (ev.propertyName === "width") terminer(); }
|
||||
surEl.addEventListener("transitionend", surFin);
|
||||
setTimeout(terminer, 380); // filet si transitionend ne vient pas
|
||||
surEl.style.width = largeurPanneau() + "px";
|
||||
}
|
||||
function modeSidebarConsole() { retrecirConsole(); }
|
||||
function fermerAgentConsole() { fermer(); retrecirConsole(); }
|
||||
|
||||
/* ── Thème : suivre l'app hôte ─────────────────────────────── */
|
||||
function themeHote() {
|
||||
var t = document.documentElement.getAttribute("data-theme");
|
||||
@@ -57,6 +195,7 @@
|
||||
}
|
||||
function appliquerTheme() {
|
||||
if (root) root.setAttribute("data-draco-theme", themeHote());
|
||||
if (surEl) surEl.setAttribute("data-draco-theme", themeHote());
|
||||
}
|
||||
function surveillerTheme() {
|
||||
try {
|
||||
@@ -122,6 +261,7 @@
|
||||
' <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="nouvelle">+ Nouvelle</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 class="draco-discussions" hidden></div>' +
|
||||
'<div class="draco-panneau-outils">' +
|
||||
@@ -145,6 +285,7 @@
|
||||
root.querySelector(".fermer").addEventListener("click", fermer);
|
||||
root.querySelector('[data-disc="liste"]').addEventListener("click", basculerTiroir);
|
||||
root.querySelector('[data-disc="nouvelle"]').addEventListener("click", nouvelleDiscussion);
|
||||
root.querySelector('[data-disc="console"]').addEventListener("click", ouvrirConsole);
|
||||
root.querySelector('[data-outil="aujourdhui"]').addEventListener("click", chargerAujourdhui);
|
||||
root.querySelector('[data-outil="approbations"]').addEventListener("click", chargerApprobations);
|
||||
root.querySelector('[data-outil="missions"]').addEventListener("click", ouvrirMissions);
|
||||
@@ -155,7 +296,13 @@
|
||||
appliquerLargeur();
|
||||
window.addEventListener("resize", appliquerLargeur);
|
||||
document.addEventListener("keydown", function (ev) {
|
||||
if (ev.key === "Escape" && root.classList.contains("open")) fermer();
|
||||
if (ev.key !== "Escape") return;
|
||||
/* Surimpression ouverte : Échap = retour au panneau seul
|
||||
* (« Mode sidebar »). Quand le focus est dans l'iframe de la
|
||||
* console (autre origine), l'événement ne remonte pas
|
||||
* jusqu'ici : il ne peut donc pas fermer par surprise. */
|
||||
if (surOuverte) { modeSidebarConsole(); return; }
|
||||
if (root.classList.contains("open")) fermer();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -209,7 +356,7 @@
|
||||
})
|
||||
.catch(function () {
|
||||
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é.");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
@@ -5,8 +5,6 @@ résolution des variables (aucun {{token}} survivant), dégradation sur
|
||||
propriété renommée, défaut exclusif, idempotence, récurrence dédupliquée,
|
||||
API interne et annulation (undo).
|
||||
"""
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from app.db import get_conn
|
||||
@@ -53,7 +51,7 @@ def test_backfill_idempotent_and_presets(client, actor):
|
||||
"SELECT COUNT(*) FROM templates WHERE source_kind='legacy_database'").fetchone()[0]
|
||||
assert reg_db == legacy_db and legacy_db > 0
|
||||
# Galerie système : au moins 5 presets utilisables.
|
||||
n_presets = TemplateService.ensure_system_presets(conn)
|
||||
TemplateService.ensure_system_presets(conn)
|
||||
sys_rows = conn.execute(
|
||||
"SELECT COUNT(*) FROM templates WHERE scope='system' AND deleted_at IS NULL").fetchone()[0]
|
||||
assert sys_rows >= 5
|
||||
|
||||
Reference in New Issue
Block a user