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