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
151 lines
5.3 KiB
Python
151 lines
5.3 KiB
Python
"""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"])
|