Compare commits

...
10 Commits
Author SHA1 Message Date
bruno 5203b6288e Merge pull request 'Flowdeck 7.85.1 — HUB-18 : thème commun, barre v0.5.0 + initialisation thème Hub (fusion #132)' (#52) from hub18-theme-commun into main
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-11-06 09:47:54 -05:00
Spark 50d7f337da Flowdeck 7.85.1 — HUB-18 : thème commun, barre v0.5.0 + initialisation thème Hub (fusion #132)
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 1s
FlowDeck CI / test (pull_request) Failing after 1s
FlowDeck CI / docker (pull_request) Skipped
2026-11-06 14:47:02 +00:00
bruno 0eec63a5ca Merge pull request 'Flowdeck 7.85.0 — HUB-15 : provisionnement JIT des comptes draco (fusion #129)' (#51) from hub15-jit into main
FlowDeck CI / lint (push) Failing after 13s
FlowDeck CI / test (push) Failing after 30m40s
FlowDeck CI / docker (push) Skipped
2026-11-06 09:44:20 -05:00
Spark 296d9cfa25 Flowdeck 7.85.0 — HUB-15 : provisionnement JIT des comptes draco (fusion #129)
FlowDeck CI / test (push) Failing after 3s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 13s
FlowDeck CI / test (pull_request) Failing after 1s
FlowDeck CI / docker (pull_request) Skipped
FlowDeck CI / lint (pull_request) Failing after 12s
Généralisation de maybe_link : à la première connexion draco, si
aucun compte local ne correspond au sub (ni lien draco_links, ni
repli mono-utilisateur), le pendant local est créé depuis les
claims (sub, name) et lié dans la même transaction. Rôle par
défaut sûr : utilisateur standard (is_admin=0), auth_method
'draco', password_hash NULL (aucune connexion locale par mot de
passe pour ce compte ; inchangée pour les autres). Garde pierre
tombale : une ligne draco_links existante interdit toute création.
Connexion locale et OAuth Gitea/GitHub intacts.

Tests : test_fusion_draco.py étendu (création JIT, pierre
tombale) ; 9/9 sur le fichier, balayage auth 226 réussis
(3 échecs préexistants sur main vierge, réseau).
2026-11-06 14:03:23 +00:00
bruno be3e4e5f69 Merge pull request 'Flowdeck 7.84.15 — barre v0.4.1 : correctif des icônes invisibles du menu compte (#125)' (#50) from hub12-barre-v041 into main
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-11-05 02:09:32 -05:00
Spark 54bd43e9f2 Flowdeck 7.84.15 : barre v0.4.1 — menu compte resserré à 232 px, re-vendange verbatim (fusion #125)
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 2s
FlowDeck CI / test (pull_request) Failing after 1s
FlowDeck CI / docker (pull_request) Skipped
FlowDeck CI / lint (push) Failing after 13s
2026-11-05 07:08:06 +00:00
Spark 9068e1572f Flowdeck 7.84.15 : barre v0.4.1 — correctif des icônes invisibles du menu compte, vendange verbatim (fusion #125) + garde + version/changelog
FlowDeck CI / lint (pull_request) Failing after 1s
FlowDeck CI / test (pull_request) Failing after 1s
FlowDeck CI / docker (pull_request) Skipped
FlowDeck CI / lint (push) Failing after 2s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-11-05 05:52:32 +00:00
bruno 23bcea4b56 Merge pull request '7.84.14 — barre v0.4.0 : icônes monoline dans le menu compte (HUB-11)' (#49) from hub11-barre-v040 into main
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-11-05 00:33:58 -05:00
Spark e7c65b6f49 Flowdeck 7.84.14 : garde v0.4.0 corrigée (clés JS quotées dans MENU_ICONES)
FlowDeck CI / test (push) Failing after 3s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 14s
FlowDeck CI / lint (pull_request) Failing after 5s
FlowDeck CI / test (pull_request) Failing after 2s
FlowDeck CI / docker (pull_request) Skipped
2026-11-05 05:05:32 +00:00
Spark 77b4ee29e6 Flowdeck 7.84.14 : barre v0.4.0 — icônes monoline du menu compte, vendange verbatim (HUB-11) + garde + version/changelog 2026-11-05 05:05:11 +00:00
9 changed files with 483 additions and 12 deletions
+32
View File
@@ -1,3 +1,35 @@
## 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é.
+1 -1
View File
@@ -1 +1 @@
7.84.13
7.85.1
+1 -1
View File
@@ -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()
+85 -1
View File
@@ -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
+17 -1
View File
@@ -24,7 +24,14 @@
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>). */
@@ -82,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);
}
@@ -94,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;
+19
View File
@@ -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');
+210
View File
@@ -29,6 +29,19 @@
* 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
@@ -261,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. */
@@ -483,6 +621,72 @@
if (!appsMenu.hidden) lastOpener = brand;
}
/* 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
@@ -508,6 +712,7 @@
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");
@@ -520,6 +725,7 @@
b.setAttribute("data-compte-act", "deconnexion");
b.setAttribute("role", "menuitem");
b.textContent = "Déconnexion";
b.appendChild(menuIcone("deconnexion"));
compteMenu.appendChild(b);
bar.appendChild(compteMenu);
}
@@ -620,6 +826,9 @@
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();
@@ -634,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);
+46
View File
@@ -72,6 +72,36 @@ def test_barre_v030_liens_hub_menu_compte():
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()
@@ -85,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
+72 -8
View File
@@ -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