feat: HUB-21 — réglages généraux du Hub consommés par ObsiGate (bannière + drapeau recherche_partout) #229
CI / lint (pull_request) Successful in 1m16s
CI / security (pull_request) Successful in 19s
CI / test (pull_request) Successful in 3m41s
CI / build (pull_request) Successful in 13s
CI / e2e (pull_request) Failing after 14m58s

This commit is contained in:
Spark
2026-11-06 17:02:04 +00:00
parent 15d015e59b
commit b88191ffcb
17 changed files with 647 additions and 15 deletions
+66
View File
@@ -859,6 +859,49 @@ def _html_with_nonce(request: Request, name: str) -> str:
)
def _fragment_banniere_hub() -> str:
"""Bandeau d'annonce des réglages généraux du Hub (HUB-21).
Retourne le fragment HTML à insérer sous la barre commune, ou
``""`` quand le Hub n'annonce rien (bannière vide ou Hub muet —
repli intégral du client :mod:`backend.hub_reglages`). Le texte
est **échappé** (jamais de HTML injecté). Le petit script inline
(le nonce CSP est ajouté par l'appelant via ``inject_csp_nonce``)
révèle le bandeau sauf si le même texte a déjà été fermé dans la
session du navigateur (``sessionStorage``, clé
``obsigate_banniere_masquee``) : une nouvelle annonce réapparaît.
"""
import html as _html
from backend import hub_reglages
texte = hub_reglages.banniere()
if not texte:
return ""
return (
'<div class="banniere-hub" id="banniere-hub" role="status" hidden>'
f'<span class="banniere-hub-texte">{_html.escape(texte)}</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("obsigate_banniere_masquee")===t)'
"{b.remove();return;}"
"b.hidden=false;"
'document.getElementById("banniere-hub-fermer")'
'.addEventListener("click",function(){'
'try{sessionStorage.setItem("obsigate_banniere_masquee",t);}'
"catch(e){}"
"b.remove();});"
"}catch(e){"
'var b2=document.getElementById("banniere-hub");'
"if(b2)b2.hidden=false;"
"}})();</script>"
)
def _index_html(request: Request) -> str:
"""``index.html`` + injection des URL publiques de l'écosystème (#223).
@@ -886,6 +929,15 @@ def _index_html(request: Request) -> str:
html = html.replace("</head>", script + "</head>", 1)
else: # repli défensif : tout en tête de document
html = script + html
# Bannière d'annonce du Hub (HUB-21) : sous la barre commune,
# juste avant son rappel ; vide = aucun changement visuel.
fragment = _fragment_banniere_hub()
if fragment:
marqueur = '<div class="draco-bar-rappel"'
if marqueur in html:
html = html.replace(marqueur, fragment + marqueur, 1)
elif "</header>" in html:
html = html.replace("</header>", "</header>" + fragment, 1)
return inject_csp_nonce(html, request.state.csp_nonce)
if FRONTEND_DIR.exists():
@@ -989,6 +1041,15 @@ if FRONTEND_DIR.exists():
if not recherche_file.exists():
raise HTTPException(status_code=404, detail="Recherche page not found")
html = recherche_file.read_text(encoding="utf-8")
# Drapeau « Rechercher partout » des réglages généraux du Hub
# (HUB-21) : faux = la partie fédérée de la page est désactivée
# par le JS vendangé, la recherche locale continue ; Hub muet
# = défaut actuel (actif).
from backend import hub_reglages
html = html.replace(
"{{RECHERCHE_PARTOUT}}",
"true" if hub_reglages.recherche_partout() else "false")
urls = urls_publiques()
for jeton, cle in (
("{{URL_OBSIGATE}}", "obsigate"),
@@ -1008,6 +1069,11 @@ if FRONTEND_DIR.exists():
html = html.replace("</head>", script + "</head>", 1)
else:
html = script + html
# Bannière d'annonce du Hub (HUB-21) : sous la barre commune ;
# vide = aucun changement visuel.
fragment = _fragment_banniere_hub()
if fragment and "</header>" in html:
html = html.replace("</header>", "</header>" + fragment, 1)
html = inject_csp_nonce(html, request.state.csp_nonce)
soeurs = " ".join(sorted({
origin_of(urls.get(cle, "")) or ""