Kanbante 0.46.3 — HUB-21 : consommation des réglages généraux du Hub (bannière + drapeau recherche_partout)
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
"""Réglages généraux — consommation des réglages du Hub (HUB-21).
|
||||
|
||||
Propagation du 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, sur le patron du pilote : l'horodatage est
|
||||
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. L'URL
|
||||
du Hub est résolue comme celle de la barre commune
|
||||
(``app.config.resoudre_urls_draco`` : variable explicite >
|
||||
hôte public de la requête > dérivation par l'hôte sœur de
|
||||
l'agent > défaut officiel) — jamais d'URL dev figée.
|
||||
|
||||
**Repli intégral** : Hub injoignable, en erreur ou réponse de
|
||||
forme inattendue → les **défauts actuels** (``recherche_partout:
|
||||
True``, ``banniere: ""``, ``domaine_defaut: None``), c'est-à-dire
|
||||
le comportement de Kanbante avant HUB-21. Ce module ne lève
|
||||
jamais.
|
||||
|
||||
Consommation chez Kanbante :
|
||||
|
||||
- ``banniere`` : bandeau sous la barre commune (partiel
|
||||
``_header.html``, présent sur toutes les pages applicatives),
|
||||
texte échappé par l'autoescape Jinja ;
|
||||
- ``recherche_partout`` : attribut ``data-recherche-partout`` de
|
||||
la page /recherche — si faux, la partie fédérée est désactivée
|
||||
(la recherche locale Kanbante continue) ;
|
||||
- ``domaine_defaut`` : **non consommé** — Kanbante n'a pas de
|
||||
notion de domaine (Pro/Famille/Perso) propre ; le sélecteur de
|
||||
domaine de la barre commune pilote le panneau Spark, pas les
|
||||
vues de Kanbante. La valeur est lue et validée, sans effet
|
||||
local.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
|
||||
import httpx
|
||||
|
||||
from app.config import resoudre_urls_draco
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
#: Réglages par défaut = comportement actuel de Kanbante (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.
|
||||
CACHE_TTL = 60.0 # secondes
|
||||
|
||||
_verrou = threading.Lock()
|
||||
_cache: dict | None = None # derniers réglages validés
|
||||
_cache_fetched_at: float = 0.0 # horodatage (monotone) du dernier appel
|
||||
|
||||
|
||||
def _reset_cache() -> None:
|
||||
"""Vide le cache (tests, ou redémarrage de processus)."""
|
||||
global _cache, _cache_fetched_at
|
||||
with _verrou:
|
||||
_cache = None
|
||||
_cache_fetched_at = 0.0
|
||||
|
||||
|
||||
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:
|
||||
"""Un appel au Hub ; réglages validés ou ``None`` (repli)."""
|
||||
url = f"{hub_url.rstrip('/')}/api/reglages/effectifs"
|
||||
try:
|
||||
reponse = httpx.get(
|
||||
url, headers={"Accept": "application/json"}, timeout=2.0)
|
||||
except Exception as exc: # Hub injoignable : repli, jamais un blocage
|
||||
logger.debug(
|
||||
"Réglages généraux du Hub injoignables (%s) : %s", url, exc)
|
||||
return None
|
||||
if getattr(reponse, "status_code", None) != 200:
|
||||
logger.debug(
|
||||
"Réglages généraux du Hub en erreur (%s) : statut %s",
|
||||
url, getattr(reponse, "status_code", "?"))
|
||||
return None
|
||||
try:
|
||||
donnees = reponse.json()
|
||||
except Exception:
|
||||
return None
|
||||
return _valider(donnees)
|
||||
|
||||
|
||||
def reglages_effectifs(hote: str | None = None) -> dict:
|
||||
"""Réglages généraux effectifs pour Kanbante.
|
||||
|
||||
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. ``hote`` est l'hôte public
|
||||
de la requête, pour résoudre l'URL du Hub comme la barre
|
||||
commune. Ne lève jamais.
|
||||
"""
|
||||
global _cache, _cache_fetched_at
|
||||
now = time.monotonic()
|
||||
with _verrou:
|
||||
if _cache is not None and now - _cache_fetched_at < CACHE_TTL:
|
||||
return dict(_cache)
|
||||
# Horodatage posé AVANT l'appel (patron du pilote) :
|
||||
# l'ancien cache reste en vigueur pendant l'appel.
|
||||
_cache_fetched_at = now
|
||||
hub_url = resoudre_urls_draco(hote).hub_url
|
||||
reglages = _consulter(hub_url)
|
||||
if reglages is None:
|
||||
logger.debug(
|
||||
"Réglages généraux du Hub indisponibles (%s) — "
|
||||
"comportement actuel conservé", hub_url)
|
||||
with _verrou:
|
||||
return dict(_cache) if _cache is not None else dict(DEFAUTS)
|
||||
with _verrou:
|
||||
_cache = reglages
|
||||
_cache_fetched_at = time.monotonic()
|
||||
return dict(reglages)
|
||||
|
||||
|
||||
def banniere(hote: str | None = None) -> str:
|
||||
"""Texte de la bannière d'annonce (``""`` = aucune bannière)."""
|
||||
return reglages_effectifs(hote)["banniere"]
|
||||
|
||||
|
||||
def recherche_partout(hote: str | None = None) -> bool:
|
||||
"""Drapeau de la recherche fédérée (défaut actuel : actif)."""
|
||||
return bool(reglages_effectifs(hote)["recherche_partout"])
|
||||
+11
@@ -2401,6 +2401,17 @@ def render(
|
||||
},
|
||||
)
|
||||
context.setdefault("agent_url", _urls.agent_url)
|
||||
# Réglages généraux du Hub (HUB-21) : bannière d'annonce du
|
||||
# partiel _header.html et drapeau de la recherche fédérée de la
|
||||
# page /recherche. Client à cache 60 s et repli intégral
|
||||
# (app.hub_reglages) : Hub muet = bannière vide et fédération
|
||||
# active, c'est-à-dire le comportement actuel.
|
||||
from app import hub_reglages as _hub_reglages
|
||||
|
||||
_hote = request.headers.get("host")
|
||||
context.setdefault("banniere_hub", _hub_reglages.banniere(_hote))
|
||||
context.setdefault(
|
||||
"recherche_partout_hub", _hub_reglages.recherche_partout(_hote))
|
||||
context.setdefault(
|
||||
"sso_authorize_url",
|
||||
f"{_urls.sso_url}/sso/authorize?return_to="
|
||||
|
||||
@@ -11,6 +11,37 @@
|
||||
} catch (e) { /* mode privé : le thème ne survit qu'à la page */ }
|
||||
</script>
|
||||
{% include "_draco_bar.html" %}
|
||||
{# ── Bannière d'annonce des réglages généraux du Hub (HUB-21) ────
|
||||
Texte posé par render() (contexte `banniere_hub`, client serveur
|
||||
à cache 60 s et repli intégral) et échappé par l'autoescape
|
||||
Jinja — jamais de HTML injecté. Vide = aucun fragment rendu.
|
||||
Le script masque le bandeau si le même texte a déjà été fermé
|
||||
dans la session du navigateur (sessionStorage) : une nouvelle
|
||||
annonce réapparaît. #}
|
||||
{% 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="Fermer l'annonce">✕</button>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
var b = document.getElementById('banniere-hub');
|
||||
if (!b) return;
|
||||
var t = b.querySelector('.banniere-hub-texte').textContent;
|
||||
if (sessionStorage.getItem('kanbante_banniere_masquee') === t) { b.remove(); return; }
|
||||
b.hidden = false;
|
||||
document.getElementById('banniere-hub-fermer').addEventListener('click', function () {
|
||||
try { sessionStorage.setItem('kanbante_banniere_masquee', t); } catch (e) { /* stockage indisponible */ }
|
||||
b.remove();
|
||||
});
|
||||
} catch (e) {
|
||||
var b2 = document.getElementById('banniere-hub');
|
||||
if (b2) b2.hidden = false;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{% endif %}
|
||||
{# ── Cadre applicatif partagé (issue #16, refonte 0.16.0) ─────────
|
||||
Barre latérale unique pour TOUTES les pages applicatives :
|
||||
desktop = barre fixe repliable en rail d'icônes (état mémorisé),
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
{% include "_header.html" %}
|
||||
|
||||
<main class="rp-page" id="recherche" data-host="kanbante"
|
||||
data-recherche-partout="{{ 'true' if recherche_partout_hub else 'false' }}"
|
||||
data-base-obsigate="{{ draco_urls.obsigate }}"
|
||||
data-base-flowdeck="{{ draco_urls.flowdeck }}"
|
||||
data-base-tiktak="{{ draco_urls.tiktak }}"
|
||||
|
||||
Reference in New Issue
Block a user