Flowdeck 7.85.3 — HUB-21 : consommation des réglages généraux du Hub (bannière + drapeau recherche_partout)
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
FlowDeck CI / lint (push) Failing after 11s

This commit is contained in:
Spark
2026-11-06 16:57:05 +00:00
parent e6f5cff870
commit 463dd7b752
11 changed files with 512 additions and 4 deletions
+9 -1
View File
@@ -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")},
))
+150
View File
@@ -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"])
+28
View File
@@ -150,6 +150,34 @@
</head>
<body hx-headers='{"X-CSRF-Token":{{ csrf_token() | tojson }}}'{% if embed_mode %} class="embed-mode"{% else %} class="has-draco-bar"{% endif %}>
{% if not embed_mode %}{% include "_draco_bar.html" %}{% endif %}
{% if not embed_mode %}{% set _banniere_hub = banniere_hub() %}{% if _banniere_hub %}
<div class="banniere-hub" id="banniere-hub" role="status" hidden>
<span class="banniere-hub-texte">{{ _banniere_hub }}</span>
<button type="button" class="banniere-hub-fermer" id="banniere-hub-fermer" aria-label="Masquer la bannière" title="Masquer pour cette session du navigateur">×</button>
</div>
<script nonce="{{ csp_nonce() }}">
/* Bannière d'annonce des réglages généraux du Hub (HUB-21) :
masquage par l'utilisateur pour la session du navigateur —
sessionStorage, valeur = texte de la bannière (une nouvelle
annonce réapparaît donc toujours). La consultation du Hub se
fait côté serveur (app.services.hub_reglages) ; le texte est
échappé par l'autoescape Jinja. Sans JS, le bandeau reste
masqué (attribut hidden). */
(function () {
var bandeau = document.getElementById("banniere-hub");
if (!bandeau) return;
var texte = {{ _banniere_hub | tojson }};
try {
if (sessionStorage.getItem("flowdeck_banniere_masquee") === texte) return;
} catch (e) { /* stockage indisponible : on affiche */ }
bandeau.hidden = false;
document.getElementById("banniere-hub-fermer").addEventListener("click", function () {
bandeau.hidden = true;
try { sessionStorage.setItem("flowdeck_banniere_masquee", texte); } catch (err) { /* tant pis */ }
});
})();
</script>
{% endif %}{% endif %}
<div class="app-layout" x-data="appState()" x-cloak>
<!-- ═══════════ MOBILE SIDEBAR OVERLAY ═══════════ -->
+1
View File
@@ -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. #}
<main class="rp-page" id="recherche" data-host="flowdeck"
data-recherche-partout="{{ 'true' if recherche_partout else 'false' }}"
data-base-obsigate="{{ bases.obsigate }}"
data-base-flowdeck="{{ bases.flowdeck }}"
data-base-tiktak="{{ bases.tiktak }}"
+12
View File
@@ -91,6 +91,12 @@ ENV.globals["app_base_url"] = lambda: _app_base_url()
ENV.globals["draco_urls"] = lambda: _draco_urls()
ENV.globals["draco_agent_url"] = lambda: _draco_urls()["agent"]
ENV.globals["gitea_url"] = lambda: _draco_urls()["gitea"]
# Bannière d'annonce des réglages généraux du Hub (HUB-21) : la
# FONCTION est exposée (pas un instantané) — chaque rendu du
# gabarit principal lit le cache serveur de 60 s de
# app.services.hub_reglages. Hub muet → chaîne vide → aucune
# bannière, comme aujourd'hui.
ENV.globals["banniere_hub"] = lambda: _banniere_hub()
def _app_base_url() -> 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