Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5203b6288e | ||
|
|
50d7f337da | ||
|
|
0eec63a5ca | ||
|
|
296d9cfa25 | ||
|
|
be3e4e5f69 | ||
|
|
54bd43e9f2 | ||
|
|
9068e1572f | ||
|
|
23bcea4b56 | ||
|
|
e7c65b6f49 | ||
|
|
77b4ee29e6 | ||
|
|
95ba369e04 | ||
|
|
a75a477fea | ||
|
|
c8beccaa2e | ||
|
|
08e912d56f | ||
|
|
0464f6156e | ||
|
|
8bba621223 |
@@ -1,3 +1,43 @@
|
||||
## v7.85.1 (2026-11-06) — Barre commune v0.5.0 : thème commun (HUB-18, fusion #132)
|
||||
|
||||
> **Hypothèse d'ordre** : cette version suppose la fusion préalable de la PR HUB-15 #51 (Flowdeck 7.85.0) ; branchée sur `main` (7.84.15), elle vise **7.85.1**.
|
||||
|
||||
- **Barre d'apps v0.5.0 vendangée (dracodev-commons, branche `hub18-theme-commun`, PR commons #12)** : seul `static/js/vendor/draco-barre.js` change, remplacé verbatim par la v0.5.0 ; le CSS et le fragment `app/templates/_draco_bar.html` sont inchangés.
|
||||
- **Thème commun** : la barre apprend la préférence de thème du profil du Hub (`GET <hub>/api/profil/theme`, cookie draco ; `<hub>` = URL résolue de la barre, jamais figée) et l'applique à l'ouverture : cache `localStorage` `draco-theme-hub` (TTL 12 h) appliqué synchrone, puis fetch asynchrone non bloquant ; `sombre`/`clair` posent `data-theme`, `systeme` le retire (`prefers-color-scheme` décide). **Repli intégral** si le Hub ne répond pas, répond non-OK ou `theme: null` : le comportement actuel (choix local, puis système) reste seul maître.
|
||||
- **Initialisation propre à Flowdeck** (`static/js/app.js`) : écoute de l'événement `draco:theme` de la barre — le mode effectif est appliqué sur `data-theme` et mémorisé dans la clé locale `theme` (convergence) ; le sélecteur de thème des Réglages reste fonctionnel pour la session.
|
||||
- **Cache** : le bump 7.85.0 → 7.85.1 change aussi `asset_version`, donc les navigateurs ne garderont pas l'ancienne barre.
|
||||
- Tests : `tests/test_draco_bar_texte.py` étendu (marqueurs v0.5.0 du JS vendangé + écouteur `draco:theme` dans app.js).
|
||||
|
||||
## v7.85.0 (2026-11-06) — HUB-15 : provisionnement JIT des comptes draco (fusion #129)
|
||||
|
||||
- **Généralisation de `maybe_link` en création JIT** (`app/services/draco.py`) : à la première connexion avec un cookie `draco_session` valide, si aucun compte local ne correspond au `sub` (ni lien `draco_links`, ni repli mono-utilisateur), le pendant local est **créé automatiquement** depuis les claims (`sub`, `name`) et lié au `sub` dans la même transaction — aucune création manuelle dans l'app. Un compte de suite créé dans le Hub fonctionne donc dans Flowdeck dès sa première visite.
|
||||
- **Rôle par défaut sûr** : utilisateur standard (`is_admin = 0`, jamais admin), `auth_method = 'draco'`, **aucun mot de passe local** (`password_hash` NULL — la connexion locale par mot de passe reste impossible pour ce compte, et inchangée pour tous les autres). Login déterministe `draco-<sub>` (suffixe d'empreinte en cas de collision avec un login existant non lié).
|
||||
- **Gardes** : une ligne `draco_links` existante pour le `sub` (lien actif, compte inactif ou **pierre tombale** de dissociation) interdit toute création — une dissociation explicite n'est jamais contournée. Les comptes déjà liés ne changent pas ; la connexion locale et l'OAuth Gitea/GitHub sont intacts.
|
||||
- **Numérotation** : fonction nouvelle → série mineure 7.84 → **7.85.0** (patron du dépôt : la série porte les vagues de fonctions, les correctifs restent en patch).
|
||||
- Tests : `tests/test_fusion_draco.py` — création JIT en multi-utilisateurs (rôle, absence de mot de passe, lien, réutilisation à la 2e visite, connexion locale refusée), pierre tombale sans re-création ; l'ancien refus « lien requis en multi-utilisateurs » est remplacé par le nouveau contrat (le lien explicite continue d'ouvrir la porte).
|
||||
|
||||
## v7.84.15 (2026-11-05) — Barre commune v0.4.1 : correctif des icônes invisibles du menu compte (fusion #125)
|
||||
|
||||
- **Barre d'apps v0.4.1 vendangée (dracodev-commons, branche `menu-compte-icones-svg-ns-v0-4-1`, PR commons #11)** : seul `static/js/vendor/draco-barre.js` change, remplacé verbatim par la v0.4.1 ; le CSS et le fragment `app/templates/_draco_bar.html` sont inchangés.
|
||||
- **Correctif** : en v0.4.0, `menuIcone()` créait le `<svg>` des icônes et ses primitives via `document.createElement()` — espace de noms **HTML**, jamais dessinés par le navigateur : les libellés du menu compte étaient décalés d'un cran d'icône, sans aucun glyphe. La v0.4.1 crée tout via `document.createElementNS()` (espace de noms SVG). Glyphes, CSS et comportements inchangés.
|
||||
- **Cache** : le bump 7.84.14 → 7.84.15 change aussi `asset_version`, donc les navigateurs ne garderont pas l'ancienne barre.
|
||||
- Tests : `tests/test_draco_bar_texte.py` étendu (création en espace de noms SVG verrouillée).
|
||||
|
||||
## v7.84.14 (2026-11-05) — Barre commune v0.4.0 : icônes monoline du menu compte (HUB-11)
|
||||
|
||||
- **Barre d'apps v0.4.0 vendangée (dracodev-commons, branche `menu-compte-icones-v0-4-0`, PR commons #10)** : `static/css/draco-barre.css` et `static/js/vendor/draco-barre.js` remplacés verbatim par la v0.4.0 ; le fragment `app/templates/_draco_bar.html` est inchangé (aucune clé d'URL nouvelle).
|
||||
- **Menu compte à icônes** : **Télécharger**, **À Propos**, **Aides**, **Profil** et **Déconnexion** reçoivent des **icônes SVG monoline** (contour, trait fin, `currentColor`, ~18 px, couleur atténuée suivant le survol) dans le style des icônes du rail de la console Spark — aucun emoji. L'en-tête d'identité reste sans icône ; les liens Hub et le relais « Déconnexion » sont inchangés.
|
||||
- **Cache** : le bump 7.84.13 → 7.84.14 change aussi `asset_version`, donc les navigateurs ne garderont pas l'ancienne barre.
|
||||
- Tests : `tests/test_draco_bar_texte.py` étendu (marqueurs v0.4.0 du JS et du CSS vendangés).
|
||||
|
||||
## v7.84.13 (2026-11-04) — Barre commune v0.3.0 : liens Hub du menu compte (HUB-1)
|
||||
|
||||
- **Barre d'apps v0.3.0 vendangée (dracodev-commons, branche `hub-menu-compte-v0-3-0`, PR commons #9)** : `static/css/draco-barre.css` et `static/js/vendor/draco-barre.js` remplacés verbatim par la v0.3.0 ; fragment `app/templates/_draco_bar.html` porté (note de vendange + `urls.hub` dans la configuration), le reste de l'intégration Flowdeck inchangé.
|
||||
- **Menu compte** : liens **Télécharger**, **À Propos**, **Aides** et **Profil** (devenu un lien vers `<hub>/profil`, plus un relais) construits par le JS vendangé depuis la base Hub résolue ; « Déconnexion » reste relayée à l'hôte, inchangée.
|
||||
- **URL Hub par le même mécanisme que les autres** : `app/services/draco_urls.py` gagne la clé `hub` (défaut officiel `https://hub.dracodev.net` ; explicite `HUB_URL` / `settings.hub_url` ; dérivation par l'hôte de la requête — motif fermé étendu à `hub` — et par l'hôte de l'agent : `hub-dev` en dev), exposée dans `draco_urls.toutes()` et passée au gabarit en `urls.hub`. Les consommateurs par listes de clés (CORS, CSP, recherche fédérée) ne sont pas affectés : le Hub n'y entre pas.
|
||||
- **Cache** : le bump 7.84.12 → 7.84.13 change aussi `asset_version`, donc les navigateurs ne garderont pas l'ancienne barre.
|
||||
- Tests : `tests/test_draco_urls_devprod.py` (résolution Hub : défaut, dérivations, explicite) et `tests/test_draco_bar_texte.py` (marqueurs v0.3.0 du JS vendangé et du gabarit) étendus.
|
||||
|
||||
## v7.84.11 (2026-11-04) — Recherche fédérée : ShaarIt, 5e source de « Rechercher partout »
|
||||
|
||||
- **`static/js/recherche.js` re-vendangé depuis dracodev-commons v0.2.1** (source canonique `ui/recherche/`, contrat `contrats/recherche-federee.md`) : ShaarIt devient une source de la page `/recherche` comme les autres — bases prod/dev, onglet, normaliseur (lien profond canonique `{base}shaare/<shorturl>`, contexte « Bookmark · privé · tags », repli recherche native `/?q=`). Le fichier reste **strictement identique** chez les cinq hôtes.
|
||||
|
||||
@@ -157,6 +157,7 @@ class Settings(BaseSettings):
|
||||
# TIKTAK_URL=https://tiktak-dev.dracodev.net
|
||||
# KANBANTE_URL=https://kanbante-dev.dracodev.net
|
||||
# SHAARIT_URL=https://shaarit-dev.dracodev.net
|
||||
# HUB_URL=https://hub-dev.dracodev.net
|
||||
# FLOWDECK_URL=https://flowdeck-dev.dracodev.net
|
||||
draco_sso_url: str = "https://og.dracodev.net" # ObsiGate (émettrice SSO)
|
||||
# ── Coffre ObsiGate, côté consommateur Flowdeck (Phase 4, issue #22) ──
|
||||
@@ -170,6 +171,7 @@ class Settings(BaseSettings):
|
||||
tiktak_url: str = "https://tiktak.dracodev.net"
|
||||
kanbante_url: str = "https://kanbante.dracodev.net"
|
||||
shaarit_url: str = "https://shaarit.dracodev.net"
|
||||
hub_url: str = "https://hub.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.
|
||||
|
||||
+1
-1
@@ -216,7 +216,7 @@ def _lire_version() -> str:
|
||||
|
||||
|
||||
# Littéral de version synchronisé avec le fichier VERSION :
|
||||
# version="7.84.3"
|
||||
# version="7.85.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()
|
||||
|
||||
@@ -178,10 +178,17 @@ class ContentSecurityPolicyMiddleware(BaseHTTPMiddleware):
|
||||
# notifications des applications soeurs depuis cet hôte ;
|
||||
# leurs origines rejoignent connect-src (même résolution
|
||||
# draco_urls que les liens de la barre et frame-src).
|
||||
# Fusion #111 : la recherche fédérée appelle en plus
|
||||
# l'API de ShaarIt depuis la page /recherche — sans son
|
||||
# origine ici, le navigateur bloque l'appel et la section
|
||||
# ShaarIt affichait « Injoignable depuis cet hôte (CORS ou
|
||||
# réseau) » alors que le CORS de ShaarIt était correct
|
||||
# (retour d'usage du 2026-11-04).
|
||||
_toutes69 = draco_urls.toutes()
|
||||
sisters = " ".join(sorted({
|
||||
draco_urls.origine(_toutes69[k])
|
||||
for k in ("obsigate", "flowdeck", "tiktak", "kanbante")
|
||||
for k in ("obsigate", "flowdeck", "tiktak", "kanbante",
|
||||
"shaarit")
|
||||
} - {""}))
|
||||
response.headers[self.CSP_HEADER] = self.CSP_VALUE.format(
|
||||
nonce=nonce, host=host, agent=agent, sisters=sisters
|
||||
|
||||
+85
-1
@@ -15,6 +15,17 @@ local : table ``draco_links`` (migration 51), créée automatiquement
|
||||
coexistent (association 1:1, dissociation = pierre tombale) ; à
|
||||
défaut de lien, une instance à un seul utilisateur actif associe ce
|
||||
sub à cet unique compte. Sans secret configuré, module inerte.
|
||||
|
||||
HUB-15 (plan Hub v2.1, §2.2) — provisionnement JIT : si aucun compte
|
||||
local ne correspond au ``sub`` (ni lien, ni repli mono-utilisateur),
|
||||
le pendant local est **créé automatiquement** à la première
|
||||
connexion draco, depuis les claims (``sub``, ``name``), avec le rôle
|
||||
par défaut sûr : utilisateur standard (``is_admin = 0``, jamais
|
||||
admin), aucun mot de passe local (``password_hash`` NULL,
|
||||
``auth_method = 'draco'`` — l'authentification passe par draco, la
|
||||
connexion locale par mot de passe reste impossible pour ce compte
|
||||
et inchangée pour les autres). Une pierre tombale (dissociation
|
||||
explicite) interdit toute re-création automatique pour ce ``sub``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -138,8 +149,79 @@ def _linked_user(conn, sub: str) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
def _jit_login(sub: str) -> str:
|
||||
"""Login local déterministe dérivé du ``sub`` draco.
|
||||
|
||||
Préfixe ``draco-`` (aucune collision possible avec un login
|
||||
choisi par un humain ou par l'OAuth, préfixés par fournisseur),
|
||||
caractères restreints à ``[a-z0-9._-]``.
|
||||
"""
|
||||
nettoye = "".join(
|
||||
c if (c.isascii() and (c.isalnum() or c in "._-")) else "-"
|
||||
for c in sub.strip().lower()
|
||||
).strip("-")
|
||||
return f"draco-{nettoye}" if nettoye else "draco-compte"
|
||||
|
||||
|
||||
def _provision_jit(conn, claims: dict) -> dict | None:
|
||||
"""Crée le pendant local d'un ``sub`` draco inconnu (HUB-15).
|
||||
|
||||
Création + liaison dans la même transaction. Retourne le nouvel
|
||||
utilisateur, ou None si la création ne doit pas avoir lieu :
|
||||
une ligne ``draco_links`` existe déjà pour ce ``sub`` (lien actif
|
||||
— déjà résolu par ``_linked_user`` —, compte lié inactif, ou
|
||||
pierre tombale d'une dissociation explicite : on ne recrée
|
||||
jamais automatiquement derrière une dissociation).
|
||||
"""
|
||||
sub = str(claims.get("sub", ""))
|
||||
if not sub:
|
||||
return None
|
||||
deja = conn.execute(
|
||||
"SELECT user_id FROM draco_links WHERE sub = ?", (sub,)
|
||||
).fetchone()
|
||||
if deja is not None:
|
||||
return None
|
||||
nom = str(claims.get("name", "") or "").strip()
|
||||
login = _jit_login(sub)
|
||||
# Collision résiduelle : un login identique pris par un autre
|
||||
# compte (non lié) — on suffixe par une empreinte courte du sub
|
||||
# plutôt que de lier ou d'écraser quoi que ce soit.
|
||||
if conn.execute(
|
||||
"SELECT id FROM users WHERE login = ?", (login,)
|
||||
).fetchone() is not None:
|
||||
suffixe = hashlib.sha256(sub.encode("utf-8")).hexdigest()[:8]
|
||||
login = f"{login}-{suffixe}"
|
||||
try:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO users (login, full_name, email, password_hash, "
|
||||
"is_admin, is_active, auth_method) "
|
||||
"VALUES (?, ?, '', NULL, 0, 1, 'draco')",
|
||||
(login, nom or login),
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO draco_links (sub, user_id) VALUES (?, ?)",
|
||||
(sub, cur.lastrowid),
|
||||
)
|
||||
conn.commit()
|
||||
except Exception as exc: # la création ne casse jamais la requête
|
||||
logger.debug("Provisionnement JIT draco refusé: %s", exc)
|
||||
conn.rollback()
|
||||
return None
|
||||
row = conn.execute(
|
||||
"SELECT * FROM users WHERE id = ?", (cur.lastrowid,)
|
||||
).fetchone()
|
||||
logger.info("Compte local créé en JIT pour le sub draco %s (login %s)",
|
||||
sub, login)
|
||||
return dict(row) if row is not None else None
|
||||
|
||||
|
||||
def user_for_draco(request) -> dict | None:
|
||||
"""Utilisateur Flowdeck prouvé par le cookie draco_session, ou None."""
|
||||
"""Utilisateur Flowdeck prouvé par le cookie draco_session, ou None.
|
||||
|
||||
Résolution : lien ``draco_links`` existant, sinon repli
|
||||
mono-utilisateur, sinon provisionnement JIT (HUB-15) — le
|
||||
pendant local est créé depuis les claims et lié au ``sub``.
|
||||
"""
|
||||
if not enabled():
|
||||
return None
|
||||
claims = verify_token(request.cookies.get(COOKIE_NAME, ""))
|
||||
@@ -153,6 +235,8 @@ def user_for_draco(request) -> dict | None:
|
||||
return None
|
||||
with get_conn() as conn:
|
||||
user = _linked_user(conn, sub)
|
||||
if user is None:
|
||||
user = _provision_jit(conn, claims)
|
||||
if user is not None and not user.get("is_active"):
|
||||
return None
|
||||
return user
|
||||
|
||||
@@ -98,6 +98,7 @@ OFFICIELLES = {
|
||||
"kanbante": "https://kanbante.dracodev.net",
|
||||
"shaarit": "https://shaarit.dracodev.net",
|
||||
"flowdeck": "https://flowdeck.dracodev.net",
|
||||
"hub": "https://hub.dracodev.net",
|
||||
"gitea": "https://git.dracodev.net",
|
||||
}
|
||||
|
||||
@@ -123,6 +124,7 @@ _SLUGS_SOEURS = {
|
||||
"tiktak": "tiktak",
|
||||
"kanbante": "kanbante",
|
||||
"shaarit": "shaarit",
|
||||
"hub": "hub",
|
||||
}
|
||||
|
||||
# Hôte de l'agent : agent<SUFFIXE>.dracodev.net (SUFFIXE = ``-dev``…).
|
||||
@@ -133,7 +135,7 @@ _MOTIF_HOTE_AGENT = re.compile(r"^agent(-[a-z0-9-]+)?\.dracodev\.net$")
|
||||
# de TOUTE l'installation — jamais un suffixe inventé depuis un
|
||||
# hôte arbitraire : hors motif (autre domaine, LAN, IP), None.
|
||||
_MOTIF_HOTE_PUBLIC = re.compile(
|
||||
r"^(og|flowdeck|tiktak|kanbante|agent)(-[a-z0-9-]+)?\.dracodev\.net$")
|
||||
r"^(og|flowdeck|tiktak|kanbante|agent|hub)(-[a-z0-9-]+)?\.dracodev\.net$")
|
||||
|
||||
# Slugs pour la résolution par l'hôte de la requête (l'agent inclus).
|
||||
_SLUGS_TOUTES = {**_SLUGS_SOEURS, "agent": "agent"}
|
||||
@@ -282,6 +284,14 @@ def shaarit_url() -> str:
|
||||
return _resoudre_soeur("SHAARIT_URL", "shaarit_url", "shaarit")
|
||||
|
||||
|
||||
def hub_url() -> str:
|
||||
"""Hub (barre v0.3.0, HUB-1) : même résolution que les sœurs —
|
||||
il n'a pas d'onglet, sa base sert les liens du menu compte
|
||||
(Télécharger, À Propos, Aides, Profil) via ``urls.hub`` de la
|
||||
configuration de la barre."""
|
||||
return _resoudre_soeur("HUB_URL", "hub_url", "hub")
|
||||
|
||||
|
||||
def gitea_url() -> str:
|
||||
return _resoudre("GITEA_URL", "gitea_url", "gitea")
|
||||
|
||||
@@ -366,6 +376,7 @@ def toutes() -> dict[str, str]:
|
||||
"kanbante": kanbante_url(),
|
||||
"shaarit": shaarit_url(),
|
||||
"flowdeck": flowdeck_url(),
|
||||
"hub": hub_url(),
|
||||
"gitea": gitea_url(),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
{# Barre d'apps commune dracodev — Fusion ACC-1.1 (issue #17).
|
||||
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.2.0
|
||||
(ui/barre-apps/, commit 70c837a de la branche
|
||||
shaarit-phase3-barre-contrats) : ne pas modifier la structure ici ;
|
||||
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.3.0
|
||||
(ui/barre-apps/, branche hub-menu-compte-v0-3-0 de la PR commons
|
||||
#9 — HUB-1, fusion #115) : ne pas modifier la structure ici ;
|
||||
toute évolution passe par une nouvelle version de commons.
|
||||
v0.3.0 : menu compte — liens Hub Télécharger / À Propos / Aides
|
||||
et Profil devenu lien (base draco_urls().hub passée en urls.hub
|
||||
de la configuration ci-dessous) ; « Déconnexion » reste relayée.
|
||||
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
|
||||
@@ -105,6 +108,12 @@
|
||||
repli si le module ne charge pas). */
|
||||
window.DRACO_BAR_CONFIG = {
|
||||
current: "flowdeck",
|
||||
/* Barre v0.3.0 (HUB-1) : le Hub n'a pas d'onglet — sa base
|
||||
(draco_urls().hub : HUB_URL explicite, hôte de la requête,
|
||||
dérivation par l'agent, défaut officiel) alimente les
|
||||
liens du menu compte (Télécharger, À Propos, Aides,
|
||||
Profil) via urls.hub. */
|
||||
urls: { hub: "{{ draco_urls().hub }}/" },
|
||||
domain: (function () { try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; } })(),
|
||||
counts: {},
|
||||
onAction: function (act, el) {
|
||||
|
||||
@@ -20,7 +20,18 @@
|
||||
le clair via data-theme="light" héritait quand même du sombre
|
||||
système (piège constaté dans les copies vendangées de
|
||||
Kanbante/Flowdeck ; les re-vendanges de la v0.2.0 le corrigent).
|
||||
v0.3.0 (HUB-1) : aucun style nouveau — les liens du Hub du menu
|
||||
compte (Télécharger, À Propos, Aides, Profil) réutilisent
|
||||
.draco-menu-item (déjà ancre-compatible : text-decoration none)
|
||||
et le séparateur .draco-menu-sep du menu des applications.
|
||||
v0.4.0 (HUB-11) : .draco-menu-icone — les icônes monoline des
|
||||
items du menu compte (SVG inline construit par le JS) : 18 px,
|
||||
replacées à gauche du libellé par order: -1 (l'item est en
|
||||
flex), couleur atténuée --ink-2, pleine --ink au survol/focus.
|
||||
Jetons repris du prototype validé Console v7.2.
|
||||
v0.4.1 (fusion #125) : menu compte resserré — min-width
|
||||
248 → 232 px, le panneau épouse le contenu (demande de Bruno :
|
||||
le menu était trop large une fois les icônes visibles).
|
||||
Thème : clair par défaut, sombre via [data-theme="dark"] sur <html>
|
||||
ou prefers-color-scheme si l'app intégratrice ne force rien
|
||||
(aucun attribut data-theme sur <html>). */
|
||||
@@ -78,7 +89,7 @@
|
||||
* droite ; ses items réutilisent .draco-apps-item. */
|
||||
.draco-compte-menu {
|
||||
position: absolute; top: calc(100% + 6px); right: 10px; z-index: 400;
|
||||
min-width: 248px; padding: 6px;
|
||||
min-width: 232px; padding: 6px;
|
||||
background: var(--surface); border: 1px solid var(--line); border-radius: 14px;
|
||||
box-shadow: 0 16px 44px rgb(0 0 0 / .28);
|
||||
}
|
||||
@@ -90,6 +101,15 @@
|
||||
text-decoration: none; cursor: pointer;
|
||||
}
|
||||
.draco-menu-item:hover, .draco-menu-item:focus-visible { background: var(--surface-2); }
|
||||
/* Icônes monoline du menu compte (v0.4.0) : appendues après le
|
||||
libellé dans le DOM, replacées à gauche par order: -1. */
|
||||
.draco-menu-icone {
|
||||
order: -1; flex: none; display: inline-flex; align-items: center;
|
||||
width: 18px; height: 18px; color: var(--ink-2);
|
||||
}
|
||||
.draco-menu-icone svg { display: block; width: 18px; height: 18px; }
|
||||
.draco-menu-item:hover .draco-menu-icone,
|
||||
.draco-menu-item:focus-visible .draco-menu-icone { color: var(--ink); }
|
||||
.draco-menu-sep { height: 1px; margin: 6px 10px; background: var(--line); }
|
||||
.draco-compte-nom {
|
||||
padding: 8px 10px 10px; font-weight: 700; font-size: 14px;
|
||||
|
||||
@@ -64,6 +64,25 @@
|
||||
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||
}
|
||||
|
||||
// Thème commun de la suite (HUB-18, fusion #132) : la barre
|
||||
// commune (barre v0.5.0) applique la préférence du Hub à
|
||||
// l'ouverture — directement sur data-theme depuis son cache, et
|
||||
// via l'événement « draco:theme » quand le fetch aboutit. Le mode
|
||||
// effectif est mémorisé dans la clé locale « theme » (convergence :
|
||||
// la préférence du Hub devient le choix local courant) ; le
|
||||
// sélecteur de thème de Flowdeck (Réglages) reste le maître pour
|
||||
// la suite de la session. Repli : si le Hub ne répond pas, aucun
|
||||
// événement n'arrive et le choix local ci-dessus reste seul
|
||||
// maître, comme avant. app.js (defer) s'exécute avant la barre :
|
||||
// l'écouteur est donc toujours posé à temps pour le fetch.
|
||||
document.addEventListener('draco:theme', function(e) {
|
||||
const mode = e && e.detail && e.detail.mode;
|
||||
if (mode === 'dark' || mode === 'light') {
|
||||
document.documentElement.setAttribute('data-theme', mode);
|
||||
try { localStorage.setItem('theme', mode); } catch (err) { /* stockage indisponible */ }
|
||||
}
|
||||
});
|
||||
|
||||
// Close context menu on click outside
|
||||
document.addEventListener('click', function(e) {
|
||||
const menu = document.getElementById('context-menu');
|
||||
|
||||
Vendored
+296
-15
@@ -14,6 +14,34 @@
|
||||
* contrat que les autres apps), entrée APP_LABELS ; l'onglet
|
||||
* (data-app="shaarit", après Kanbante, avant agent) est repris
|
||||
* automatiquement par la construction du menu de la marque.
|
||||
* v0.3.0 (carte HUB-1, fusion #115) : le Hub, point central de la
|
||||
* suite — nouvelle clé d'URL « hub » (jeton {{URL_HUB}} côté hôte,
|
||||
* DRACO_BAR_CONFIG.urls.hub), défaut officiel
|
||||
* https://hub.dracodev.net/, dérivation -dev hub-dev comme les
|
||||
* sœurs. Le menu compte gagne trois vrais liens (patron
|
||||
* buildAppsMenu : <a href>, classe draco-menu-item) — Télécharger
|
||||
* → <hub>/telecharger, À Propos → <hub>/a-propos, Aides →
|
||||
* <hub>/aide — et « Profil » devient lui aussi un lien, vers
|
||||
* <hub>/profil (il n'est plus relayé aux hôtes : un seul Profil,
|
||||
* celui du Hub). « Déconnexion » reste l'action relayée
|
||||
* (data-compte-act="deconnexion" / onAction), inchangée.
|
||||
* refreshMenuHrefs() resynchronise aussi les liens du Hub à
|
||||
* l'ouverture des menus. Le Hub n'a pas d'onglet dans la barre :
|
||||
* comme spark, sa clé est résolue et exposée via DRACO_BAR.urls,
|
||||
* sans étape « href rendu » (voir « URL » ci-dessous).
|
||||
* v0.4.0 (carte HUB-11, fusion #125) : icônes monoline dans le
|
||||
* menu compte — chaque item (les 4 liens du Hub et « Déconnexion »)
|
||||
* reçoit un glyphe SVG inline dans le style des icônes du rail de
|
||||
* la console (contour, trait fin régulier, currentColor, aucun
|
||||
* remplissage), aligné à gauche du libellé via .draco-menu-icone.
|
||||
* L'en-tête d'identité ne reçoit pas d'icône. Aucun changement de
|
||||
* comportement : liens résolus et Déconnexion relayée inchangés.
|
||||
* v0.5.0 (carte HUB-18, fusion #132) : thème commun de la suite —
|
||||
* la barre apprend la préférence de thème du profil du Hub
|
||||
* (GET <hub>/api/profil/theme, cookie draco) et l'applique à
|
||||
* l'ouverture par le mécanisme data-theme des hôtes, avec cache
|
||||
* local court et repli intégral sur le comportement actuel de
|
||||
* l'hôte si le Hub ne répond pas. Voir « Thème commun » plus bas.
|
||||
*
|
||||
* Aucun gestionnaire inline : un seul écouteur délégué sur le document
|
||||
* (patron validé par le prototype — les onclick du HTML généré sont
|
||||
@@ -33,7 +61,8 @@
|
||||
* kanbante: "https://kanbante.dracodev.net/",
|
||||
* shaarit: "https://shaarit.dracodev.net/",
|
||||
* agent: "https://agent.dracodev.net/",
|
||||
* spark: "https://spark.dracodev.net/"
|
||||
* spark: "https://spark.dracodev.net/",
|
||||
* hub: "https://hub.dracodev.net/"
|
||||
* },
|
||||
* onAction: function (act, el) { … } // l'app relaie les actions
|
||||
* } // qu'elle ne connaît pas
|
||||
@@ -67,6 +96,12 @@
|
||||
* la marque et DRACO_BAR.urls. Même résolution pour shaarit (v0.2.0) :
|
||||
* explicite > dérivation -dev shaarit-dev > href rendu
|
||||
* ({{URL_SHAARIT}}) > défaut officiel.
|
||||
* Cas hub (v0.3.0) : PAS d'onglet, donc pas d'étape « href rendu » —
|
||||
* explicite (DRACO_BAR_CONFIG.urls.hub, alimenté par le jeton
|
||||
* {{URL_HUB}} remplacé côté serveur par l'hôte) > dérivation -dev
|
||||
* hub-dev sur hôte -dev > défaut officiel
|
||||
* https://hub.dracodev.net/. La base résolue sert les quatre liens
|
||||
* du menu compte (telecharger, a-propos, aide, profil).
|
||||
* Actions émises (data-act) : rail, palette, domaine (data-dom),
|
||||
* alertes, spark, rappel-entete. L'app intégratrice décide de chacune
|
||||
* (ouvrir la palette, le panneau Spark, changer de domaine…).
|
||||
@@ -141,6 +176,14 @@
|
||||
* mais le menu affiche l'identité même sans gestionnaire hôte.
|
||||
* Accessibilité : vrais <button>, aria-haspopup / aria-expanded,
|
||||
* role="menu" / "menuitem", fermeture par Échap et clic extérieur.
|
||||
* Menu compte (v0.3.0) — Profil n'est PLUS relayé : c'est un lien
|
||||
* vers <hub>/profil, comme Télécharger / À Propos / Aides (liens
|
||||
* Hub, data-hub-link, href resynchronisés à chaque ouverture via
|
||||
* refreshCompteHrefs / refreshMenuHrefs). Seule « Déconnexion »
|
||||
* reste relayée à l'hôte (data-compte-act="deconnexion"), inchangée.
|
||||
* Les hôtes qui traitaient onAction("profil") (Kanbante → /profile)
|
||||
* voient ce comportement remplacé par le lien Hub — décision du
|
||||
* plan Hub : un seul Profil, celui du Hub.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
@@ -155,7 +198,8 @@
|
||||
kanbante: "https://kanbante.dracodev.net/",
|
||||
shaarit: "https://shaarit.dracodev.net/",
|
||||
agent: "https://agent.dracodev.net/",
|
||||
spark: "https://spark.dracodev.net/"
|
||||
spark: "https://spark.dracodev.net/",
|
||||
hub: "https://hub.dracodev.net/"
|
||||
};
|
||||
|
||||
/* Dérivation -dev (v0.1.6) — appliquée UNIQUEMENT quand l'hôte courant
|
||||
@@ -168,7 +212,8 @@
|
||||
tiktak: "https://tiktak-dev.dracodev.net/",
|
||||
kanbante: "https://kanbante-dev.dracodev.net/",
|
||||
shaarit: "https://shaarit-dev.dracodev.net/",
|
||||
agent: "https://agent-dev.dracodev.net/"
|
||||
agent: "https://agent-dev.dracodev.net/",
|
||||
hub: "https://hub-dev.dracodev.net/"
|
||||
};
|
||||
|
||||
var APP_LABELS = {
|
||||
@@ -229,6 +274,131 @@
|
||||
}
|
||||
var resolvedUrls = resolveUrls();
|
||||
|
||||
/* ── Thème commun (v0.5.0, carte HUB-18, fusion #132) ────────────
|
||||
* La préférence de thème de la suite vit dans le profil du Hub :
|
||||
* GET <hub>/api/profil/theme → {"theme": "sombre"|"clair"|"systeme"|null}
|
||||
* authentifié par le cookie draco (credentials: "include").
|
||||
* <hub> est l'URL RÉSOLUE ci-dessus (explicite > dérivation -dev >
|
||||
* défaut officiel) — jamais un nom figé de plus.
|
||||
* Application par le mécanisme data-theme existant des hôtes :
|
||||
* "sombre" → data-theme="dark", "clair" → data-theme="light",
|
||||
* "systeme" → attribut RETIRÉ (prefers-color-scheme décide, garde
|
||||
* html:not([data-theme]) du CSS de la barre comme des hôtes).
|
||||
* L'événement draco:theme ({theme, mode} — mode = "dark"/"light"
|
||||
* EFFECTIF) est ensuite dispatché pour les hôtes dont le mécanisme
|
||||
* propre doit suivre (icônes et libellés, feuilles de style,
|
||||
* palettes ShaarIt, attribut dédié de la console…) ; un hôte qui
|
||||
* s'initialise après la barre lit la même information dans
|
||||
* DRACO_BAR.themePref / DRACO_BAR.themeMode.
|
||||
* Ordre d'application : 1) le cache local FRAIS est appliqué de
|
||||
* façon SYNCHRONE à l'initialisation de la barre (script en defer
|
||||
* chez tous les hôtes : avant DOMContentLoaded, donc avant le
|
||||
* premier rendu dans le cas général) ; 2) un fetch ASYNCHRONE et
|
||||
* NON BLOQUANT rafraîchit le cache et ré-applique si la préférence
|
||||
* a changé. À la toute première visite (aucun cache), un bref
|
||||
* flash du thème local de l'hôte reste possible le temps du fetch :
|
||||
* c'est le prix du repli — l'état affiché transitoire est le thème
|
||||
* que l'hôte aurait de toute façon choisi seul, jamais un état
|
||||
* cassé, et les visites suivantes sont servies par le cache.
|
||||
* Cache : localStorage "draco-theme-hub" ({theme, ts}), TTL 12 h
|
||||
* (ordre de la journée) ; il ne contient QUE la préférence du Hub,
|
||||
* jamais le choix local de l'hôte.
|
||||
* REPLI INTÉGRAL : Hub injoignable, réponse non-OK (401…), JSON
|
||||
* invalide, theme null ou valeur inconnue → RIEN n'est appliqué et
|
||||
* RIEN n'est mis en cache ; un échec de fetch n'efface pas un cache
|
||||
* existant. Le comportement actuel de l'hôte (son localStorage,
|
||||
* puis le système) reste alors exactement le maître, comme avant
|
||||
* la v0.5.0. Le sélecteur de thème LOCAL de chaque hôte reste
|
||||
* fonctionnel : la barre n'applique qu'à l'ouverture (cache, puis
|
||||
* fetch) ; elle ne ré-applique ensuite que sur changement du
|
||||
* schéma système, et seulement tant que la préférence résolue est
|
||||
* "systeme". */
|
||||
var THEME_CACHE_CLE = "draco-theme-hub";
|
||||
var THEME_CACHE_TTL_MS = 12 * 60 * 60 * 1000;
|
||||
var themePref = null; /* "sombre"|"clair"|"systeme"|null (non résolue) */
|
||||
var themeMode = null; /* "dark"|"light"|null : mode effectif appliqué */
|
||||
|
||||
function themeSystemeEstSombre() {
|
||||
try {
|
||||
return !!(window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
function lireCacheTheme() {
|
||||
try {
|
||||
var brut = window.localStorage &&
|
||||
window.localStorage.getItem(THEME_CACHE_CLE);
|
||||
if (!brut) return null;
|
||||
var c = JSON.parse(brut);
|
||||
if (!c || typeof c.ts !== "number") return null;
|
||||
if (Date.now() - c.ts > THEME_CACHE_TTL_MS) return null;
|
||||
if (c.theme === "sombre" || c.theme === "clair" || c.theme === "systeme")
|
||||
return c.theme;
|
||||
} catch (e) { /* stockage indisponible ou corrompu : pas de cache */ }
|
||||
return null;
|
||||
}
|
||||
function ecrireCacheTheme(theme) {
|
||||
try {
|
||||
if (window.localStorage) {
|
||||
window.localStorage.setItem(THEME_CACHE_CLE,
|
||||
JSON.stringify({ theme: theme, ts: Date.now() }));
|
||||
}
|
||||
} catch (e) { /* stockage indisponible : le prochain fetch refera le travail */ }
|
||||
}
|
||||
function appliquerThemeCommun(theme) {
|
||||
var root = document.documentElement;
|
||||
if (!root) return;
|
||||
if (theme === "sombre" || theme === "clair") {
|
||||
themeMode = theme === "sombre" ? "dark" : "light";
|
||||
root.setAttribute("data-theme", themeMode);
|
||||
} else if (theme === "systeme") {
|
||||
themeMode = themeSystemeEstSombre() ? "dark" : "light";
|
||||
root.removeAttribute("data-theme");
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
themePref = theme;
|
||||
if (window.DRACO_BAR) {
|
||||
window.DRACO_BAR.themePref = themePref;
|
||||
window.DRACO_BAR.themeMode = themeMode;
|
||||
}
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent("draco:theme",
|
||||
{ detail: { theme: themePref, mode: themeMode } }));
|
||||
} catch (e) { /* aucun écouteur hôte : data-theme suffit */ }
|
||||
}
|
||||
function initThemeCommun() {
|
||||
var enCache = lireCacheTheme();
|
||||
if (enCache) appliquerThemeCommun(enCache);
|
||||
/* Suivi du schéma système tant que la préférence est "systeme" :
|
||||
* sans data-theme, le CSS suit tout seul ; l'événement est
|
||||
* re-dispatché pour les hôtes qui matérialisent le mode. */
|
||||
try {
|
||||
var mq = window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)");
|
||||
if (mq && mq.addEventListener) {
|
||||
mq.addEventListener("change", function () {
|
||||
if (themePref === "systeme") appliquerThemeCommun("systeme");
|
||||
});
|
||||
}
|
||||
} catch (e) { /* pas de matchMedia : pas de suivi */ }
|
||||
if (typeof window.fetch !== "function") return;
|
||||
var url = withSlash(resolvedUrls.hub || DEFAULT_URLS.hub) +
|
||||
"api/profil/theme";
|
||||
window.fetch(url, { credentials: "include" }).then(function (resp) {
|
||||
if (!resp || !resp.ok) return null;
|
||||
return resp.json();
|
||||
}).then(function (data) {
|
||||
if (!data) return;
|
||||
var t = data.theme;
|
||||
if (t === "sombre" || t === "clair" || t === "systeme") {
|
||||
ecrireCacheTheme(t);
|
||||
appliquerThemeCommun(t);
|
||||
}
|
||||
/* theme null ou inconnu : repli intégral, rien n'est touché. */
|
||||
}).catch(function () { /* Hub injoignable : repli intégral */ });
|
||||
}
|
||||
|
||||
function setCounts(counts) {
|
||||
Object.keys(counts || {}).forEach(function (k) {
|
||||
/* Toutes les occurrences : l'onglet ET sa ligne dans le menu. */
|
||||
@@ -307,6 +477,28 @@
|
||||
menu.querySelectorAll(".draco-apps-item").forEach(function (a) {
|
||||
if (a.dataset.app && resolvedUrls[a.dataset.app]) a.setAttribute("href", resolvedUrls[a.dataset.app]);
|
||||
});
|
||||
/* v0.3.0 : les liens du Hub (menu compte) sont resynchronisés
|
||||
* eux aussi à l'ouverture d'un menu, pour refléter setUrls et
|
||||
* la dérivation -dev même s'ils ont été construits avant. */
|
||||
refreshCompteHrefs();
|
||||
}
|
||||
/* Liens du Hub dans le menu compte (v0.3.0) : chemin → libellé.
|
||||
* « Profil » est le quatrième lien (vers <hub>/profil) ; il
|
||||
* remplace l'ancien item relayé « profil ». */
|
||||
var HUB_LINKS = [
|
||||
["telecharger", "Télécharger"],
|
||||
["a-propos", "À Propos"],
|
||||
["aide", "Aides"],
|
||||
["profil", "Profil"]
|
||||
];
|
||||
function hubHref(path) {
|
||||
return withSlash(resolvedUrls.hub || DEFAULT_URLS.hub) + path;
|
||||
}
|
||||
function refreshCompteHrefs() {
|
||||
if (!compteMenu) return;
|
||||
compteMenu.querySelectorAll("[data-hub-link]").forEach(function (a) {
|
||||
a.setAttribute("href", hubHref(a.getAttribute("data-hub-link")));
|
||||
});
|
||||
}
|
||||
function togglePopup(menu, btn, force) {
|
||||
if (!menu) return;
|
||||
@@ -429,10 +621,80 @@
|
||||
if (!appsMenu.hidden) lastOpener = brand;
|
||||
}
|
||||
|
||||
/* Menu compte (v0.1.6) — en-tête d'identité + Profil / Déconnexion
|
||||
* relayés à l'hôte. L'en-tête est rafraîchi à chaque ouverture, pour
|
||||
* refléter un title posé après coup par l'hôte (ex. ObsiGate, qui
|
||||
* remplit l'avatar depuis /api/auth/me après chargement). */
|
||||
/* Icônes monoline du menu compte (v0.4.0) — glyphes contour
|
||||
* dans le style des icônes du rail de la console : viewBox 24,
|
||||
* stroke currentColor, remplissage nul, extrémités rondes.
|
||||
* Chaque glyphe est une liste de primitives [balise, attributs]
|
||||
* construites en DOM (jamais d'innerHTML), comme le reste du
|
||||
* fichier. Le libellé reste le nœud texte de l'item ; l'icône,
|
||||
* appendue après, est replacée à gauche par le CSS (order: -1
|
||||
* sur .draco-menu-icone, l'item étant déjà en flex). */
|
||||
var MENU_ICONES = {
|
||||
/* Flèche vers le bas dans un plateau. */
|
||||
"telecharger": [
|
||||
["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }],
|
||||
["polyline", { points: "7 10 12 15 17 10" }],
|
||||
["line", { x1: "12", y1: "15", x2: "12", y2: "3" }]
|
||||
],
|
||||
/* Cercle d'information. */
|
||||
"a-propos": [
|
||||
["circle", { cx: "12", cy: "12", r: "9" }],
|
||||
["line", { x1: "12", y1: "11", x2: "12", y2: "16" }],
|
||||
["line", { x1: "12", y1: "8", x2: "12.01", y2: "8" }]
|
||||
],
|
||||
/* Cercle d'interrogation. */
|
||||
"aide": [
|
||||
["circle", { cx: "12", cy: "12", r: "9" }],
|
||||
["path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }],
|
||||
["line", { x1: "12", y1: "17", x2: "12.01", y2: "17" }]
|
||||
],
|
||||
/* Personne : tête + épaules. */
|
||||
"profil": [
|
||||
["path", { d: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" }],
|
||||
["circle", { cx: "12", cy: "7", r: "4" }]
|
||||
],
|
||||
/* Flèche sortant d'un cadre (porte). */
|
||||
"deconnexion": [
|
||||
["path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }],
|
||||
["polyline", { points: "16 17 21 12 16 7" }],
|
||||
["line", { x1: "21", y1: "12", x2: "9", y2: "12" }]
|
||||
]
|
||||
};
|
||||
/* Espace de noms SVG — OBLIGATOIRE : un élément « svg » (ou
|
||||
* « path », « circle »…) créé par createElement() appartient à
|
||||
* l'espace de noms HTML et le navigateur ne le dessine JAMAIS
|
||||
* (bug v0.4.0 : libellés décalés d'un cran d'icône, aucun glyphe
|
||||
* visible ; corrigé en v0.4.1, carte fusion #125). */
|
||||
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||
function menuIcone(nom) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "draco-menu-icone";
|
||||
span.setAttribute("aria-hidden", "true");
|
||||
var svg = document.createElementNS(SVG_NS, "svg");
|
||||
svg.setAttribute("viewBox", "0 0 24 24");
|
||||
svg.setAttribute("fill", "none");
|
||||
svg.setAttribute("stroke", "currentColor");
|
||||
svg.setAttribute("stroke-width", "1.8");
|
||||
svg.setAttribute("stroke-linecap", "round");
|
||||
svg.setAttribute("stroke-linejoin", "round");
|
||||
svg.setAttribute("focusable", "false");
|
||||
(MENU_ICONES[nom] || []).forEach(function (spec) {
|
||||
var el = document.createElementNS(SVG_NS, spec[0]);
|
||||
for (var k in spec[1]) el.setAttribute(k, spec[1][k]);
|
||||
svg.appendChild(el);
|
||||
});
|
||||
span.appendChild(svg);
|
||||
return span;
|
||||
}
|
||||
|
||||
/* Menu compte (v0.1.6 ; v0.3.0 pour les liens) — en-tête
|
||||
* d'identité, puis les liens du Hub (Télécharger, À Propos,
|
||||
* Aides, Profil — vrais <a href>, même patron que le menu des
|
||||
* applications), un séparateur, puis « Déconnexion » relayée à
|
||||
* l'hôte. L'en-tête et les href sont rafraîchis à chaque
|
||||
* ouverture, pour refléter un title posé après coup par l'hôte
|
||||
* (ex. ObsiGate, qui remplit l'avatar depuis /api/auth/me après
|
||||
* chargement) et la résolution d'URL la plus récente. */
|
||||
function buildCompteMenu() {
|
||||
if (!bar || !avatarBtn || compteMenu) return;
|
||||
compteMenu = document.createElement("div");
|
||||
@@ -443,15 +705,28 @@
|
||||
var head = document.createElement("div");
|
||||
head.className = "draco-compte-nom";
|
||||
compteMenu.appendChild(head);
|
||||
[["profil", "Profil"], ["deconnexion", "Déconnexion"]].forEach(function (pair) {
|
||||
var b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = "draco-menu-item";
|
||||
b.setAttribute("data-compte-act", pair[0]);
|
||||
b.setAttribute("role", "menuitem");
|
||||
b.textContent = pair[1];
|
||||
compteMenu.appendChild(b);
|
||||
HUB_LINKS.forEach(function (pair) {
|
||||
var a = document.createElement("a");
|
||||
a.className = "draco-menu-item";
|
||||
a.href = hubHref(pair[0]);
|
||||
a.setAttribute("data-hub-link", pair[0]);
|
||||
a.setAttribute("role", "menuitem");
|
||||
a.textContent = pair[1];
|
||||
a.appendChild(menuIcone(pair[0]));
|
||||
compteMenu.appendChild(a);
|
||||
});
|
||||
var sep = document.createElement("div");
|
||||
sep.className = "draco-menu-sep";
|
||||
sep.setAttribute("aria-hidden", "true");
|
||||
compteMenu.appendChild(sep);
|
||||
var b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = "draco-menu-item";
|
||||
b.setAttribute("data-compte-act", "deconnexion");
|
||||
b.setAttribute("role", "menuitem");
|
||||
b.textContent = "Déconnexion";
|
||||
b.appendChild(menuIcone("deconnexion"));
|
||||
compteMenu.appendChild(b);
|
||||
bar.appendChild(compteMenu);
|
||||
}
|
||||
function refreshCompteMenu() {
|
||||
@@ -459,6 +734,7 @@
|
||||
var head = compteMenu.querySelector(".draco-compte-nom");
|
||||
var name = displayName();
|
||||
if (head) head.textContent = name || "Compte";
|
||||
refreshCompteHrefs();
|
||||
}
|
||||
function toggleCompteMenu(force) {
|
||||
buildCompteMenu();
|
||||
@@ -550,11 +826,15 @@
|
||||
window.DRACO_BAR = {
|
||||
setCounts: setCounts, setCurrent: setCurrent, setDomain: setDomain,
|
||||
urls: resolvedUrls,
|
||||
/* Thème commun (v0.5.0) : préférence du Hub résolue et mode
|
||||
* effectif appliqué ; null tant que rien n'a été résolu (repli). */
|
||||
themePref: null, themeMode: null,
|
||||
setUrls: function (urls) {
|
||||
cfg.urls = Object.assign({}, cfg.urls, urls || {});
|
||||
resolvedUrls = resolveUrls();
|
||||
window.DRACO_BAR.urls = resolvedUrls;
|
||||
applyUrls(resolvedUrls);
|
||||
refreshCompteHrefs();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -563,6 +843,7 @@
|
||||
buildCompteMenu();
|
||||
syncAvatar();
|
||||
refreshCompteMenu();
|
||||
initThemeCommun();
|
||||
if (cfg.current) setCurrent(cfg.current);
|
||||
if (cfg.domain) setDomain(cfg.domain);
|
||||
if (cfg.counts) setCounts(cfg.counts);
|
||||
|
||||
@@ -53,6 +53,55 @@ def test_barre_v020_onglet_shaarit_et_garde_data_theme():
|
||||
assert 'shaarit: "https://shaarit-dev.dracodev.net/"' in js
|
||||
|
||||
|
||||
def test_barre_v030_liens_hub_menu_compte():
|
||||
"""Barre v0.3.0 (commons, HUB-1 / fusion #115) : JS vendangé avec
|
||||
la clé hub (défaut + dérivation hub-dev) et les liens du menu
|
||||
compte ; le gabarit passe draco_urls().hub en urls.hub."""
|
||||
js = (ROOT / "static/js/vendor/draco-barre.js").read_text()
|
||||
assert 'hub: "https://hub.dracodev.net/"' in js
|
||||
assert 'hub: "https://hub-dev.dracodev.net/"' in js
|
||||
assert '"telecharger", "Télécharger"' in js
|
||||
assert '"a-propos", "À Propos"' in js
|
||||
assert '"aide", "Aides"' in js
|
||||
assert '"profil", "Profil"' in js
|
||||
assert "data-hub-link" in js
|
||||
assert 'data-compte-act", "profil"' not in js
|
||||
assert 'data-compte-act", "deconnexion"' in js
|
||||
gabarit = (ROOT / "app/templates/_draco_bar.html").read_text()
|
||||
rendu = _hors_commentaires(gabarit)
|
||||
assert 'urls: { hub: "{{ draco_urls().hub }}/" }' in rendu
|
||||
|
||||
|
||||
def test_barre_v040_icones_menu_compte():
|
||||
"""Barre v0.4.0 (commons, HUB-11 / fusion #125) : les items du
|
||||
menu compte portent des icônes SVG monoline (construites en DOM,
|
||||
currentColor, sans remplissage) ; le libellé reste un nœud
|
||||
texte ; l'en-tête d'identité reste sans icône."""
|
||||
js = (ROOT / "static/js/vendor/draco-barre.js").read_text()
|
||||
assert "MENU_ICONES" in js
|
||||
for nom in ("telecharger", "a-propos", "aide", "profil", "deconnexion"):
|
||||
assert f'"{nom}"' in js
|
||||
assert "draco-menu-icone" in js
|
||||
assert 'setAttribute("stroke", "currentColor")' in js
|
||||
assert 'setAttribute("fill", "none")' in js
|
||||
css = (ROOT / "static/css/draco-barre.css").read_text()
|
||||
assert ".draco-menu-icone" in css
|
||||
assert "width: 18px" in css
|
||||
|
||||
|
||||
def test_barre_v041_correctif_icones_espace_de_noms_svg():
|
||||
"""Barre v0.4.1 (commons, correctif fusion #125) : les icônes
|
||||
du menu compte sont créées via createElementNS (espace de
|
||||
noms SVG). En v0.4.0, createElement() les créait dans
|
||||
l'espace de noms HTML : jamais dessinées par le navigateur,
|
||||
libellés décalés d'un cran d'icône sans aucun glyphe."""
|
||||
js = (ROOT / "static/js/vendor/draco-barre.js").read_text()
|
||||
assert 'createElementNS(SVG_NS, "svg")' in js
|
||||
assert "createElementNS(SVG_NS, spec[0])" in js
|
||||
assert 'createElement("svg")' not in js
|
||||
assert "createElement(spec[0])" not in js
|
||||
|
||||
|
||||
def test_barre_intacte_apres_suppression():
|
||||
"""Le 5e onglet Console Spark et la marque survivent a la suppression."""
|
||||
gabarit = (ROOT / "app/templates/_draco_bar.html").read_text()
|
||||
@@ -66,3 +115,19 @@ def test_barre_intacte_apres_suppression():
|
||||
# draco-barre.js depuis les onglets : l'onglet agent suffit a l'alimenter.
|
||||
js_barre = (ROOT / "static/js/vendor/draco-barre.js").read_text()
|
||||
assert "Console Spark" in js_barre
|
||||
|
||||
|
||||
def test_barre_v050_theme_commun_hub18():
|
||||
# v0.5.0 (HUB-18, fusion #132) : la barre apprend la préférence
|
||||
# de thème du Hub et l'applique à l'ouverture. Vendange verbatim
|
||||
# commons ; l'initialisation propre à Flowdeck vit dans app.js
|
||||
# (écouteur draco:theme → data-theme + clé locale « theme »).
|
||||
js = (ROOT / "static/js/vendor/draco-barre.js").read_text(encoding="utf-8")
|
||||
assert "api/profil/theme" in js
|
||||
assert 'credentials: "include"' in js
|
||||
assert "draco-theme-hub" in js
|
||||
assert 'CustomEvent("draco:theme"' in js
|
||||
assert "themePref" in js and "themeMode" in js
|
||||
assert "resolvedUrls.hub" in js
|
||||
app = (ROOT / "static/js/app.js").read_text(encoding="utf-8")
|
||||
assert 'addEventListener("draco:theme"' in app or "addEventListener('draco:theme'" in app
|
||||
|
||||
@@ -12,7 +12,7 @@ import pytest
|
||||
from app.services import draco_urls
|
||||
|
||||
VARS = ("DRACO_SSO_URL", "DRACO_AGENT_URL", "TIKTAK_URL", "KANBANTE_URL",
|
||||
"SHAARIT_URL", "FLOWDECK_URL", "OAUTH_REDIRECT_URI",
|
||||
"SHAARIT_URL", "HUB_URL", "FLOWDECK_URL", "OAUTH_REDIRECT_URI",
|
||||
"AGENT_EVENEMENTS_URL")
|
||||
|
||||
|
||||
@@ -63,6 +63,24 @@ def test_shaarit_defaut_derivation_et_explicite(monkeypatch, avec_hote):
|
||||
assert draco_urls.shaarit_url() == "https://bm.example.net"
|
||||
|
||||
|
||||
def test_hub_defaut_derivation_et_explicite(monkeypatch, avec_hote):
|
||||
# Barre v0.3.0 (HUB-1) : le Hub suit le même mécanisme que les
|
||||
# autres sœurs — défaut officiel, dérivation -dev (agent connu
|
||||
# puis hôte de la requête), explicite prioritaire. Pas d'onglet :
|
||||
# sa base sert les liens du menu compte via urls.hub.
|
||||
assert draco_urls.hub_url() == "https://hub.dracodev.net"
|
||||
assert draco_urls.toutes()["hub"] == "https://hub.dracodev.net"
|
||||
monkeypatch.setenv(
|
||||
"AGENT_EVENEMENTS_URL",
|
||||
"https://agent-dev.dracodev.net/api/v1/evenements")
|
||||
assert draco_urls.hub_url() == "https://hub-dev.dracodev.net"
|
||||
monkeypatch.delenv("AGENT_EVENEMENTS_URL")
|
||||
avec_hote("flowdeck-dev.dracodev.net")
|
||||
assert draco_urls.hub_url() == "https://hub-dev.dracodev.net"
|
||||
monkeypatch.setenv("HUB_URL", "https://hub.example.net/")
|
||||
assert draco_urls.hub_url() == "https://hub.example.net"
|
||||
|
||||
|
||||
def test_flowdeck_url_derivée_oauth_redirect(monkeypatch):
|
||||
monkeypatch.setenv(
|
||||
"OAUTH_REDIRECT_URI", "https://flowdeck-dev.dracodev.net/auth/callback")
|
||||
|
||||
@@ -4,7 +4,10 @@ Le cookie émis par ObsiGate (ACC-3.1) est vérifié localement
|
||||
(secret partagé, HS256), résolu vers le compte Flowdeck
|
||||
(draco_links, migration 51 ; repli mono-utilisateur), et converti en
|
||||
session native par GET /auth/draco. Sans DRACO_SESSION_SECRET, le
|
||||
pont renvoie simplement à la connexion locale.
|
||||
pont renvoie simplement à la connexion locale. HUB-15 ajoute le
|
||||
provisionnement JIT : aucun compte correspondant → le pendant local
|
||||
est créé depuis les claims (rôle standard, jamais admin, sans mot
|
||||
de passe local), sauf pierre tombale de dissociation.
|
||||
"""
|
||||
|
||||
import base64
|
||||
@@ -21,13 +24,13 @@ from app.services import draco
|
||||
SECRET = "secret-partage-de-test"
|
||||
|
||||
|
||||
def _jeton(sub="admin-1", secret=SECRET, exp=None, jti="jti-test"):
|
||||
def _jeton(sub="admin-1", secret=SECRET, exp=None, jti="jti-test", name="Bruno"):
|
||||
def b64(d: bytes) -> str:
|
||||
return base64.urlsafe_b64encode(d).rstrip(b"=").decode()
|
||||
|
||||
header = b64(json.dumps({"alg": "HS256", "typ": "JWT"}).encode())
|
||||
payload = b64(json.dumps({
|
||||
"sub": sub, "name": "Bruno", "iat": int(time.time()),
|
||||
"sub": sub, "name": name, "iat": int(time.time()),
|
||||
"exp": exp if exp is not None else int(time.time()) + 3600,
|
||||
"jti": jti,
|
||||
}).encode())
|
||||
@@ -101,13 +104,74 @@ def test_lien_requis_en_multi_utilisateurs(client):
|
||||
"INSERT INTO users (login, email, password_hash, full_name, is_active) "
|
||||
"VALUES ('deuxieme', '[email protected]', 'x', 'Deuxième', 1)")
|
||||
conn.commit()
|
||||
# Sans lien et avec deux utilisateurs actifs : le pont refuse.
|
||||
client.cookies.set("draco_session", _jeton(sub="og-inconnu"))
|
||||
r = client.get("/auth/draco", follow_redirects=False)
|
||||
assert r.headers["location"] == "/auth/login?provider=local"
|
||||
# Le lien explicite (créé pendant une visite connectée) rouvre la porte.
|
||||
# Le lien explicite (créé pendant une visite connectée) ouvre la porte.
|
||||
client.cookies.set("draco_session", _jeton(sub="og-bruno-1"))
|
||||
assert client.post("/auth/draco-link").json()["sub"] == "og-bruno-1"
|
||||
client.cookies.delete("flowdeck_session")
|
||||
r = client.get("/auth/draco", follow_redirects=False)
|
||||
assert r.headers["location"] == "/workspaces"
|
||||
|
||||
|
||||
# ── HUB-15 : provisionnement JIT ─────────────────────────────────────
|
||||
|
||||
def _ajoute_deuxieme_utilisateur():
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO users (login, email, password_hash, full_name, is_active) "
|
||||
"VALUES ('deuxieme', '[email protected]', 'x', 'Deuxième', 1)")
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _compte_utilisateurs():
|
||||
with get_conn() as conn:
|
||||
return conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
|
||||
|
||||
|
||||
def test_jit_cree_le_pendant_local_en_multi_utilisateurs(client):
|
||||
_ajoute_deuxieme_utilisateur()
|
||||
avant = _compte_utilisateurs()
|
||||
# Sub inconnu, sans lien, en multi-utilisateurs : avant HUB-15 le
|
||||
# pont refusait ; désormais le pendant local est créé en JIT.
|
||||
client.cookies.set("draco_session", _jeton(sub="og-nouveau", name="Nouvelle"))
|
||||
r = client.get("/auth/draco", follow_redirects=False)
|
||||
assert r.headers["location"] == "/workspaces"
|
||||
assert _compte_utilisateurs() == avant + 1
|
||||
with get_conn() as conn:
|
||||
user = conn.execute(
|
||||
"SELECT * FROM users WHERE login = 'draco-og-nouveau'").fetchone()
|
||||
assert user is not None
|
||||
assert user["full_name"] == "Nouvelle"
|
||||
assert user["is_admin"] == 0 # jamais admin
|
||||
assert user["is_active"] == 1
|
||||
assert user["password_hash"] is None # aucun mot de passe local
|
||||
assert user["auth_method"] == "draco"
|
||||
lien = conn.execute(
|
||||
"SELECT * FROM draco_links WHERE sub = 'og-nouveau'").fetchone()
|
||||
assert lien is not None and lien["user_id"] == user["id"]
|
||||
assert lien["unlinked_at"] is None
|
||||
# Deuxième arrivée : même compte réutilisé, aucune création de plus.
|
||||
client.cookies.delete("flowdeck_session")
|
||||
r = client.get("/auth/draco", follow_redirects=False)
|
||||
assert r.headers["location"] == "/workspaces"
|
||||
assert _compte_utilisateurs() == avant + 1
|
||||
# Le compte JIT ne peut pas se connecter par mot de passe local.
|
||||
r = client.post("/auth/local-login",
|
||||
json={"email": "draco-og-nouveau", "password": "nimporte"})
|
||||
assert r.status_code == 401
|
||||
|
||||
|
||||
def test_jit_pierre_tombale_sans_recreation(client):
|
||||
_ajoute_deuxieme_utilisateur()
|
||||
client.cookies.set("draco_session", _jeton(sub="og-ephemere"))
|
||||
r = client.get("/auth/draco", follow_redirects=False)
|
||||
assert r.headers["location"] == "/workspaces" # compte JIT créé
|
||||
# La session courante est celle du compte JIT : dissociation.
|
||||
r = client.delete("/auth/draco-link")
|
||||
assert r.json()["ok"] is True
|
||||
apres_creation = _compte_utilisateurs()
|
||||
# Nouvelle arrivée du même sub : la pierre tombale interdit toute
|
||||
# re-création automatique — retour à la connexion locale.
|
||||
client.cookies.delete("flowdeck_session")
|
||||
r = client.get("/auth/draco", follow_redirects=False)
|
||||
assert r.headers["location"] == "/auth/login?provider=local"
|
||||
assert _compte_utilisateurs() == apres_creation
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Recherche fédérée « Rechercher partout » (fusion #111) — garde-fous
|
||||
côté Flowdeck : l'API /api/search et les origines sœurs sont couvertes
|
||||
par les suites Node et les tests CORS ; ici, la CSP des pages, qui
|
||||
conditionne les appels cross-origin de la page /recherche."""
|
||||
|
||||
|
||||
def test_csp_connect_src_contient_shaarit(client):
|
||||
"""La CSP des pages doit autoriser l'origine de ShaarIt en
|
||||
connect-src, sinon le navigateur bloque l'appel de la page
|
||||
/recherche avant même le CORS et la section ShaarIt affiche
|
||||
« Injoignable depuis cet hôte » (retour d'usage du 2026-11-04 :
|
||||
la liste des sœurs du connect-src, écrite pour les Alertes
|
||||
#69, n'avait pas été étendue à ShaarIt)."""
|
||||
r = client.get("/")
|
||||
csp = r.headers.get("content-security-policy", "")
|
||||
assert "connect-src" in csp
|
||||
assert "https://shaarit.dracodev.net" in csp, csp
|
||||
Reference in New Issue
Block a user