feat: URLs publiques de l'écosystème par configuration — dev/prod sans noms en dur #223
This commit is contained in:
@@ -104,3 +104,28 @@ DEEPSEEK_MODEL=deepseek-chat
|
|||||||
# OBSIGATE_GITEA_URL=https://git.example.net
|
# OBSIGATE_GITEA_URL=https://git.example.net
|
||||||
# OBSIGATE_GITEA_TOKEN=
|
# OBSIGATE_GITEA_TOKEN=
|
||||||
# OBSIGATE_GITHUB_TOKEN=
|
# OBSIGATE_GITHUB_TOKEN=
|
||||||
|
|
||||||
|
# ── Fusion / dev-prod — URL publiques de l'écosystème (#223) ──
|
||||||
|
# Aucun nom public n'est figé dans le code : ces variables portent les
|
||||||
|
# URL des autres applications. Valeurs par défaut (si absentes) = noms
|
||||||
|
# OFFICIELS de la production. L'environnement dev déclare ses noms en
|
||||||
|
# -dev dans son propre .env uniquement, par exemple :
|
||||||
|
# DRACO_TIKTAK_URL=https://tiktak-dev.dracodev.net
|
||||||
|
# DRACO_AGENT_URL=https://agent.dracodev.net
|
||||||
|
# DRACO_OBSIGATE_URL=https://og.dracodev.net
|
||||||
|
# DRACO_FLOWDECK_URL=https://flowdeck.dracodev.net
|
||||||
|
# DRACO_TIKTAK_URL=https://tiktak.dracodev.net
|
||||||
|
# DRACO_KANBANTE_URL=https://kanbante.dracodev.net
|
||||||
|
# DRACO_GITEA_URL=https://git.dracodev.net
|
||||||
|
|
||||||
|
# ── Fusion — émission d'événements vers le service d'agents (#220) ──
|
||||||
|
# URL complète de réception ; à défaut, dérivée de DRACO_AGENT_URL.
|
||||||
|
# AGENT_EVENEMENTS_URL=https://agent.dracodev.net/api/v1/evenements
|
||||||
|
# AGENT_EVENEMENTS_CLE=<clé propre à ObsiGate, jamais commitée>
|
||||||
|
# AGENT_EVENEMENTS_DOMAINES={"NomDuVault": "famille"}
|
||||||
|
|
||||||
|
# ── Fusion — émettrice SSO draco_session (#216/#217) ──
|
||||||
|
# DRACO_SESSION_SECRET=<secret partagé, jamais commité>
|
||||||
|
# DRACO_SESSION_SECRET_PREV=<ancien secret, fenêtre de rotation>
|
||||||
|
# DRACO_SESSION_COOKIE_DOMAIN=.dracodev.net
|
||||||
|
# DRACO_SSO_MACHINE_KEY=<clé machine des révocations, jamais commitée>
|
||||||
|
|||||||
+23
-1
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
|||||||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
> **En cours de développement** : les changements à venir sont listés dans la section
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version livrée est **2.77.1**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.78.0**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,28 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [2.78.0] — 2026-10-25
|
||||||
|
|
||||||
|
### Modifié
|
||||||
|
|
||||||
|
- **Dev/prod sans noms publics en dur (#223, Fusion)** — toutes les URL
|
||||||
|
publiques de l'écosystème dracodev viennent désormais de la
|
||||||
|
configuration (`backend/draco_config.py` : `DRACO_AGENT_URL`,
|
||||||
|
`DRACO_OBSIGATE_URL`, `DRACO_FLOWDECK_URL`, `DRACO_TIKTAK_URL`,
|
||||||
|
`DRACO_KANBANTE_URL`, `DRACO_GITEA_URL` ; défauts = noms officiels,
|
||||||
|
l'environnement dev déclare ses noms en `-dev` dans son seul `.env`).
|
||||||
|
La CSP (`connect-src`/`frame-src`) dérive l'origine de l'agent de la
|
||||||
|
config (à défaut, de l'origine d'`AGENT_EVENEMENTS_URL`) ; le backend
|
||||||
|
injecte `window.DRACO_URLS` dans `index.html` pour le panneau Spark,
|
||||||
|
la barre d'apps et les liens inter-apps ; les références croisées
|
||||||
|
reconnaissent les hôtes officiels et configurés et leur URL
|
||||||
|
d'affichage est reconstruite depuis la config à la lecture (aucune
|
||||||
|
donnée stockée migrée). WebAuthn confirmé dérivé de la requête /
|
||||||
|
`OBSIGATE_WEBAUTHN_*` (aucun changement). Redéploiement Docker
|
||||||
|
requis ; en dev, déclarer les noms en `-dev` dans le `.env` du poste.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [2.77.1] — 2026-10-24
|
## [2.77.1] — 2026-10-24
|
||||||
|
|
||||||
### Corrigé
|
### Corrigé
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -977,8 +977,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.77.1).
|
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.78.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.77.1 | Dernière mise à jour : Septembre 2026*
|
*Projet : ObsiGate | Version : 2.78.0 | Dernière mise à jour : Septembre 2026*
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](https://www.python.org/)
|
||||||
@@ -1152,8 +1152,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
|
|||||||
|
|
||||||
## 📝 Changelog
|
## 📝 Changelog
|
||||||
|
|
||||||
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.77.1).
|
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.78.0).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 2.77.1 | Last updated: September 2026*
|
*Project: ObsiGate | Version: 2.78.0 | Last updated: September 2026*
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ import httpx
|
|||||||
logger = logging.getLogger("obsigate.agent_journal")
|
logger = logging.getLogger("obsigate.agent_journal")
|
||||||
|
|
||||||
SOURCE_APP = "obsigate"
|
SOURCE_APP = "obsigate"
|
||||||
|
# Défaut officiel, conservé pour référence : depuis #223, ``_url()``
|
||||||
|
# passe par la config centralisée (``backend.draco_config``) — jamais
|
||||||
|
# un nom d'environnement particulier.
|
||||||
URL_DEFAUT = "https://agent.dracodev.net/api/v1/evenements"
|
URL_DEFAUT = "https://agent.dracodev.net/api/v1/evenements"
|
||||||
DELAI_S = 2.0
|
DELAI_S = 2.0
|
||||||
|
|
||||||
@@ -72,7 +75,15 @@ def _cle() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _url() -> str:
|
def _url() -> str:
|
||||||
return os.environ.get("AGENT_EVENEMENTS_URL", "").strip() or URL_DEFAUT
|
# #223 : AGENT_EVENEMENTS_URL d'abord ; à défaut, l'URL du service
|
||||||
|
# configurée (DRACO_AGENT_URL) complétée du chemin d'événements ;
|
||||||
|
# à défaut, le défaut officiel.
|
||||||
|
explicite = os.environ.get("AGENT_EVENEMENTS_URL", "").strip()
|
||||||
|
if explicite:
|
||||||
|
return explicite
|
||||||
|
from backend.draco_config import agent_url
|
||||||
|
|
||||||
|
return f"{agent_url()}/api/v1/evenements"
|
||||||
|
|
||||||
|
|
||||||
def actif() -> bool:
|
def actif() -> bool:
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
"""URLs publiques de l'écosystème dracodev — par configuration (#223).
|
||||||
|
|
||||||
|
Règle dev/prod (Bruno, 2026-10-25) : **aucun nom public de
|
||||||
|
l'écosystème n'est figé dans le code vivant**. Chaque URL publique
|
||||||
|
vient de l'environnement ; les valeurs par défaut sont les noms
|
||||||
|
**officiels** (sans suffixe) de la future production. L'environnement
|
||||||
|
dev déclare ses noms en ``-dev`` dans son seul ``.env`` — le code
|
||||||
|
n'en connaît aucun.
|
||||||
|
|
||||||
|
Variables (voir ``.env.example``) :
|
||||||
|
|
||||||
|
- ``DRACO_AGENT_URL`` — base publique du service d'agents ;
|
||||||
|
à défaut, l'origine est **dérivée** de ``AGENT_EVENEMENTS_URL``
|
||||||
|
(la configuration réelle de l'émission d'événements, #220), puis
|
||||||
|
le défaut officiel.
|
||||||
|
- ``DRACO_OBSIGATE_URL`` — base publique d'ObsiGate (liens profonds).
|
||||||
|
- ``DRACO_FLOWDECK_URL`` — base publique de Flowdeck.
|
||||||
|
- ``DRACO_TIKTAK_URL`` — base publique de TikTak.
|
||||||
|
- ``DRACO_KANBANTE_URL`` — base publique de Kanbante.
|
||||||
|
- ``DRACO_GITEA_URL`` — base publique de Gitea (les cartes
|
||||||
|
Kanbante *sont* des issues Gitea, contrat liens-profonds) ;
|
||||||
|
distincte de ``OBSIGATE_GITEA_URL`` (source connectée de
|
||||||
|
l'assistant, réglable depuis l'UI, pas une URL d'affichage).
|
||||||
|
|
||||||
|
Ce module ne lit l'environnement qu'à l'appel (jamais à l'import) :
|
||||||
|
les tests et un changement de ``.env`` suivi d'un redémarrage
|
||||||
|
prennent effet sans état figé.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
# Défauts officiels (production future) — jamais de nom en ``-dev`` ici.
|
||||||
|
DEFAUTS: dict[str, str] = {
|
||||||
|
"agent": "https://agent.dracodev.net",
|
||||||
|
"obsigate": "https://og.dracodev.net",
|
||||||
|
"flowdeck": "https://flowdeck.dracodev.net",
|
||||||
|
"tiktak": "https://tiktak.dracodev.net",
|
||||||
|
"kanbante": "https://kanbante.dracodev.net",
|
||||||
|
"gitea": "https://git.dracodev.net",
|
||||||
|
}
|
||||||
|
|
||||||
|
_VARIABLES: dict[str, str] = {
|
||||||
|
"agent": "DRACO_AGENT_URL",
|
||||||
|
"obsigate": "DRACO_OBSIGATE_URL",
|
||||||
|
"flowdeck": "DRACO_FLOWDECK_URL",
|
||||||
|
"tiktak": "DRACO_TIKTAK_URL",
|
||||||
|
"kanbante": "DRACO_KANBANTE_URL",
|
||||||
|
"gitea": "DRACO_GITEA_URL",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _nettoyer(valeur: str) -> str:
|
||||||
|
return valeur.strip().rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def origine(url: str) -> str:
|
||||||
|
"""Origine ``schéma://hôte[:port]`` d'une URL (``""`` si illisible)."""
|
||||||
|
u = urlparse(url or "")
|
||||||
|
if u.scheme and u.netloc:
|
||||||
|
return f"{u.scheme}://{u.netloc}"
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def agent_url() -> str:
|
||||||
|
"""Base publique du service d'agents.
|
||||||
|
|
||||||
|
Priorité : ``DRACO_AGENT_URL``, puis l'origine de
|
||||||
|
``AGENT_EVENEMENTS_URL`` (déjà la configuration réelle du poste),
|
||||||
|
puis le défaut officiel.
|
||||||
|
"""
|
||||||
|
explicite = _nettoyer(os.environ.get(_VARIABLES["agent"], ""))
|
||||||
|
if explicite:
|
||||||
|
return explicite
|
||||||
|
evenements = _nettoyer(os.environ.get("AGENT_EVENEMENTS_URL", ""))
|
||||||
|
if evenements:
|
||||||
|
derivee = origine(evenements)
|
||||||
|
if derivee:
|
||||||
|
return derivee
|
||||||
|
return DEFAUTS["agent"]
|
||||||
|
|
||||||
|
|
||||||
|
def agent_origine() -> str:
|
||||||
|
"""Origine du service d'agents, pour la CSP (``connect-src``/``frame-src``)."""
|
||||||
|
return origine(agent_url()) or DEFAUTS["agent"]
|
||||||
|
|
||||||
|
|
||||||
|
def app_url(app: str) -> str:
|
||||||
|
"""Base publique d'une application de l'écosystème (sans ``/`` final)."""
|
||||||
|
if app == "agent":
|
||||||
|
return agent_url()
|
||||||
|
variable = _VARIABLES[app]
|
||||||
|
return _nettoyer(os.environ.get(variable, "")) or DEFAUTS[app]
|
||||||
|
|
||||||
|
|
||||||
|
def app_hote(app: str) -> str:
|
||||||
|
"""Nom d'hôte (minuscules) de l'URL publique configurée de *app*."""
|
||||||
|
return (urlparse(app_url(app)).hostname or "").lower()
|
||||||
|
|
||||||
|
|
||||||
|
def hotes_reconnus() -> dict[str, str]:
|
||||||
|
"""``{hôte: app}`` pour le parseur de liens profonds.
|
||||||
|
|
||||||
|
Contient **toujours** les hôtes officiels (un lien collé depuis la
|
||||||
|
production reste reconnaissable en dev, et réciproquement) **plus**
|
||||||
|
l'hôte configuré de chaque application (les noms en ``-dev`` du
|
||||||
|
poste quand ils sont déclarés dans son ``.env``).
|
||||||
|
"""
|
||||||
|
hotes: dict[str, str] = {}
|
||||||
|
for app, defaut in DEFAUTS.items():
|
||||||
|
hote = (urlparse(defaut).hostname or "").lower()
|
||||||
|
if hote:
|
||||||
|
hotes[hote] = app
|
||||||
|
for app in DEFAUTS:
|
||||||
|
hote = app_hote(app)
|
||||||
|
if hote:
|
||||||
|
hotes[hote] = app
|
||||||
|
return hotes
|
||||||
|
|
||||||
|
|
||||||
|
def urls_publiques() -> dict[str, str]:
|
||||||
|
"""Carte des URL publiques injectée à l'hôte web (``window.DRACO_URLS``).
|
||||||
|
|
||||||
|
C'est le contrat d'injection de dracodev-commons : l'application
|
||||||
|
hôte (ici ObsiGate, qui sert ``index.html``) fournit les URL aux
|
||||||
|
assets partagés (panneau Spark, barre, liens) — les assets ne
|
||||||
|
portent que les défauts officiels en repli.
|
||||||
|
"""
|
||||||
|
return {app: app_url(app) for app in DEFAUTS}
|
||||||
+76
-10
@@ -24,7 +24,7 @@ import uuid
|
|||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import parse_qs, unquote, urlparse
|
from urllib.parse import parse_qs, quote, unquote, urlparse
|
||||||
|
|
||||||
logger = logging.getLogger("obsigate.external_refs")
|
logger = logging.getLogger("obsigate.external_refs")
|
||||||
|
|
||||||
@@ -64,26 +64,87 @@ def _write_refs(username: str, refs: list[dict[str, Any]]) -> None:
|
|||||||
|
|
||||||
# ------------------------------------------------------------ parseur d'URL
|
# ------------------------------------------------------------ parseur d'URL
|
||||||
|
|
||||||
|
def build_deep_link(target_app: str, target_type: str, target_id: str) -> str | None:
|
||||||
|
"""Construit l'URL profonde d'affichage depuis la **configuration**
|
||||||
|
courante (#223), ou ``None`` si la forme est inconnue.
|
||||||
|
|
||||||
|
Les références stockées portent une URL absolue figée au moment du
|
||||||
|
collage (voir ``add_ref``) : elle peut nommer l'autre environnement
|
||||||
|
(un lien collé en prod, lu en dev). L'affichage ne l'utilise donc
|
||||||
|
jamais telle quelle — il reconstruit l'URL depuis
|
||||||
|
``(target_app, target_type, target_id)`` et les bases publiques
|
||||||
|
configurées (``backend.draco_config``), au moment de la lecture.
|
||||||
|
Aucune donnée stockée n'est migrée ni réécrite.
|
||||||
|
"""
|
||||||
|
from backend.draco_config import app_url
|
||||||
|
|
||||||
|
if target_app == "tiktak":
|
||||||
|
if target_type == "task":
|
||||||
|
return f"{app_url('tiktak')}/?task={quote(target_id, safe='')}"
|
||||||
|
if target_type == "list":
|
||||||
|
return f"{app_url('tiktak')}/?list={quote(target_id, safe='')}"
|
||||||
|
return None
|
||||||
|
if target_app == "obsigate" and target_type == "note":
|
||||||
|
return f"{app_url('obsigate')}/#file={quote(target_id, safe=':')}"
|
||||||
|
if target_app == "flowdeck":
|
||||||
|
if target_type == "page":
|
||||||
|
return f"{app_url('flowdeck')}/pages/{quote(target_id, safe='')}"
|
||||||
|
if target_type == "project":
|
||||||
|
return f"{app_url('flowdeck')}/collections/{quote(target_id, safe='')}"
|
||||||
|
return None
|
||||||
|
if target_app == "kanbante" and target_type == "card":
|
||||||
|
m = re.fullmatch(r"([^/]+)/([^/#]+)#(\d+)", target_id or "")
|
||||||
|
if m:
|
||||||
|
owner, repo, num = m.groups()
|
||||||
|
return f"{app_url('gitea')}/{owner}/{repo}/issues/{num}"
|
||||||
|
return None
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _avec_url_affichee(ref: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Copie de la référence dont ``target_url`` est reconstruite
|
||||||
|
depuis la config courante (#223) — le stockage n'est pas touché."""
|
||||||
|
construite = build_deep_link(
|
||||||
|
str(ref.get("target_app") or ""),
|
||||||
|
str(ref.get("target_type") or ""),
|
||||||
|
str(ref.get("target_id") or ""),
|
||||||
|
)
|
||||||
|
if construite is None:
|
||||||
|
return ref
|
||||||
|
copie = dict(ref)
|
||||||
|
copie["target_url"] = construite
|
||||||
|
return copie
|
||||||
|
|
||||||
|
|
||||||
def parse_deep_link(url: str) -> tuple[str, str, str, str] | None:
|
def parse_deep_link(url: str) -> tuple[str, str, str, str] | None:
|
||||||
"""Reconnaît une URL profonde des applications dracodev.
|
"""Reconnaît une URL profonde des applications dracodev.
|
||||||
|
|
||||||
Formes figées par ``dracodev-commons/contrats/liens-profonds.md``.
|
Formes figées par ``dracodev-commons/contrats/liens-profonds.md``.
|
||||||
Retourne ``(target_app, target_type, target_id, label_par_défaut)``
|
Retourne ``(target_app, target_type, target_id, label_par_défaut)``
|
||||||
ou ``None`` si l'URL ne correspond à aucune forme du contrat.
|
ou ``None`` si l'URL ne correspond à aucune forme du contrat.
|
||||||
|
|
||||||
|
Hôtes reconnus (#223) : les hôtes **officiels** et les hôtes
|
||||||
|
**configurés** de chaque application (``backend.draco_config``) —
|
||||||
|
un lien collé depuis l'environnement courant (noms en ``-dev``
|
||||||
|
déclarés dans son ``.env``) comme depuis l'autre environnement
|
||||||
|
est donc reconnu ; aucun nom d'environnement n'est figé ici.
|
||||||
"""
|
"""
|
||||||
|
from backend.draco_config import hotes_reconnus
|
||||||
|
|
||||||
try:
|
try:
|
||||||
u = urlparse((url or "").strip())
|
u = urlparse((url or "").strip())
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
host = (u.hostname or "").lower()
|
host = (u.hostname or "").lower()
|
||||||
if host == "tiktak.dracodev.net":
|
app = hotes_reconnus().get(host)
|
||||||
|
if app == "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 app == "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:])
|
||||||
@@ -91,7 +152,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 app == "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)}")
|
||||||
@@ -99,7 +160,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 app == "gitea":
|
||||||
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()
|
||||||
@@ -111,20 +172,25 @@ def parse_deep_link(url: str) -> tuple[str, str, str, str] | None:
|
|||||||
# ------------------------------------------------------------ CRUD
|
# ------------------------------------------------------------ CRUD
|
||||||
|
|
||||||
def list_refs(username: str, vault: str, path: str) -> list[dict[str, Any]]:
|
def list_refs(username: str, vault: str, path: str) -> list[dict[str, Any]]:
|
||||||
"""Références posées sur une note, dans l'ordre de création."""
|
"""Références posées sur une note, dans l'ordre de création.
|
||||||
|
|
||||||
|
``target_url`` de chaque référence retournée est **reconstruite
|
||||||
|
depuis la config courante** (#223, ``_avec_url_affichee``) : la
|
||||||
|
valeur stockée, figée au collage, n'est jamais servie telle quelle.
|
||||||
|
"""
|
||||||
if not username:
|
if not username:
|
||||||
return []
|
return []
|
||||||
return [
|
return [
|
||||||
r for r in _read_refs(username)
|
_avec_url_affichee(r) for r in _read_refs(username)
|
||||||
if r.get("owner_vault") == vault and r.get("owner_path") == path
|
if r.get("owner_vault") == vault and r.get("owner_path") == path
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_ref(username: str, ref_id: str) -> dict[str, Any] | None:
|
def get_ref(username: str, ref_id: str) -> dict[str, Any] | None:
|
||||||
"""Une référence par son identifiant, ou ``None``."""
|
"""Une référence par son identifiant, ou ``None`` (URL reconstruite, #223)."""
|
||||||
for r in _read_refs(username):
|
for r in _read_refs(username):
|
||||||
if r.get("id") == ref_id:
|
if r.get("id") == ref_id:
|
||||||
return r
|
return _avec_url_affichee(r)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -169,4 +235,4 @@ def delete_ref(username: str, ref_id: str) -> dict[str, Any] | None:
|
|||||||
return None
|
return None
|
||||||
removed = next(r for r in refs if r.get("id") == ref_id)
|
removed = next(r for r in refs if r.get("id") == ref_id)
|
||||||
_write_refs(username, kept)
|
_write_refs(username, kept)
|
||||||
return removed
|
return _avec_url_affichee(removed)
|
||||||
|
|||||||
+39
-3
@@ -186,15 +186,21 @@ class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|||||||
# avec un nonce frais (`backend.csp`) ou servis par 'self'/CDN
|
# avec un nonce frais (`backend.csp`) ou servis par 'self'/CDN
|
||||||
# listés s'exécutent. `style-src` garde 'unsafe-inline' (attributs
|
# listés s'exécutent. `style-src` garde 'unsafe-inline' (attributs
|
||||||
# `style=` et `el.style` omniprésents — chantier séparé).
|
# `style=` et `el.style` omniprésents — chantier séparé).
|
||||||
|
# #223 : l'origine du service d'agents n'est pas figée — elle
|
||||||
|
# est dérivée de la configuration (DRACO_AGENT_URL, sinon
|
||||||
|
# l'origine d'AGENT_EVENEMENTS_URL, sinon le défaut officiel).
|
||||||
|
from backend.draco_config import agent_origine
|
||||||
|
|
||||||
|
agent = agent_origine()
|
||||||
response.headers["Content-Security-Policy"] = (
|
response.headers["Content-Security-Policy"] = (
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
f"script-src 'self' 'nonce-{nonce}' blob: https://cdnjs.cloudflare.com https://unpkg.com https://esm.sh https://cdn.jsdelivr.net https://static.cloudflareinsights.com; "
|
f"script-src 'self' 'nonce-{nonce}' blob: https://cdnjs.cloudflare.com https://unpkg.com https://esm.sh https://cdn.jsdelivr.net https://static.cloudflareinsights.com; "
|
||||||
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com https://cdn.jsdelivr.net https://esm.sh; "
|
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://fonts.googleapis.com https://cdn.jsdelivr.net https://esm.sh; "
|
||||||
"img-src 'self' data: blob:; "
|
"img-src 'self' data: blob:; "
|
||||||
"connect-src 'self' blob: https://esm.sh https://unpkg.com https://cdnjs.cloudflare.com https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net https://agent.dracodev.net; "
|
f"connect-src 'self' blob: https://esm.sh https://unpkg.com https://cdnjs.cloudflare.com https://fonts.googleapis.com https://fonts.gstatic.com https://cdn.jsdelivr.net {agent}; "
|
||||||
"font-src 'self' data: https://fonts.gstatic.com https://esm.sh; "
|
"font-src 'self' data: https://fonts.gstatic.com https://esm.sh; "
|
||||||
"worker-src 'self' blob:; "
|
"worker-src 'self' blob:; "
|
||||||
"frame-src 'self' blob: https://agent.dracodev.net; "
|
f"frame-src 'self' blob: {agent}; "
|
||||||
"object-src 'none'; "
|
"object-src 'none'; "
|
||||||
"base-uri 'self'; "
|
"base-uri 'self'; "
|
||||||
"form-action 'self'; "
|
"form-action 'self'; "
|
||||||
@@ -829,6 +835,36 @@ def _html_with_nonce(request: Request, name: str) -> str:
|
|||||||
request.state.csp_nonce,
|
request.state.csp_nonce,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _index_html(request: Request) -> str:
|
||||||
|
"""``index.html`` + injection des URL publiques de l'écosystème (#223).
|
||||||
|
|
||||||
|
Contrat d'injection de dracodev-commons : l'hôte (ObsiGate, qui
|
||||||
|
sert la page) fournit les URL aux assets partagés — panneau Spark,
|
||||||
|
barre d'apps, liens inter-apps lisent ``window.DRACO_URLS`` et ne
|
||||||
|
portent que les défauts officiels en repli. Les valeurs viennent
|
||||||
|
de :mod:`backend.draco_config` (environnement ; défauts officiels,
|
||||||
|
noms en ``-dev`` déclarés par le seul ``.env`` du poste). Le script
|
||||||
|
injecté passe par le même mécanisme de nonce CSP que les autres
|
||||||
|
scripts inline de la page.
|
||||||
|
"""
|
||||||
|
import json as _json
|
||||||
|
|
||||||
|
from backend.csp import inject_csp_nonce
|
||||||
|
from backend.draco_config import urls_publiques
|
||||||
|
|
||||||
|
html = (FRONTEND_DIR / "index.html").read_text(encoding="utf-8")
|
||||||
|
script = (
|
||||||
|
"<script>window.DRACO_URLS = "
|
||||||
|
+ _json.dumps(urls_publiques()).replace("<", "\\u003c")
|
||||||
|
+ ";</script>"
|
||||||
|
)
|
||||||
|
if "</head>" in html:
|
||||||
|
html = html.replace("</head>", script + "</head>", 1)
|
||||||
|
else: # repli défensif : tout en tête de document
|
||||||
|
html = script + html
|
||||||
|
return inject_csp_nonce(html, request.state.csp_nonce)
|
||||||
|
|
||||||
if FRONTEND_DIR.exists():
|
if FRONTEND_DIR.exists():
|
||||||
# ``Cache-Control`` for /static is set by SecurityHeadersMiddleware (no-cache).
|
# ``Cache-Control`` for /static is set by SecurityHeadersMiddleware (no-cache).
|
||||||
app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static")
|
app.mount("/static", StaticFiles(directory=str(FRONTEND_DIR)), name="static")
|
||||||
@@ -907,5 +943,5 @@ if FRONTEND_DIR.exists():
|
|||||||
"""Serve the SPA index.html for all non-API routes."""
|
"""Serve the SPA index.html for all non-API routes."""
|
||||||
index_file = FRONTEND_DIR / "index.html"
|
index_file = FRONTEND_DIR / "index.html"
|
||||||
if index_file.exists():
|
if index_file.exists():
|
||||||
return HTMLResponse(content=_html_with_nonce(request, "index.html"), headers={"Cache-Control": "no-cache"})
|
return HTMLResponse(content=_index_html(request), headers={"Cache-Control": "no-cache"})
|
||||||
raise HTTPException(status_code=404, detail="Frontend not found")
|
raise HTTPException(status_code=404, detail="Frontend not found")
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.77.1"
|
version = "2.78.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.77.1"
|
version = "2.78.0"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.77.1",
|
"version": "2.78.0",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
+20
-1
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.77.1 | **Dernière mise à jour :** 2026-10-24
|
> **Version :** 2.78.0 | **Dernière mise à jour :** 2026-10-25
|
||||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
@@ -30,6 +30,25 @@
|
|||||||
|
|
||||||
## 🔵 En cours
|
## 🔵 En cours
|
||||||
|
|
||||||
|
### 223. Dev/prod sans noms publics en dur — URLs de l'écosystème par configuration
|
||||||
|
|
||||||
|
- **Effort :** ~0,5 jour | **Impact :** 🟡 | **Ouvert :** 2026-10-25 | **Statut :** ✅ livré le 2026-10-25 (version du hook — voir CHANGELOG ; redéploiement Docker requis, et en dev déclarer les noms en `-dev` dans le `.env` du poste)
|
||||||
|
- **Fiche :** [features/devprod-urls-config-223.md](./features/devprod-urls-config-223.md)
|
||||||
|
- **Description :** aucun nom public de l'écosystème dracodev ne doit être
|
||||||
|
figé dans le code vivant : les URL publiques (service d'agents, autres
|
||||||
|
apps) viennent de la configuration (défauts = noms **officiels** sans
|
||||||
|
suffixe ; l'environnement dev déclare ses noms en `-dev` dans son
|
||||||
|
`.env`). CSP dérivée de l'origine de l'agent configurée, panneau Spark
|
||||||
|
et liens inter-apps alimentés par l'hôte (injection `window.DRACO_URLS`
|
||||||
|
au service de `index.html`), références croisées reconstruites depuis
|
||||||
|
la config à l'affichage (aucune donnée migrée).
|
||||||
|
- **Sous-tâches :**
|
||||||
|
- [x] Module `backend/draco_config.py` + `.env.example`
|
||||||
|
- [x] CSP `connect-src`/`frame-src` dérivée de la config (tests via config)
|
||||||
|
- [x] `external_refs` : hôtes reconnus + URL d'affichage construites par config
|
||||||
|
- [x] Frontend : panneau Spark, liens TikTak, onglets des apps via `DRACO_URLS`
|
||||||
|
- [x] WebAuthn : confirmation rp_id/origines dérivés de la requête / `OBSIGATE_WEBAUTHN_*`
|
||||||
|
|
||||||
### 222. Fusion — panneau Spark V3 : la console en surimpression pleine page
|
### 222. Fusion — panneau Spark V3 : la console en surimpression pleine page
|
||||||
|
|
||||||
- **Effort :** ~0,1 jour | **Impact :** 🟢 | **Ouvert :** 2026-10-23 | **Statut :** ✅ livré le 2026-10-23 (2.77.0, redéploiement Docker en attente)
|
- **Effort :** ~0,1 jour | **Impact :** 🟢 | **Ouvert :** 2026-10-23 | **Statut :** ✅ livré le 2026-10-23 (2.77.0, redéploiement Docker en attente)
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# #223 — Dev/prod sans noms publics en dur : URLs de l'écosystème par configuration
|
||||||
|
|
||||||
|
> Ouvert le 2026-10-25 (chantier dev/prod de Bruno). Contexte : l'installation
|
||||||
|
> actuelle (`deepin.lab.home`) devient l'environnement **dev** (noms en `-dev`),
|
||||||
|
> la future installation (`jump.point.mint.home`) sera la **prod** (noms
|
||||||
|
> officiels sans suffixe), identique sauf noms et clés. Règle : aucun nom
|
||||||
|
> public de l'écosystème codé en dur dans le code vivant ; défauts = noms
|
||||||
|
> officiels ; le dev déclare ses `-dev` dans son seul `.env`.
|
||||||
|
|
||||||
|
## Configuration (nouveau module `backend/draco_config.py`)
|
||||||
|
|
||||||
|
Lecture à l'appel, jamais à l'import. Défauts officiels :
|
||||||
|
|
||||||
|
| Variable | Défaut officiel | Rôle |
|
||||||
|
|---|---|---|
|
||||||
|
| `DRACO_AGENT_URL` | `https://agent.dracodev.net` | Base du service d'agents. À défaut : origine dérivée d'`AGENT_EVENEMENTS_URL`, puis défaut. |
|
||||||
|
| `DRACO_OBSIGATE_URL` | `https://og.dracodev.net` | Liens profonds ObsiGate |
|
||||||
|
| `DRACO_FLOWDECK_URL` | `https://flowdeck.dracodev.net` | Liens profonds Flowdeck |
|
||||||
|
| `DRACO_TIKTAK_URL` | `https://tiktak.dracodev.net` | Liens profonds TikTak |
|
||||||
|
| `DRACO_KANBANTE_URL` | `https://kanbante.dracodev.net` | Onglet Kanbante de la barre |
|
||||||
|
| `DRACO_GITEA_URL` | `https://git.dracodev.net` | Cartes Kanbante (= issues Gitea). Distincte d'`OBSIGATE_GITEA_URL` (source connectée de l'assistant). |
|
||||||
|
|
||||||
|
`AGENT_EVENEMENTS_URL` (existant, #220) reste la configuration première du
|
||||||
|
journal d'événements ; `agent_journal._url()` retombe sur
|
||||||
|
`DRACO_AGENT_URL + /api/v1/evenements` quand elle est absente.
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
- **CSP** (`backend/main.py`, `SecurityHeadersMiddleware`) : `connect-src` et
|
||||||
|
`frame-src` utilisent `draco_config.agent_origine()` — dérivée de la config
|
||||||
|
à chaque réponse, plus aucun littéral. Tests dans
|
||||||
|
`tests/test_security_headers.py` **via la config** (défaut officiel neutralisé,
|
||||||
|
dérivation depuis `AGENT_EVENEMENTS_URL`, priorité `DRACO_AGENT_URL`).
|
||||||
|
- **Références croisées** (`backend/external_refs.py`) :
|
||||||
|
- `parse_deep_link` reconnaît les hôtes officiels **et** configurés
|
||||||
|
(`draco_config.hotes_reconnus()`) — un lien collé depuis le dev comme
|
||||||
|
depuis la prod est reconnu.
|
||||||
|
- `build_deep_link(app, type, id)` construit l'URL d'affichage depuis la
|
||||||
|
config ; `list_refs` / `get_ref` / `delete_ref` servent des copies dont
|
||||||
|
`target_url` est **reconstruite à la lecture**. ⚠️ Les références
|
||||||
|
**stockées** (`data/external_refs/*.json`) gardent l'URL absolue collée
|
||||||
|
par l'utilisateur : aucune migration, la valeur stockée n'est simplement
|
||||||
|
plus jamais servie telle quelle.
|
||||||
|
- **WebAuthn — confirmé, aucun changement** : `resolve_relying_party()`
|
||||||
|
(`backend/auth/webauthn_mfa.py`, BUG-070) dérive `rp_id`/origines de la
|
||||||
|
requête (hôte exact, `X-Forwarded-Host`/`X-Forwarded-Proto` honorés si
|
||||||
|
`OBSIGATE_TRUST_PROXY=true`), et `OBSIGATE_WEBAUTHN_RP_ID` /
|
||||||
|
`OBSIGATE_WEBAUTHN_ORIGINS` priment quand ils sont posés. Aucun nom figé.
|
||||||
|
|
||||||
|
## Frontend (contrat d'injection de dracodev-commons)
|
||||||
|
|
||||||
|
L'hôte fournit les URL : `backend/main.py::_index_html()` injecte
|
||||||
|
`window.DRACO_URLS = {…}` (JSON issu de `draco_config.urls_publiques()`,
|
||||||
|
nonce CSP par le mécanisme existant) dans `index.html` au service.
|
||||||
|
|
||||||
|
- `frontend/draco-panneau.js` : `API = window.DRACO_URLS.agent || défaut officiel` ;
|
||||||
|
`appHote()` retire un suffixe `-dev` éventuel du premier label d'hôte
|
||||||
|
(`og-dev` → `obsigate` pour le lien profond `?app=`).
|
||||||
|
- `frontend/draco-liens.js` : « Ouvrir dans TikTak » =
|
||||||
|
`window.DRACO_URLS.tiktak || défaut officiel`.
|
||||||
|
- `frontend/index.html` : les `href` des onglets portent les défauts officiels ;
|
||||||
|
le script de configuration de la barre les réécrit depuis `DRACO_URLS`.
|
||||||
|
Le menu des apps (`draco-barre.js`) est construit depuis ces onglets : il
|
||||||
|
suit donc sans changement.
|
||||||
|
- Chemin réellement servi : `/static/draco-*.js` via le montage `StaticFiles`,
|
||||||
|
`Cache-Control: no-cache` posé par `SecurityHeadersMiddleware` sur
|
||||||
|
`/static/*`, et service worker **network-first** pour le code — aucun
|
||||||
|
cache-busting par empreinte n'existe (revalidation systématique).
|
||||||
|
|
||||||
|
## Restes assumés
|
||||||
|
|
||||||
|
- Suffixe SSO `dracodev.net` (`draco_session.validate_return_to`,
|
||||||
|
`frontend/js/auth.js`) : ce n'est pas un nom d'hôte mais le domaine parent
|
||||||
|
commun aux deux environnements (les noms `-dev` restent des sous-domaines) ;
|
||||||
|
le domaine du cookie est déjà configurable (`DRACO_SESSION_COOKIE_DOMAIN`).
|
||||||
|
- Mentions textuelles (titres/messages du panneau, contact OpenAPI et lien
|
||||||
|
« À propos » vers le dépôt Git) : infra Git hors périmètre, ou libellés.
|
||||||
|
- `OBSIGATE_SEARXNG_URL` : déjà configurable, défaut officiel conservé.
|
||||||
|
- Le panneau reste un asset partagé : la même ligne d'injection doit être
|
||||||
|
recopiée dans les 3 autres apps par leurs chantiers respectifs pour
|
||||||
|
retrouver l'octet-identique.
|
||||||
@@ -23,6 +23,12 @@
|
|||||||
kanbante: "#0d9488",
|
kanbante: "#0d9488",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Base TikTak fournie par l'hôte via window.DRACO_URLS (contrat
|
||||||
|
* d'injection dracodev-commons, #223) ; repli = défaut officiel. */
|
||||||
|
function baseTikTak() {
|
||||||
|
return (window.DRACO_URLS && window.DRACO_URLS.tiktak) || "https://tiktak.dracodev.net";
|
||||||
|
}
|
||||||
|
|
||||||
function t(key, fallback) {
|
function t(key, fallback) {
|
||||||
try {
|
try {
|
||||||
var v = window.t && window.t(key);
|
var v = window.t && window.t(key);
|
||||||
@@ -92,7 +98,7 @@
|
|||||||
t("draco.links.projected", "Tâche TikTak projetée")));
|
t("draco.links.projected", "Tâche TikTak projetée")));
|
||||||
var ouvrir = el("a", "btn-action draco-liens-ouvrir",
|
var ouvrir = el("a", "btn-action draco-liens-ouvrir",
|
||||||
t("draco.links.openInTikTak", "Ouvrir dans TikTak"));
|
t("draco.links.openInTikTak", "Ouvrir dans TikTak"));
|
||||||
ouvrir.href = "https://tiktak.dracodev.net/?task=" + tid[1];
|
ouvrir.href = baseTikTak() + "/?task=" + tid[1];
|
||||||
ouvrir.target = "_blank";
|
ouvrir.target = "_blank";
|
||||||
ouvrir.rel = "noopener";
|
ouvrir.rel = "noopener";
|
||||||
bandeau.appendChild(ouvrir);
|
bandeau.appendChild(ouvrir);
|
||||||
|
|||||||
@@ -32,7 +32,10 @@
|
|||||||
* 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";
|
/* URL du service : fournie par l'hôte via window.DRACO_URLS
|
||||||
|
* (contrat d'injection dracodev-commons, #223) — le repli est le
|
||||||
|
* défaut officiel ; aucun nom d'environnement n'est figé ici. */
|
||||||
|
var API = (window.DRACO_URLS && window.DRACO_URLS.agent) || "https://agent.dracodev.net";
|
||||||
var convId = null;
|
var convId = null;
|
||||||
var convKind = null; // 'principale' | 'parallele'
|
var convKind = null; // 'principale' | 'parallele'
|
||||||
var convTitre = "";
|
var convTitre = "";
|
||||||
@@ -56,7 +59,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];
|
/* Premier label de l'hôte, suffixe d'environnement éventuel
|
||||||
|
* retiré (ex. « og-dev » → « og » en dev, #223). */
|
||||||
|
var h = (window.location.hostname || "").split(".")[0].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;
|
||||||
|
|||||||
@@ -397,6 +397,16 @@
|
|||||||
localement ; la recherche focus la recherche native
|
localement ; la recherche focus la recherche native
|
||||||
d'ObsiGate ; Alertes unifiées et Spark arrivent avec la
|
d'ObsiGate ; Alertes unifiées et Spark arrivent avec la
|
||||||
fédération / l'agent commun (phases suivantes). */
|
fédération / l'agent commun (phases suivantes). */
|
||||||
|
/* #223 : les href des onglets portent les défauts officiels ;
|
||||||
|
l'hôte les réécrit depuis window.DRACO_URLS (injecté par le
|
||||||
|
backend depuis sa configuration) quand il est présent. */
|
||||||
|
(function () {
|
||||||
|
if (!window.DRACO_URLS) return;
|
||||||
|
document.querySelectorAll(".draco-apptab").forEach(function (a) {
|
||||||
|
var u = window.DRACO_URLS[a.dataset.app];
|
||||||
|
if (u) a.href = u.replace(/\/$/, "") + "/";
|
||||||
|
});
|
||||||
|
})();
|
||||||
window.DRACO_BAR_CONFIG = {
|
window.DRACO_BAR_CONFIG = {
|
||||||
current: "obsigate",
|
current: "obsigate",
|
||||||
domain: (function () { try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; } })(),
|
domain: (function () { try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; } })(),
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.77.1",
|
"version": "2.78.0",
|
||||||
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
||||||
"main": "patch.js",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -100,9 +100,28 @@ def test_panneau_console_surimpression_222():
|
|||||||
def test_csp_ouvre_le_service_d_agents_218():
|
def test_csp_ouvre_le_service_d_agents_218():
|
||||||
# Le panneau parle au service d'agents : la CSP doit nommer son
|
# Le panneau parle au service d'agents : la CSP doit nommer son
|
||||||
# hôte en connect-src, sans ouvrir de générique supplémentaire.
|
# hôte en connect-src, sans ouvrir de générique supplémentaire.
|
||||||
|
# #223 : l'origine n'est plus un littéral dans main.py — elle est
|
||||||
|
# dérivée de la configuration (backend.draco_config.agent_origine,
|
||||||
|
# testée via la config dans tests/test_security_headers.py).
|
||||||
main = (Path(__file__).resolve().parent.parent / "backend" / "main.py").read_text(encoding="utf-8")
|
main = (Path(__file__).resolve().parent.parent / "backend" / "main.py").read_text(encoding="utf-8")
|
||||||
ligne = next(l for l in main.splitlines() if "connect-src" in l)
|
ligne = next(l for l in main.splitlines() if "connect-src" in l)
|
||||||
assert "https://agent.dracodev.net" in ligne
|
assert "agent_origine" in main
|
||||||
|
assert "{agent}" in ligne
|
||||||
|
|
||||||
|
|
||||||
|
def test_urls_ecosysteme_via_injection_hote_223():
|
||||||
|
# #223 : panneau Spark, liens TikTak et onglets des apps lisent
|
||||||
|
# window.DRACO_URLS (injecté par l'hôte/backend depuis sa config) ;
|
||||||
|
# les littéraux restants ne sont que les défauts officiels en repli.
|
||||||
|
panneau = (FRONTEND / "draco-panneau.js").read_text(encoding="utf-8")
|
||||||
|
assert "window.DRACO_URLS" in panneau
|
||||||
|
assert "DRACO_URLS.agent" in panneau
|
||||||
|
liens = (FRONTEND / "draco-liens.js").read_text(encoding="utf-8")
|
||||||
|
assert "window.DRACO_URLS" in liens
|
||||||
|
assert "baseTikTak()" in liens
|
||||||
|
html = _index()
|
||||||
|
assert "window.DRACO_URLS" in html
|
||||||
|
assert "DRACO_URLS[a.dataset.app]" in html
|
||||||
|
|
||||||
|
|
||||||
def test_rail_mobile_vers_le_vrai_hamburger():
|
def test_rail_mobile_vers_le_vrai_hamburger():
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
"""URLs publiques de l'écosystème par configuration — #223 (dev/prod).
|
||||||
|
|
||||||
|
Règle : aucun nom public de l'écosystème n'est figé dans le code
|
||||||
|
vivant. Défauts = noms **officiels** (production future) ; le dev
|
||||||
|
déclare ses noms en ``-dev`` dans son seul ``.env``
|
||||||
|
(``DRACO_*_URL`` / ``AGENT_EVENEMENTS_URL``). On vérifie ici la
|
||||||
|
config centralisée, la dérivation de l'URL d'événements du journal,
|
||||||
|
la reconnaissance des liens profonds des deux environnements et la
|
||||||
|
reconstruction des URL d'affichage des références croisées.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from backend import draco_config
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def env_propre(monkeypatch):
|
||||||
|
"""Aucune variable d'URL d'écosystème posée (défauts officiels)."""
|
||||||
|
for nom in (
|
||||||
|
"DRACO_AGENT_URL", "DRACO_OBSIGATE_URL", "DRACO_FLOWDECK_URL",
|
||||||
|
"DRACO_TIKTAK_URL", "DRACO_KANBANTE_URL", "DRACO_GITEA_URL",
|
||||||
|
"AGENT_EVENEMENTS_URL",
|
||||||
|
):
|
||||||
|
monkeypatch.delenv(nom, raising=False)
|
||||||
|
return monkeypatch
|
||||||
|
|
||||||
|
|
||||||
|
def test_defauts_officiels_sans_dev(env_propre):
|
||||||
|
assert draco_config.agent_url() == "https://agent.dracodev.net"
|
||||||
|
assert draco_config.app_url("tiktak") == "https://tiktak.dracodev.net"
|
||||||
|
assert draco_config.app_url("obsigate") == "https://og.dracodev.net"
|
||||||
|
assert draco_config.app_url("flowdeck") == "https://flowdeck.dracodev.net"
|
||||||
|
assert draco_config.app_url("kanbante") == "https://kanbante.dracodev.net"
|
||||||
|
assert draco_config.app_url("gitea") == "https://git.dracodev.net"
|
||||||
|
for url in draco_config.urls_publiques().values():
|
||||||
|
assert "-dev" not in url
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_derive_des_evenements(env_propre):
|
||||||
|
"""Sans DRACO_AGENT_URL, l'origine suit AGENT_EVENEMENTS_URL —
|
||||||
|
la configuration réelle du poste (dev inclus)."""
|
||||||
|
env_propre.setenv(
|
||||||
|
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
|
||||||
|
assert draco_config.agent_url() == "https://agent-dev.dracodev.net"
|
||||||
|
assert draco_config.agent_origine() == "https://agent-dev.dracodev.net"
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_explicite_prime(env_propre):
|
||||||
|
env_propre.setenv(
|
||||||
|
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
|
||||||
|
env_propre.setenv("DRACO_AGENT_URL", "https://agent.example.net/")
|
||||||
|
assert draco_config.agent_url() == "https://agent.example.net"
|
||||||
|
|
||||||
|
|
||||||
|
def test_journal_url_suit_la_config(env_propre):
|
||||||
|
from backend import agent_journal
|
||||||
|
|
||||||
|
assert agent_journal._url() == "https://agent.dracodev.net/api/v1/evenements"
|
||||||
|
env_propre.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
|
||||||
|
assert agent_journal._url() == "https://agent-dev.dracodev.net/api/v1/evenements"
|
||||||
|
env_propre.setenv(
|
||||||
|
"AGENT_EVENEMENTS_URL", "https://autre.example.net/api/v1/evenements")
|
||||||
|
assert agent_journal._url() == "https://autre.example.net/api/v1/evenements"
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ liens profonds
|
||||||
|
|
||||||
|
def test_parse_hotes_configures(env_propre):
|
||||||
|
"""Un lien collé depuis l'environnement dev (hôtes déclarés dans
|
||||||
|
son .env) est reconnu, comme un lien officiel."""
|
||||||
|
from backend.external_refs import parse_deep_link
|
||||||
|
|
||||||
|
env_propre.setenv("DRACO_TIKTAK_URL", "https://tiktak-dev.dracodev.net")
|
||||||
|
env_propre.setenv("DRACO_OBSIGATE_URL", "https://og-dev.dracodev.net")
|
||||||
|
assert parse_deep_link("https://tiktak-dev.dracodev.net/?task=5") == (
|
||||||
|
"tiktak", "task", "5", "TikTak · tâche 5")
|
||||||
|
assert parse_deep_link("https://og-dev.dracodev.net/#file=v:Dossier/Note.md") == (
|
||||||
|
"obsigate", "note", "v:Dossier/Note.md", "ObsiGate · Note.md")
|
||||||
|
# L'officiel reste reconnu en parallèle (lien venu de la prod).
|
||||||
|
assert parse_deep_link("https://tiktak.dracodev.net/?task=5") is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_url_affichage_reconstruite_depuis_config(env_propre, tmp_path):
|
||||||
|
"""#223 : la référence stockée garde son URL absolue d'origine
|
||||||
|
(aucune migration), mais l'URL **servie** est reconstruite depuis
|
||||||
|
la config courante."""
|
||||||
|
from backend.external_refs import add_ref, build_deep_link, list_refs
|
||||||
|
|
||||||
|
env_propre.setenv("OBSIGATE_EXTERNAL_REFS_DIR", str(tmp_path / "refs"))
|
||||||
|
assert build_deep_link("tiktak", "task", "42") == \
|
||||||
|
"https://tiktak.dracodev.net/?task=42"
|
||||||
|
assert build_deep_link("kanbante", "card", "Projets/obsigate#7") == \
|
||||||
|
"https://git.dracodev.net/Projets/obsigate/issues/7"
|
||||||
|
assert build_deep_link("obsigate", "note", "v:Dossier/Note.md") == \
|
||||||
|
"https://og.dracodev.net/#file=v:Dossier%2FNote.md"
|
||||||
|
|
||||||
|
env_propre.setenv("DRACO_TIKTAK_URL", "https://tiktak-dev.dracodev.net")
|
||||||
|
ref = add_ref("alice", "Vault", "N.md", "tiktak", "task", "42",
|
||||||
|
"https://tiktak.dracodev.net/?task=42", "TikTak · tâche 42")
|
||||||
|
assert ref["target_url"] == "https://tiktak.dracodev.net/?task=42" # stocké tel quel
|
||||||
|
servies = list_refs("alice", "Vault", "N.md")
|
||||||
|
assert servies[0]["target_url"] == "https://tiktak-dev.dracodev.net/?task=42"
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------ injection hôte
|
||||||
|
|
||||||
|
def test_index_injecte_draco_urls(client, env_propre):
|
||||||
|
"""L'hôte sert index.html avec window.DRACO_URLS issu de sa config."""
|
||||||
|
env_propre.setenv("DRACO_AGENT_URL", "https://agent-dev.dracodev.net")
|
||||||
|
resp = client.get("/")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert "window.DRACO_URLS" in resp.text
|
||||||
|
assert '"agent": "https://agent-dev.dracodev.net"' in resp.text
|
||||||
|
assert '"tiktak": "https://tiktak.dracodev.net"' in resp.text
|
||||||
@@ -84,13 +84,19 @@ def _csp_directives(csp: str) -> dict[str, list[str]]:
|
|||||||
return directives
|
return directives
|
||||||
|
|
||||||
|
|
||||||
def test_csp_frame_src_allows_agent_console(client):
|
def test_csp_frame_src_allows_agent_console(client, monkeypatch):
|
||||||
"""BUG-119 : le « mode console » du panneau Spark charge la console du
|
"""BUG-119 : le « mode console » du panneau Spark charge la console du
|
||||||
service d'agents (``https://agent.dracodev.net``) dans une iframe —
|
service d'agents (``https://agent.dracodev.net``) dans une iframe —
|
||||||
``frame-src`` doit autoriser cette origine, sinon le navigateur bloque
|
``frame-src`` doit autoriser cette origine, sinon le navigateur bloque
|
||||||
le chargement (« This content is blocked »). Les sources historiques
|
le chargement (« This content is blocked »). Les sources historiques
|
||||||
(``'self'`` et ``blob:``, visualiseur PDF) restent en place et aucune
|
(``'self'`` et ``blob:``, visualiseur PDF) restent en place et aucune
|
||||||
autre origine n'est admise."""
|
autre origine n'est admise.
|
||||||
|
|
||||||
|
#223 : la configuration est neutralisée ici (défaut officiel) —
|
||||||
|
l'origine n'est plus figée dans le code, voir le test via config
|
||||||
|
ci-dessous."""
|
||||||
|
monkeypatch.delenv("DRACO_AGENT_URL", raising=False)
|
||||||
|
monkeypatch.delenv("AGENT_EVENEMENTS_URL", raising=False)
|
||||||
resp = client.get("/api/health")
|
resp = client.get("/api/health")
|
||||||
csp = resp.headers.get("content-security-policy", "")
|
csp = resp.headers.get("content-security-policy", "")
|
||||||
directives = _csp_directives(csp)
|
directives = _csp_directives(csp)
|
||||||
@@ -99,3 +105,26 @@ def test_csp_frame_src_allows_agent_console(client):
|
|||||||
"blob:",
|
"blob:",
|
||||||
"https://agent.dracodev.net",
|
"https://agent.dracodev.net",
|
||||||
]
|
]
|
||||||
|
assert "https://agent.dracodev.net" in directives.get("connect-src", [])
|
||||||
|
|
||||||
|
|
||||||
|
def test_csp_agent_origine_via_config(client, monkeypatch):
|
||||||
|
"""#223 : ``frame-src``/``connect-src`` suivent l'origine de l'agent
|
||||||
|
**configurée** — dérivée d'``AGENT_EVENEMENTS_URL`` (config réelle
|
||||||
|
du poste, noms en ``-dev`` en dev), ou de ``DRACO_AGENT_URL`` qui
|
||||||
|
prime. Aucun nom d'environnement n'est codé en dur."""
|
||||||
|
monkeypatch.delenv("DRACO_AGENT_URL", raising=False)
|
||||||
|
monkeypatch.setenv(
|
||||||
|
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
|
||||||
|
resp = client.get("/api/health")
|
||||||
|
directives = _csp_directives(resp.headers.get("content-security-policy", ""))
|
||||||
|
assert directives.get("frame-src") == [
|
||||||
|
"'self'", "blob:", "https://agent-dev.dracodev.net"]
|
||||||
|
assert "https://agent-dev.dracodev.net" in directives.get("connect-src", [])
|
||||||
|
assert "https://agent.dracodev.net" not in directives.get("connect-src", [])
|
||||||
|
|
||||||
|
monkeypatch.setenv("DRACO_AGENT_URL", "https://agent.example.net")
|
||||||
|
resp = client.get("/api/health")
|
||||||
|
directives = _csp_directives(resp.headers.get("content-security-policy", ""))
|
||||||
|
assert directives.get("frame-src") == [
|
||||||
|
"'self'", "blob:", "https://agent.example.net"]
|
||||||
|
|||||||
Reference in New Issue
Block a user