diff --git a/CHANGELOG.md b/CHANGELOG.md index 63719f5..a370fb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). > **En cours de développement** : les changements à venir sont listés dans la section -> [Unreleased](#unreleased). La dernière version livrée est **2.84.0**. +> [Unreleased](#unreleased). La dernière version livrée est **2.84.1**. --- @@ -14,6 +14,12 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.84.1] — 2026-11-06 + +- **HUB-18 — Thème commun (fusion #132)** : barre commune re-vendangée **verbatim** en **v0.5.0** depuis dracodev-commons (branche `hub18-theme-commun`, PR commons #12) — `frontend/draco-barre.js` seul change côté barre (CSS et fragment inchangés). La barre apprend la préférence de thème du profil du Hub (`GET /api/profil/theme`, cookie draco ; `` = 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 à ObsiGate dans `frontend/app.js` : `initTheme()` écoute l'événement `draco:theme` de la barre et route le mode effectif par `applyTheme()` — icône et libellé du bouton de thème et feuilles highlight.js restent synchronisés, le mode appliqué devient le choix local mémorisé (`obsigate-theme`), et le bouton de bascule reste fonctionnel pour la session. Garde ajoutée à `tests/test_draco_barre.py`. + +--- + ## [2.84.0] — 2026-11-06 - **HUB-13 — API d'administration des comptes, machine-à-machine (fusion #127, #226)** : nouvelles routes `/api/admin/machine/comptes` (liste, profil étendu, création, modification, désactivation/réactivation, suppression, réinitialisation du mot de passe et du MFA), protégées par la clé machine dédiée `DRACO_ADMIN_MACHINE_KEY` (en-tête `X-Draco-Machine-Key`, distincte de `DRACO_SSO_MACHINE_KEY` ; aucune session utilisateur ne les ouvre ; API inerte si la clé n'est pas configurée). Le profil étendu expose nom d'utilisateur, nom affiché, langue, avatar (valeur + présence), rôle global, état actif, MFA activé oui/non — jamais le secret TOTP ni le haché du mot de passe. La désactivation arrête l'émission de nouveaux jetons et révoque les `jti` draco actifs : `draco_session` tient désormais un registre des émissions (`data/draco_issued.json`) et `revoke_all_for_user()` révoque via la liste existante ; le repère `password_changed_at` (BUG-028) est aussi avancé. Nouveau champ utilisateur `must_change_password` : posé à la création machine et à la réinitialisation du mot de passe, signalé par la connexion et `GET /api/auth/me`, effacé au changement effectif. Chaque action est journalisée dans `data/audit.log` (acteur = identité de service `X-Draco-Actor`, action, compte cible, horodatage). Fiche : `docs/features/hub-api-admin-comptes-226.md`. Tests : `tests/test_comptes_machine.py`. diff --git a/README.fr.md b/README.fr.md index c614fa6..36571c9 100644 --- a/README.fr.md +++ b/README.fr.md @@ -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. -[![Version](https://img.shields.io/badge/Version-2.84.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.84.1-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -977,8 +977,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 Changelog -Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.84.0). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.84.1). --- -*Projet : ObsiGate | Version : 2.84.0 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.84.1 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 7a4946c..fc24394 100644 --- a/README.md +++ b/README.md @@ -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. -[![Version](https://img.shields.io/badge/Version-2.84.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.84.1-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1152,8 +1152,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 Changelog -See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.84.0). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.84.1). --- -*Project: ObsiGate | Version: 2.84.0 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.84.1 | Last updated: September 2026* diff --git a/VERSION b/VERSION index b52112a..0bdeb73 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.84.0 +2.84.1 diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 9975fe0..19f1087 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.84.0" +version = "2.84.1" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 5c2e546..90afab7 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.84.0" +version = "2.84.1" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 147f1cc..3e2aafe 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.84.0", + "version": "2.84.1", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 40e082a..341348e 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.84.0 | **Dernière mise à jour :** 2026-11-06 +> **Version :** 2.84.1 | **Dernière mise à jour :** 2026-11-06 > **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact > vers les fonctionnalités livrées. > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** diff --git a/frontend/app.js b/frontend/app.js index 4fae22d..9ad1664 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1214,6 +1214,30 @@ function initTheme() { const saved = localStorage.getItem("obsigate-theme") || "dark"; applyTheme(saved); + // Thème commun de la suite (HUB-18, fusion #132) : la barre + // commune (barre v0.5.0) applique la préférence du Hub + // (sombre/clair/système) à l'ouverture — directement sur + // data-theme, et via l'événement « draco:theme » pour que le + // mécanisme propre d'ObsiGate suive (icône et libellé du bouton, + // feuilles highlight.js). applyTheme mémorise le mode appliqué + // dans « obsigate-theme » : la préférence du Hub devient le + // choix local courant, et le bouton de bascule reste le maître + // pour la suite de la session (aucune ré-application de la barre + // après l'ouverture). « systeme » arrive déjà résolu en mode + // effectif (detail.mode) par la barre. 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. + document.addEventListener("draco:theme", (e) => { + const mode = e && e.detail && e.detail.mode; + if (mode === "dark" || mode === "light") applyTheme(mode); + }); + // Si la barre s'est initialisée AVANT ce module (ordre des + // scripts defer/module selon la page), sa préférence déjà + // résolue depuis son cache est reprise ici. + const barre = window.DRACO_BAR; + if (barre && (barre.themeMode === "dark" || barre.themeMode === "light")) { + applyTheme(barre.themeMode); + } } function applyTheme(theme) { diff --git a/frontend/draco-barre.js b/frontend/draco-barre.js index 7078871..7b66b81 100644 --- a/frontend/draco-barre.js +++ b/frontend/draco-barre.js @@ -36,6 +36,12 @@ * 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 /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 @@ -268,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 /api/profil/theme → {"theme": "sombre"|"clair"|"systeme"|null} + * authentifié par le cookie draco (credentials: "include"). + * 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. */ @@ -695,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(); @@ -709,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); diff --git a/package.json b/package.json index 21fef1d..b99792c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsigate", - "version": "2.84.0", + "version": "2.84.1", "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", "directories": { diff --git a/tests/test_draco_barre.py b/tests/test_draco_barre.py index f55ccca..c7bd9f4 100644 --- a/tests/test_draco_barre.py +++ b/tests/test_draco_barre.py @@ -239,3 +239,23 @@ def test_barre_v041_correctif_icones_espace_de_noms_svg(): assert "createElementNS(SVG_NS, spec[0])" in js assert 'createElement("svg")' not in js assert "createElement(spec[0])" not in js + + +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. La vendange reste + # verbatim commons ; l'initialisation propre à ObsiGate vit dans + # app.js (écouteur draco:theme → applyTheme). + js = (FRONTEND / "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 "THEME_CACHE_TTL_MS" in js + assert 'CustomEvent("draco:theme"' in js + assert "themePref" in js and "themeMode" in js + # Aucun nom de Hub figé de plus que les défauts/dérivations + # documentés : l'URL du Hub reste la résolue de la barre. + assert "resolvedUrls.hub" in js + app = (FRONTEND / "app.js").read_text(encoding="utf-8") + assert 'addEventListener("draco:theme"' in app + assert "barre.themeMode" in app