diff --git a/CHANGELOG.md b/CHANGELOG.md index 32e2fe4..01fce7c 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.85.1**. +> [Unreleased](#unreleased). La dernière version livrée est **2.85.2**. --- @@ -14,6 +14,9 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). --- +## [2.85.2] — 2026-11-06 + +- **HUB-21 — Consommation des réglages généraux du Hub par ObsiGate (propagation du pilote TikTak 0.61.22, liste fermée D6(a), #229)** : nouveau client `backend/hub_reglages.py` — consultation côté serveur de `GET {hub}/api/reglages/effectifs` (endpoint public, aucun cookie, `Accept: application/json`, délai 2 s), forme validée strictement, cache mémoire 60 s sur le patron du pilote (horodatage posé avant l'appel : un échec conserve l'ancien cache, pas de rafale) ; URL du Hub résolue comme la barre commune (`draco_config.app_url("hub")` — jamais d'URL dev figée). Toute erreur (réseau, statut, JSON, forme) = **repli intégral** sur les défauts actuels. **Bannière** : fragment injecté côté serveur sous la barre commune (page principale `_index_html` et page `/recherche`), texte échappé (jamais de HTML injecté), bouton fermer avec mémorisation du texte fermé en `sessionStorage` (clé `obsigate_banniere_masquee`) ; bannière vide = aucun fragment, zéro changement visuel ; styles `.banniere-hub*` dans `frontend/style.css` (variables CSS du thème). **Drapeau `recherche_partout`** : `serve_recherche_page` remplace le jeton `{{RECHERCHE_PARTOUT}}` rendu en `data-recherche-partout` du `
` de `frontend/recherche.html` ; si faux, le JS vendangé `frontend/js/recherche.js` (même modification dans les 4 hôtes applicatifs) n'interroge plus les sœurs ni la console — la recherche locale ObsiGate continue ; vrai ou Hub muet = comportement actuel. **`domaine_defaut` non consommé** : ObsiGate n'a pas de notion de domaine propre — le sélecteur de domaine de la barre commune pilote le panneau Spark, pas les vues d'ObsiGate ; la valeur est lue et validée par le client, sans effet local. Le libellé « Fermer l'annonce » est un attribut d'accessibilité du fragment injecté côté serveur (patron des autres hôtes), pas un texte d'interface du SPA : pas de clé i18n ajoutée. Fiche : `docs/features/hub-reglages-obsigate-229.md`. Tests : `tests/test_hub_reglages.py` (client : contrat exact, forme inattendue, erreurs, cache 60 s, ancien cache sans rafale ; intégration : bannière présente et échappée sur `/` et `/recherche`, absente si vide ou Hub muet, attribut du drapeau selon la valeur) ; `tests/frontend/recherche.test.mjs` étendu (`federationAutorisee`). ## [2.85.1] — 2026-11-06 - **HUB-19 — Propagation des rôles du Hub à ObsiGate (suite du pilote ShaarIt, fusion Hub v2 §2.2, décision D3(a), #228)** : ObsiGate, à la fois émettrice et consommatrice du SSO, consomme à son tour le registre des rôles du Hub. Nouveau client `backend/hub_roles.py` : à l'ouverture de session draco (validation locale `draco_session.user_for_draco`), consultation de `GET {hub}/api/roles/obsigate` (en-tête Cookie complet relayé, `Accept: application/json`, délai 2 s), cache mémoire 60 s par `sub` sur le patron du pilote (horodatage posé avant l'appel : un échec conserve l'ancien cache, pas de rafale) ; URL du Hub résolue comme la barre commune (`draco_config.app_url("hub")` : `DRACO_HUB_URL` explicite, dérivation par l'hôte sœur, défaut officiel — jamais d'URL dev figée). **Effets, sessions établies par draco seulement** : niveau connu `aucun` → session draco refusée, `require_auth` répond 403 avec un message explicite en français (l'authentification native d'ObsiGate — l'émettrice, son accès propre est le secours — n'est jamais touchée) ; routes d'administration `/api/auth/admin/*` : nouvelle garde `require_admin_hub` — canal draco + rôle connu ⇒ niveau `administrateur` du registre exigé, sessions natives et rôle inconnu (Hub injoignable ou en erreur) ⇒ règles actuelles inchangées, aucun verrouillage nouveau, et le registre n'élève aucun privilège local. Validation locale du jeton, émission, révocations et renouvellement inchangés ; aucun rôle dans le jeton. Aucun impact frontend (pas d'i18n). Fiche : `docs/features/hub-roles-obsigate-228.md`. Tests : `tests/test_hub_roles.py` (client : parsing/cache/repli/résolution d'URL ; intégration : refus draco, garde admin). **Hypothèse de version** : branche depuis le main 2.84.1, cible 2.85.1 — rebasée sur la 2.85.0 (PR « API sessions HUB-22 » fusionnée d'abord) avant fusion. diff --git a/README.fr.md b/README.fr.md index b735f89..35159a7 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.85.1-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.85.2-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.85.1). +Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.85.2). --- -*Projet : ObsiGate | Version : 2.85.1 | Dernière mise à jour : Septembre 2026* +*Projet : ObsiGate | Version : 2.85.2 | Dernière mise à jour : Septembre 2026* diff --git a/README.md b/README.md index 4bbc524..5592577 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.85.1-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.85.2-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.85.1). +See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.85.2). --- -*Project: ObsiGate | Version: 2.85.1 | Last updated: September 2026* +*Project: ObsiGate | Version: 2.85.2 | Last updated: September 2026* diff --git a/VERSION b/VERSION index d7441bf..efbe57e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.85.1 +2.85.2 diff --git a/backend/hub_reglages.py b/backend/hub_reglages.py new file mode 100644 index 0000000..d67d713 --- /dev/null +++ b/backend/hub_reglages.py @@ -0,0 +1,154 @@ +# backend/hub_reglages.py +# Hub HUB-21 (propagation) — Réglages généraux du Hub consommés par +# ObsiGate, sur le patron posé par l'hôte pilote TikTak (0.61.22). +# +# Le Hub expose ``GET {hub}/api/reglages/effectifs`` — endpoint +# **public** (aucun cookie relayé), ``Accept: application/json``, +# délai 2 s. Réponse 200, forme figée (liste fermée D6(a)) : +# ``{"recherche_partout": bool, "banniere": str, +# "domaine_defaut": "pro"|"famille"|"perso"|null, +# "ttl_secondes": 60}``. +# +# Cache mémoire 60 s, horodatage posé **avant** l'appel : un échec +# conserve l'ancien cache et ne déclenche pas de rafale de requêtes +# vers un Hub en panne. +# +# **Repli intégral** : Hub injoignable, en erreur ou réponse de forme +# inattendue → les **défauts actuels** (``recherche_partout: true``, +# ``banniere: ""``, ``domaine_defaut: null``), c'est-à-dire le +# comportement d'ObsiGate avant HUB-21. Ce module ne lève jamais. +# +# Consommation chez ObsiGate : +# - ``banniere`` : injectée côté serveur sous la barre commune +# (page principale et page /recherche), texte échappé ; +# - ``recherche_partout`` : posé en attribut ``data-recherche-partout`` +# de la page /recherche — si faux, la partie fédérée est désactivée +# (la recherche locale ObsiGate continue) ; +# - ``domaine_defaut`` : **non consommé** — ObsiGate n'a pas de +# notion de domaine propre ; le sélecteur de domaine de la barre +# commune pilote le panneau Spark, pas les vues d'ObsiGate. La +# valeur est lue et validée, sans effet local. +# +# URL du Hub : `draco_config.app_url("hub")` — la même résolution que +# la barre commune (#223) : variable explicite d'abord, dérivation +# par l'hôte sœur ensuite, défaut officiel enfin. Jamais d'URL dev +# figée. + +from __future__ import annotations + +import json +import logging +import threading +import time +import urllib.request + +logger = logging.getLogger("obsigate.hub_reglages") + +#: Réglages par défaut = comportement actuel d'ObsiGate (repli). +DEFAUTS = {"recherche_partout": True, "banniere": "", "domaine_defaut": None} + +#: Domaines reconnus du contrat D6(a). +DOMAINES = ("pro", "famille", "perso") + +#: TTL du cache, aligné sur ``ttl_secondes`` annoncé par le Hub (60 s). +CACHE_TTL_SECONDS = 60 + +#: Délai maximal de la consultation du Hub (contrat : 2 s). +DELAI_SECONDS = 2 + +_lock = threading.Lock() +_cache: dict | None = None # derniers réglages validés +_cache_at: float | None = None # horodatage (monotone) du dernier appel + + +def _hub_url() -> str: + """Base publique du Hub, résolue comme pour la barre commune.""" + from backend.draco_config import app_url + + return app_url("hub") + + +def _valider(donnees) -> dict | None: + """Valide la forme figée du contrat ; ``None`` si inattendue.""" + if not isinstance(donnees, dict): + return None + recherche = donnees.get("recherche_partout") + banniere = donnees.get("banniere") + domaine = donnees.get("domaine_defaut") + if not isinstance(recherche, bool): + return None + if not isinstance(banniere, str): + return None + if domaine is not None and domaine not in DOMAINES: + return None + return { + "recherche_partout": recherche, + "banniere": banniere, + "domaine_defaut": domaine, + } + + +def _consulter_hub(url: str) -> dict | None: + """Appelle le Hub et retourne les réglages validés, ou ``None``. + + Toute erreur (réseau, délai dépassé, statut ≠ 200, JSON + illisible, forme hors contrat) retourne ``None`` : repli. + Endpoint public : aucun cookie n'est relayé. + """ + requete = urllib.request.Request( + url, method="GET", headers={"Accept": "application/json"}) + try: + with urllib.request.urlopen(requete, timeout=DELAI_SECONDS) as reponse: + if reponse.status != 200: + return None + donnees = json.loads(reponse.read().decode("utf-8")) + except Exception: # les réglages ne cassent jamais une page + return None + return _valider(donnees) + + +def reglages_effectifs() -> dict: + """Réglages généraux effectifs pour ObsiGate. + + Réponse du Hub validée et mise en cache 60 s ; à défaut (Hub + muet, en erreur ou forme inattendue), l'ancien cache s'il + existe, sinon les défauts actuels. Ne lève jamais. + """ + global _cache, _cache_at + with _lock: + if _cache is not None and _cache_at is not None \ + and time.monotonic() - _cache_at < CACHE_TTL_SECONDS: + return dict(_cache) + # Horodatage posé AVANT l'appel (patron du pilote) : l'ancien + # cache reste en vigueur, un échec ne déclenche pas de rafale. + _cache_at = time.monotonic() + url = f"{_hub_url().rstrip('/')}/api/reglages/effectifs" + reglages = _consulter_hub(url) + if reglages is None: + logger.warning( + "Réglages généraux du Hub indisponibles (%s) — " + "comportement actuel conservé", url) + with _lock: + return dict(_cache) if _cache is not None else dict(DEFAUTS) + with _lock: + _cache = reglages + _cache_at = time.monotonic() + return dict(reglages) + + +def banniere() -> str: + """Texte de la bannière d'annonce (``""`` = aucune bannière).""" + return reglages_effectifs()["banniere"] + + +def recherche_partout() -> bool: + """Drapeau de la recherche fédérée (défaut actuel : actif).""" + return bool(reglages_effectifs()["recherche_partout"]) + + +def reinitialiser_cache() -> None: + """Vide le cache (tests).""" + global _cache, _cache_at + with _lock: + _cache = None + _cache_at = None diff --git a/backend/main.py b/backend/main.py index 854b877..0091bd5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -859,6 +859,49 @@ def _html_with_nonce(request: Request, name: str) -> str: ) +def _fragment_banniere_hub() -> str: + """Bandeau d'annonce des réglages généraux du Hub (HUB-21). + + Retourne le fragment HTML à insérer sous la barre commune, ou + ``""`` quand le Hub n'annonce rien (bannière vide ou Hub muet — + repli intégral du client :mod:`backend.hub_reglages`). Le texte + est **échappé** (jamais de HTML injecté). Le petit script inline + (le nonce CSP est ajouté par l'appelant via ``inject_csp_nonce``) + révèle le bandeau sauf si le même texte a déjà été fermé dans la + session du navigateur (``sessionStorage``, clé + ``obsigate_banniere_masquee``) : une nouvelle annonce réapparaît. + """ + import html as _html + + from backend import hub_reglages + + texte = hub_reglages.banniere() + if not texte: + return "" + return ( + '" + "" + ) + + def _index_html(request: Request) -> str: """``index.html`` + injection des URL publiques de l'écosystème (#223). @@ -886,6 +929,15 @@ def _index_html(request: Request) -> str: html = html.replace("", script + "", 1) else: # repli défensif : tout en tête de document html = script + html + # Bannière d'annonce du Hub (HUB-21) : sous la barre commune, + # juste avant son rappel ; vide = aucun changement visuel. + fragment = _fragment_banniere_hub() + if fragment: + marqueur = '
" in html: + html = html.replace("", "" + fragment, 1) return inject_csp_nonce(html, request.state.csp_nonce) if FRONTEND_DIR.exists(): @@ -989,6 +1041,15 @@ if FRONTEND_DIR.exists(): if not recherche_file.exists(): raise HTTPException(status_code=404, detail="Recherche page not found") html = recherche_file.read_text(encoding="utf-8") + # Drapeau « Rechercher partout » des réglages généraux du Hub + # (HUB-21) : faux = la partie fédérée de la page est désactivée + # par le JS vendangé, la recherche locale continue ; Hub muet + # = défaut actuel (actif). + from backend import hub_reglages + + html = html.replace( + "{{RECHERCHE_PARTOUT}}", + "true" if hub_reglages.recherche_partout() else "false") urls = urls_publiques() for jeton, cle in ( ("{{URL_OBSIGATE}}", "obsigate"), @@ -1008,6 +1069,11 @@ if FRONTEND_DIR.exists(): html = html.replace("", script + "", 1) else: html = script + html + # Bannière d'annonce du Hub (HUB-21) : sous la barre commune ; + # vide = aucun changement visuel. + fragment = _fragment_banniere_hub() + if fragment and "" in html: + html = html.replace("", "" + fragment, 1) html = inject_csp_nonce(html, request.state.csp_nonce) soeurs = " ".join(sorted({ origin_of(urls.get(cle, "")) or "" diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 666d12f..eb90ee8 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.85.1" +version = "2.85.2" dependencies = [ "chrono", "env_logger", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 329b2c1..ef414df 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.85.1" +version = "2.85.2" 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 12ae52f..c988619 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.85.1", + "version": "2.85.2", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3ab53a0..8024f3d 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # ObsiGate — Roadmap -> **Version :** 2.85.1 | **Dernière mise à jour :** 2026-11-06 +> **Version :** 2.85.2 | **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)** @@ -70,6 +70,28 @@ - [x] Garde `require_admin_hub` sur les routes `/api/auth/admin/*` - [x] Tests `tests/test_hub_roles.py` (client + intégration du chemin draco) +### 229. Hub HUB-21 — consommation des réglages généraux du Hub par ObsiGate + +- **Effort :** ~0,5 jour | **Impact :** 🟡 | **Ouvert :** 2026-11-06 | **Statut :** ✅ livré en 2.85.2 (voir CHANGELOG ; PR en attente de fusion ; redéploiement Docker requis) +- **Fiche :** [features/hub-reglages-obsigate-229.md](./features/hub-reglages-obsigate-229.md) +- **Description :** propagation de HUB-21 (pilote TikTak 0.61.22, liste + fermée D6(a)) : consultation côté serveur de + `GET {hub}/api/reglages/effectifs` (endpoint public, délai 2 s), + cache mémoire 60 s (horodatage posé avant l'appel), repli intégral + sur les défauts actuels si le Hub est muet, en erreur ou de forme + inattendue. Bannière d'annonce injectée sous la barre commune (page + principale et /recherche, texte échappé, fermable, masquage + mémorisé en `sessionStorage`) ; drapeau `recherche_partout` rendu + en `data-recherche-partout` de la page /recherche — faux = partie + fédérée désactivée, recherche locale intacte. `domaine_defaut` + **non consommé** : pas de notion de domaine propre à ObsiGate (le + sélecteur de la barre pilote le panneau Spark). +- **Sous-tâches :** + - [x] Client `backend/hub_reglages.py` (forme D6(a), cache/repli, URL du Hub résolue comme la barre — `draco_config.app_url("hub")`) + - [x] Bannière injectée (`backend/main.py::_fragment_banniere_hub`, `_index_html`, `serve_recherche_page`) + styles `frontend/style.css` + - [x] Jeton `{{RECHERCHE_PARTOUT}}` dans `frontend/recherche.html` + branchement du JS vendangé `frontend/js/recherche.js` + - [x] Tests `tests/test_hub_reglages.py` (client + intégration) et `tests/frontend/recherche.test.mjs` étendu + ### 226. Hub HUB-13 — API d'administration des comptes (machine-à-machine) - **Effort :** ~0,5 jour | **Impact :** 🟡 | **Ouvert :** 2026-11-05 | **Statut :** ✅ livré en 2.84.0 (PR en attente de fusion — voir CHANGELOG ; redéploiement Docker requis) diff --git a/docs/features/hub-reglages-obsigate-229.md b/docs/features/hub-reglages-obsigate-229.md new file mode 100644 index 0000000..b2c9353 --- /dev/null +++ b/docs/features/hub-reglages-obsigate-229.md @@ -0,0 +1,82 @@ +# #229 — HUB-21 : consommation des réglages généraux du Hub par ObsiGate + +**Carte :** propagation de HUB-21 (fusion #135) — le pilote TikTak +(0.61.22, en production) a posé le patron de client ; ObsiGate le +consomme à son tour, comme les autres hôtes de la suite. +**Version cible :** 2.85.2 (branche depuis le main 2.85.1). + +## Contrat consommé (liste fermée D6(a)) + +- `GET {hub}/api/reglages/effectifs` — endpoint **public** (aucun + cookie relayé), `Accept: application/json`, délai **2 s** ; +- réponse 200, forme figée : `{"recherche_partout": bool, + "banniere": str, "domaine_defaut": "pro"|"famille"|"perso"|null, + "ttl_secondes": 60}`, `Cache-Control: public, max-age=60` ; +- **URL du Hub** : `draco_config.app_url("hub")`, la résolution de la + barre commune (#223) — variable explicite d'abord, dérivation par + l'hôte sœur ensuite, défaut officiel enfin. Jamais d'URL dev figée. + +## Client — `backend/hub_reglages.py` + +Sur le patron du pilote TikTak (`app/hub_reglages.py`, httpx → urllib +comme `backend/hub_roles.py`) : + +- cache mémoire **60 s** ; horodatage posé **avant** l'appel : un + échec conserve l'ancien cache et ne déclenche pas de rafale vers un + Hub en panne ; +- forme validée strictement ; toute erreur (réseau, délai, statut + ≠ 200, JSON illisible, forme hors contrat) → **repli intégral** sur + les défauts actuels (`recherche_partout: true`, `banniere: ""`, + `domaine_defaut: null`) — le comportement d'ObsiGate avant HUB-21. + Le module ne lève jamais ; aucun appel au démarrage. + +## Consommation + +1. **`banniere`** — `backend/main.py::_fragment_banniere_hub()` + construit le fragment (texte **échappé** via `html.escape`, jamais + de HTML injecté), injecté sous la barre commune par `_index_html` + (page principale, juste avant le rappel de la barre) et par + `serve_recherche_page` (page /recherche, après l'en-tête de la + barre). Le script inline (nonce CSP ajouté par `inject_csp_nonce`) + révèle le bandeau sauf si le même texte a été fermé dans la + session du navigateur (`sessionStorage`, clé + `obsigate_banniere_masquee`) : une nouvelle annonce réapparaît. + Bannière vide = aucun fragment rendu, zéro changement visuel. + Styles `.banniere-hub*` dans `frontend/style.css`, sur les + variables du thème (`var(--accent)`). +2. **`recherche_partout`** — `serve_recherche_page` remplace le jeton + `{{RECHERCHE_PARTOUT}}` de `frontend/recherche.html`, rendu en + attribut `data-recherche-partout` du `
`. Le + JS vendangé `frontend/js/recherche.js` (modification identique + dans les 4 hôtes applicatifs : fonction pure + `federationAutorisee`) n'interroge plus les applications sœurs ni + la console quand la valeur est `false` — leurs sections affichent + « Recherche fédérée désactivée… » — tandis que la recherche locale + ObsiGate (même origine) continue. Attribut absent ou illisible = + fédération active (repli). +3. **`domaine_defaut` — non consommé.** ObsiGate n'a pas de notion + de domaine (Pro/Famille/Perso) propre à ses vues : le sélecteur de + domaine de la barre commune pilote le panneau Spark, pas + ObsiGate. La valeur est lue et validée par le client, sans effet + local. Seul l'hôte console (agent) applique ce réglage. + +## i18n + +Aucun texte d'interface du SPA n'est ajouté : la bannière est un +contenu d'annonce servi par le Hub (texte libre de l'administrateur), +et le libellé « Fermer l'annonce » est l'attribut d'accessibilité du +fragment injecté côté serveur, sur le même patron que les autres +hôtes. Pas de clé `frontend/locales/*` ajoutée. + +## Tests + +- `tests/test_hub_reglages.py` (nouveau) : contrat exact de l'appel + (URL, aucun cookie, délai 2 s), formes inattendues → repli, erreur + HTTP / Hub injoignable / JSON illisible → repli, cache 60 s, + ancien cache conservé sans rafale ; intégration TestClient : + bannière présente et échappée sur `/` et `/recherche`, absente si + vide ou Hub muet, `data-recherche-partout` selon la valeur du + drapeau (faux / vrai / Hub muet). +- `tests/frontend/recherche.test.mjs` : assertions ajoutées pour + `federationAutorisee` (seules les valeurs explicitement négatives + désactivent ; absent/illisible = actif). diff --git a/frontend/js/recherche.js b/frontend/js/recherche.js index 301f490..58ab135 100644 --- a/frontend/js/recherche.js +++ b/frontend/js/recherche.js @@ -246,6 +246,21 @@ return [tab]; } + /* HUB-21 (propagation des réglages généraux du Hub) : le drapeau + * « Rechercher partout », rendu par l'hôte dans l'attribut + * data-recherche-partout de #recherche (« true »/« false », lu + * côté serveur depuis GET {hub}/api/reglages/effectifs, cache + * 60 s, patron du pilote TikTak 0.61.22). Seules les valeurs + * explicitement négatives désactivent la fédération ; attribut + * absent ou illisible = fédération active — le comportement + * actuel, qui est aussi le repli quand le Hub est muet. + * Fonction pure, exposée aux tests. */ + function federationAutorisee(valeur) { + if (valeur == null) return true; + var v = String(valeur).trim().toLowerCase(); + return v !== "false" && v !== "0" && v !== "non"; + } + /* Troncature d'affichage : liste complète dans l'onglet propre à * l'application, PER_APP_LIMIT partout ailleurs (vue « Tout »). */ function pageItems(items, full) { @@ -514,6 +529,7 @@ visibleKeys: visibleKeys, pageItems: pageItems, buildSearchUrl: buildSearchUrl, + federationAutorisee: federationAutorisee, tabCounts: tabCounts, searchPath: searchPath, normalize: normalize, @@ -609,6 +625,14 @@ ".

"; return; } + if (state.status === "desactivee") { + count.textContent = ""; + body.innerHTML = + '

Recherche fédérée désactivée dans les ' + + "réglages généraux de la suite (Hub) : cette application " + + "n'est pas interrogée.

"; + return; + } if (state.status === "idle") { count.textContent = ""; body.innerHTML = @@ -651,6 +675,8 @@ var input = document.getElementById("rp-q"); if (!input) return; var hostApp = root.getAttribute("data-host") || ""; + var federation = federationAutorisee( + root.getAttribute("data-recherche-partout")); var explicit = {}; APPS.forEach(function (app) { var v = root.getAttribute("data-base-" + app.key); @@ -772,7 +798,23 @@ applyTab(tab, opts); /* La console n'est interrogée qu'une fois affichée. */ if (parseTab(tab) === "console" && currentQ && consoleFetchedQ !== currentQ) { - fetchConsole(currentQ, runId); + if (!federation) { + var secConsOnglet = sections.console; + if (secConsOnglet) { + states.console = { + status: "desactivee", + items: [], + message: "", + q: currentQ, + base: consoleBase, + app: "console", + }; + renderSection(secConsOnglet, states.console, true); + renderTabs(); + } + } else { + fetchConsole(currentQ, runId); + } } } @@ -865,6 +907,22 @@ APPS.forEach(function (app) { var sec = sections[app.key]; if (!sec) return; + if (!federation && app.key !== hostApp) { + /* HUB-21 : fédération désactivée par le Hub — la section + * sœur n'est pas interrogée, la recherche locale + * (app.key === hostApp, même origine) continue. */ + states[app.key] = { + status: "desactivee", + items: [], + message: "", + q: q, + base: bases[app.key], + app: app.key, + }; + renderSection(sec, states[app.key], activeTab === app.key); + renderTabs(); + return; + } states[app.key] = { status: "loading", items: [], @@ -915,7 +973,21 @@ }); /* Console : seulement si son onglet est affiché ; sinon son * compteur reste vide (pas de faux 0) jusqu'à son activation. */ - if (activeTab === "console") { + if (activeTab === "console" && !federation) { + var secConsRecherche = sections.console; + if (secConsRecherche) { + states.console = { + status: "desactivee", + items: [], + message: "", + q: q, + base: consoleBase, + app: "console", + }; + renderSection(secConsRecherche, states.console, true); + renderTabs(); + } + } else if (activeTab === "console") { fetchConsole(q, myRun); } else { states.console = { diff --git a/frontend/recherche.html b/frontend/recherche.html index b6f572e..3341fd9 100644 --- a/frontend/recherche.html +++ b/frontend/recherche.html @@ -129,6 +129,7 @@
gras & cie", + "domaine_defaut": None} + r = client.get("/") + assert r.status_code == 200 + assert 'id="banniere-hub"' in r.text + assert "Maintenance dimanche <b>gras</b> & cie" in r.text + assert "obsigate_banniere_masquee" in r.text + + +def test_banniere_absente_si_vide_ou_hub_muet(client, hub_banniere): + hub_banniere["reglages"] = { + "recherche_partout": True, "banniere": "", "domaine_defaut": "pro"} + r = client.get("/") + assert r.status_code == 200 + assert "banniere-hub" not in r.text + hub_banniere["reglages"] = None + hub_reglages.reinitialiser_cache() + r = client.get("/") + assert r.status_code == 200 + assert "banniere-hub" not in r.text + + +def test_recherche_banniere_et_drapeau(client, hub_banniere): + hub_banniere["reglages"] = { + "recherche_partout": False, "banniere": "Annonce recherche", + "domaine_defaut": None} + r = client.get("/recherche") + assert r.status_code == 200 + assert 'data-recherche-partout="false"' in r.text + assert 'id="banniere-hub"' in r.text + assert "Annonce recherche" in r.text + hub_banniere["reglages"] = { + "recherche_partout": True, "banniere": "", "domaine_defaut": None} + hub_reglages.reinitialiser_cache() + r = client.get("/recherche") + assert 'data-recherche-partout="true"' in r.text + assert "banniere-hub" not in r.text + hub_banniere["reglages"] = None # Hub muet → défaut actif + hub_reglages.reinitialiser_cache() + r = client.get("/recherche") + assert 'data-recherche-partout="true"' in r.text