"""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"])