diff --git a/CHANGELOG.md b/CHANGELOG.md
index db36622..32e0ee6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+## v7.85.3 (2026-11-06) — HUB-21 : consommation des réglages généraux du Hub
+
+- **Client des réglages généraux** (`app/services/hub_reglages.py`, nouveau) : propagation du pilote TikTak 0.61.22. Consultation côté serveur de `GET {hub}/api/reglages/effectifs` — endpoint public, aucun cookie, `Accept: application/json`, délai 2,0 s. URL du Hub résolue comme le reste de la suite (`draco_urls.hub_url()` : hôte de la requête, puis dérivation par l'hôte de l'agent — `xxx-dev` → `hub-dev` — jamais d'URL dev figée ni de variable explicite qui figerait la dérivation). Cache mémoire 60 s, horodatage posé **avant** l'appel : un échec conserve l'ancien cache, pas de rafale. Forme validée strictement (liste fermée D6(a)) ; toute erreur (réseau, statut, JSON, forme) = **repli intégral** sur les défauts actuels (`recherche_partout: true`, `banniere: ""`, `domaine_defaut: null`), jamais d'exception.
+- **Bannière d'annonce** : global Jinja `banniere_hub` (fonction, jamais un instantané) consommé par `app/templates/base.html` — bandeau sous la barre commune, texte rendu par l'autoescape Jinja (jamais de HTML injecté), bouton « fermer » qui mémorise le texte fermé en `sessionStorage` (clé `flowdeck_banniere_masquee`) : une nouvelle annonce réapparaît. Bannière vide = aucun fragment rendu, zéro changement visuel. Styles `.banniere-hub*` dans `static/css/app.css` (fond `var(--accent)`). Hors mode embarqué (`embed_mode`), jamais sur la page publique.
+- **Drapeau `recherche_partout`** : la route `/recherche` pose `data-recherche-partout` sur la coquille ; le JS vendangé `static/js/recherche.js` (même modification dans les 4 hôtes applicatifs) n'interroge plus les sœurs ni la console quand la valeur est `false` — leurs sections affichent « Recherche fédérée désactivée… », la recherche locale Flowdeck continue ; attribut absent ou illisible = fédération active (repli).
+- **`domaine_defaut` non consommé** : Flowdeck n'a pas de notion de domaine (Pro/Famille/Perso) propre — le sélecteur de la barre commune pilote le panneau Spark, pas les vues Flowdeck. La valeur est lue et validée par le client, sans effet local ; seul l'hôte console (agent) l'applique.
+- Tests : `tests/test_hub_reglages.py` (nouveau, 13 tests — validation de la forme, contrat exact de l'appel, cache 60 s, ancien cache après échec, repli Hub muet, bannière présente et échappée dans la page /recherche, absente si vide ou Hub muet, attribut du drapeau rendu selon la valeur) ; `tests/recherche.test.js` étendu (fonction pure `federationAutorisee`, 92 assertions).
+
## v7.85.2 (2026-11-06) — HUB-19 : propagation des rôles du Hub (registre D3(a))
- **Client des rôles du Hub** (`app/services/hub_roles.py`, nouveau) : portage Python du pilote ShaarIt 0.7.0 (`hub_roles.rs`). À l'ouverture de session draco, consultation de `GET {hub}/api/roles/flowdeck` — en-tête Cookie complet de la requête relayé tel quel, `Accept: application/json`, délai 2 s. URL du Hub résolue comme la barre commune (`draco_urls.hub_url()` : explicite `HUB_URL` > hôte de la requête > dérivation par l'hôte de l'agent > défaut officiel), jamais d'URL dev figée. Cache mémoire par `sub`, TTL 60 s, horodatage posé **avant** l'appel : un échec conserve l'ancien cache, pas de rafale. Toute erreur (401/404/réseau/JSON illisible/rôle hors ensemble {`aucun`, `utilisateur`, `administrateur`}) = rôle **inconnu** → comportement actuel conservé, intégralement.
diff --git a/VERSION b/VERSION
index f1ff083..020a223 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-7.85.2
+7.85.3
diff --git a/app/routers/search.py b/app/routers/search.py
index 3a3f9e5..87455a1 100644
--- a/app/routers/search.py
+++ b/app/routers/search.py
@@ -56,8 +56,15 @@ def recherche_page(request: Request, q: str = Query(default="")):
static/js/recherche.js (API de recherche de chaque app, en
parallèle, états par application). La palette Ctrl+K native et
``GET /api/search`` restent inchangés par ailleurs.
+
+ HUB-21 : le drapeau ``recherche_partout`` des réglages
+ généraux du Hub est rendu dans l'attribut
+ ``data-recherche-partout`` de la coquille (cache serveur de
+ 60 s de ``app.services.hub_reglages`` ; Hub muet → « true »,
+ comportement actuel) — quand il est faux, recherche.js
+ n'interroge que la section locale Flowdeck.
"""
- from app.services import draco_urls
+ from app.services import draco_urls, hub_reglages
from app.templating import ENV
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
@@ -69,6 +76,7 @@ def recherche_page(request: Request, q: str = Query(default="")):
# l'onglet Console Spark (GET /api/v1/recherche, alignement #36).
return HTMLResponse(template.render(
request=request, q=q.strip(), user=user,
+ recherche_partout=hub_reglages.recherche_partout(),
bases={k: bases[k] for k in ("obsigate", "flowdeck", "tiktak",
"kanbante", "shaarit", "agent")},
))
diff --git a/app/services/hub_reglages.py b/app/services/hub_reglages.py
new file mode 100644
index 0000000..7112691
--- /dev/null
+++ b/app/services/hub_reglages.py
@@ -0,0 +1,150 @@
+"""Réglages généraux de la suite — consommation du Hub (HUB-21).
+
+Propagation du pilote TikTak (0.61.22, PR ticktick-clone #244) à
+Flowdeck, sur le contrat figé du Hub 0.16.0 : ``GET
+{hub}/api/reglages/effectifs`` → 200 JSON
+``{"recherche_partout": bool, "banniere": string ("" = aucune),
+"domaine_defaut": "pro"|"famille"|"perso"|null,
+"ttl_secondes": 60}``, ``Cache-Control: public, max-age=60`` —
+endpoint public, aucun cookie relayé.
+
+Petit client serveur sur le patron du pilote et du client des
+rôles (``app.services.hub_roles``) : cache mémoire 60 s,
+horodatage posé **avant** l'appel — un échec ne déclenche pas de
+rafale et l'ancien cache reste en vigueur. L'URL du Hub est
+résolue comme la barre commune (``app.services.draco_urls.hub_url()``
+: variable explicite ``HUB_URL`` / réglage, dérivation par l'hôte
+de la requête, puis par l'hôte sœur de l'agent, puis défaut
+officiel) — jamais d'URL dev figée. Consultation côté serveur,
+délai court de 2 s, jamais au démarrage (premier appel paresseux,
+au premier rendu de gabarit).
+
+**Repli obligatoire** : Hub muet, en erreur ou réponse de forme
+inattendue → valeurs par défaut actuelles (pas de bannière,
+recherche partout active, aucun domaine par défaut) —
+comportement d'aujourd'hui à l'identique. Ce module ne lève
+jamais.
+
+**Périmètre Flowdeck (cette PR)** :
+- ``banniere`` : bandeau d'annonce dans le gabarit principal
+ (``app/templates/base.html``, sous la barre commune), exposé
+ aux gabarits par ``app.templating`` (fonction, pas un
+ instantané : chaque rendu lit le cache de 60 s) ; texte échappé
+ par l'autoescape Jinja, masquage de session navigateur.
+- ``recherche_partout`` : la route ``GET /recherche`` rend
+ l'attribut ``data-recherche-partout`` de la coquille ; quand le
+ drapeau est faux, ``recherche.js`` (copie vendangée, même
+ modification dans chaque hôte) n'interroge plus les
+ applications sœurs ni la console — la recherche locale
+ Flowdeck (sa propre section, même origine) continue.
+- ``domaine_defaut`` : **non consommé** — Flowdeck n'a pas de
+ notion de domaine (Pro/Famille/Perso) propre : ses vues ne sont
+ filtrées par aucun domaine ; le sélecteur de la barre commune
+ pilote le panneau Spark, pas Flowdeck. Le réglage est lu et
+ validé par le client, mais pas appliqué ici.
+"""
+
+from __future__ import annotations
+
+import logging
+import threading
+import time
+
+logger = logging.getLogger(__name__)
+
+#: TTL du cache, aligné sur ``ttl_secondes`` annoncé par le Hub.
+CACHE_TTL = 60.0 # secondes
+
+#: Délai de consultation du Hub : court par contrat (2 s).
+DELAI = 2.0 # secondes
+
+#: Valeurs par défaut actuelles (repli quand le Hub ne répond pas).
+DEFAUTS: dict = {
+ "recherche_partout": True,
+ "banniere": "",
+ "domaine_defaut": None,
+}
+
+_DOMAINES = ("pro", "famille", "perso")
+
+_verrou = threading.Lock()
+_cache: dict | None = None
+_cache_at: float = 0.0
+
+
+def reinitialiser_cache() -> None:
+ """Vide le cache (tests uniquement)."""
+ global _cache, _cache_at
+ with _verrou:
+ _cache = None
+ _cache_at = 0.0
+
+
+def _valider(data) -> dict | None:
+ """Valide la charge utile du Hub ; dict normalisé ou None."""
+ if not isinstance(data, dict):
+ return None
+ recherche = data.get("recherche_partout")
+ banniere = data.get("banniere")
+ domaine = data.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() -> dict | None:
+ """Un appel au Hub ; retourne les réglages validés ou None."""
+ from app.services import draco_urls
+
+ url = f"{draco_urls.hub_url().rstrip('/')}/api/reglages/effectifs"
+ try:
+ import httpx
+
+ r = httpx.get(url, headers={"Accept": "application/json"},
+ timeout=DELAI)
+ if r.status_code != 200:
+ return None
+ return _valider(r.json())
+ except Exception as exc: # Hub injoignable : repli, jamais d'exception
+ logger.debug("Réglages du Hub indisponibles (%s): %s", url, exc)
+ return None
+
+
+def reglages_effectifs() -> dict:
+ """Réglages généraux en vigueur : réponse du Hub (cache 60 s),
+ sinon ancien cache, sinon les valeurs par défaut actuelles."""
+ global _cache, _cache_at
+ with _verrou:
+ if _cache is not None and time.monotonic() - _cache_at < CACHE_TTL:
+ return dict(_cache)
+ # Horodatage posé avant l'appel (patron révocations) :
+ # l'ancien cache reste en vigueur si l'appel échoue.
+ _cache_at = time.monotonic()
+ ancien = dict(_cache) if _cache is not None else None
+ resultat = _consulter_hub()
+ if resultat is not None:
+ with _verrou:
+ _cache = resultat
+ _cache_at = time.monotonic()
+ return dict(resultat)
+ if ancien is not None:
+ return ancien
+ return dict(DEFAUTS)
+
+
+def banniere() -> str:
+ """Texte de la bannière d'annonce en vigueur ("" = aucune)."""
+ return str(reglages_effectifs()["banniere"] or "")
+
+
+def recherche_partout() -> bool:
+ """Drapeau « Rechercher partout » en vigueur (True = actif)."""
+ return bool(reglages_effectifs()["recherche_partout"])
diff --git a/app/templates/base.html b/app/templates/base.html
index 360d1d2..204e98a 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -150,6 +150,34 @@
{% if not embed_mode %}{% include "_draco_bar.html" %}{% endif %}
+{% if not embed_mode %}{% set _banniere_hub = banniere_hub() %}{% if _banniere_hub %}
+
+ {{ _banniere_hub }}
+
+
+
+{% endif %}{% endif %}
diff --git a/app/templates/recherche.html b/app/templates/recherche.html
index 3f68efa..85cd8ab 100644
--- a/app/templates/recherche.html
+++ b/app/templates/recherche.html
@@ -12,6 +12,7 @@
son contexte `bases` (defaut 6). recherche.js garde ses replis
(derivation -dev, href d'onglet de barre) si l'attribut est vide. #}
str:
@@ -105,6 +111,12 @@ def _draco_urls() -> dict:
return draco_urls.toutes()
+def _banniere_hub() -> str:
+ from app.services import hub_reglages
+
+ return hub_reglages.banniere()
+
+
def _plugin_enabled(slug: str) -> bool:
from app.services.plugins import is_enabled
diff --git a/static/css/app.css b/static/css/app.css
index cd559fb..a492d84 100644
--- a/static/css/app.css
+++ b/static/css/app.css
@@ -5342,3 +5342,21 @@ body.has-draco-bar .app-layout { height: calc(100vh - 60px); }
background: transparent; cursor: pointer; font: inherit; font-size: 13.5px;
}
.draco-liens-erreur { color: #c0392b; font-size: 13px; margin: 8px 0 0; }
+
+/* Bannière d'annonce des réglages généraux du Hub (HUB-21) :
+ bandeau rendu par base.html sous la barre commune, patron du
+ pilote TikTak — accent Flowdeck, texte centré. */
+.banniere-hub {
+ display: flex; align-items: center; gap: 10px;
+ padding: 8px 14px;
+ background: var(--accent, #2f6fed); color: #fff;
+ font-size: 14px; line-height: 1.35;
+}
+.banniere-hub[hidden] { display: none; }
+.banniere-hub-texte { flex: 1 1 auto; text-align: center; overflow-wrap: anywhere; }
+.banniere-hub-fermer {
+ flex: 0 0 auto; border: 0; background: transparent; color: inherit;
+ font-size: 18px; line-height: 1; cursor: pointer; padding: 2px 6px;
+ border-radius: 6px;
+}
+.banniere-hub-fermer:hover { background: rgba(255, 255, 255, .18); }
diff --git a/static/js/recherche.js b/static/js/recherche.js
index 301f490..58ab135 100644
--- a/static/js/recherche.js
+++ b/static/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.