Compare commits

..

No commits in common. "7c14bdfe5efc615ae3aeedfcc76d2b91155d419d" and "1de8b298659a1bea0804561cc845b4637d9708d0" have entirely different histories.

3 changed files with 0 additions and 100 deletions

View File

@ -1,14 +1,3 @@
secubox-toolbox (2.6.33-1~bookworm1) bookworm; urgency=medium
* Shared broadcast pin in every banner (#578). An operator-set "📌 pin"
(or the top-1 tracker of the day) is stored in /run/secubox/pin.json and
shown as the first chip in EVERY R2/R3 client's banner (24 h window) —
a shared signal around all banner injections. api: GET/POST /admin/pin
+ GET /admin/pin/ui setter (with an "use top-1 tracker" auto-fill from
the social aggregate). inject_banner reads + renders it.
-- Gerald KERMA <devel@cybermind.fr> Sun, 14 Jun 2026 15:15:00 +0200
secubox-toolbox (2.6.32-1~bookworm1) bookworm; urgency=medium
* Shared media proxy-cache (#577) — DEFAULT OFF (opt-in filter `media_cache`).

View File

@ -452,9 +452,6 @@ def _banner_html_dynamic(sha1: str, ctx: dict, csp_strict: bool,
# renders correctly regardless of page charset (some legacy pages declare
# iso-8859-1 which would mangle our raw UTF-8 emoji bytes).
right_parts = [f"{_ncr(ctx['status_icon'])} {ctx['status']}"]
# #578 — shared broadcast pin first, so every banner shows it.
if ctx.get("pin"):
right_parts.insert(0, "&#x1F4CC; " + _ncr(ctx["pin"])) # 📌
if ctx["flag"]:
# Phase 6.M (#496) : flags are Unicode "regional indicator" pairs
# (🇫🇷 = U+1F1EB + U+1F1F7). NCR-encoded pairs do NOT join into a
@ -710,18 +707,6 @@ class InjectBanner:
except Exception:
ctx["ghost_blocked"] = 0
ctx["ghost_kb"] = 0
# #578 — shared broadcast pin (operator/top-1), shown in every
# client's banner. Fresh window 24 h.
ctx["pin"] = ""
try:
import json as _json
import time as _time
with open("/run/secubox/pin.json", "r", encoding="utf-8") as _pf:
_p = _json.load(_pf)
if _p.get("text") and (_time.time() - _p.get("ts", 0)) < 86400:
ctx["pin"] = str(_p["text"])[:80]
except Exception:
pass
csp_strict = _detect_csp_strict(flow)
report_url = _report_url_for(flow)
level_label = _level_label(flow)

View File

@ -2481,80 +2481,6 @@ async def admin_cache() -> dict:
return out
_PIN_PATH = "/run/secubox/pin.json"
@router.get("/admin/pin")
async def admin_pin() -> dict:
"""#578 — the shared broadcast pin shown in every client's banner."""
import json as _json
from pathlib import Path as _P
cur = {"text": "", "url": "", "ts": 0, "by": ""}
try:
p = _P(_PIN_PATH)
if p.exists():
cur.update(_json.loads(p.read_text()))
except Exception:
pass
return cur
@router.post("/admin/pin")
async def admin_pin_set(request: Request) -> dict:
"""#578 — set/clear the shared pin (broadcast to all banners). Empty
text clears it."""
import json as _json
import time as _time
from pathlib import Path as _P
try:
body = await request.json()
except Exception:
body = {}
text = (str(body.get("text", "")) if isinstance(body, dict) else "")[:80].strip()
url = (str(body.get("url", "")) if isinstance(body, dict) else "")[:300].strip()
rec = {"text": text, "url": url, "ts": int(_time.time()) if text else 0, "by": "admin"}
try:
_P(_PIN_PATH).parent.mkdir(parents=True, exist_ok=True)
_P(_PIN_PATH).write_text(_json.dumps(rec))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return rec
@router.get("/admin/pin/ui", response_class=HTMLResponse)
async def admin_pin_ui() -> HTMLResponse:
"""#578 — minimal pin setter; can auto-fill the current top-1 tracker."""
html = """<!doctype html><html lang=fr><meta charset=utf-8>
<meta name=viewport content="width=device-width,initial-scale=1">
<title>Pin partagé ToolBoX</title>
<style>
body{background:#0a0a0f;color:#e8e6d9;font:14px system-ui,sans-serif;max-width:520px;margin:30px auto;padding:0 18px}
h1{color:#c9a84c;font-size:18px} label{display:block;color:#6b6b7a;font-size:12px;margin:12px 0 4px}
input{width:100%;padding:8px;border-radius:6px;border:1px solid #333;background:#14141c;color:#e8e6d9}
button{margin:14px 8px 0 0;padding:9px 14px;border-radius:6px;border:1px solid #c9a84c;background:#c9a84c;color:#0a0a0f;font-weight:700;cursor:pointer}
button.alt{background:transparent;color:#00d4ff;border-color:#00d4ff}
button.danger{background:transparent;color:#e63946;border-color:#e63946}
#msg{color:#00ff41;min-height:18px;margin-top:10px} .muted{color:#6b6b7a;font-size:12px}
</style>
<h1>📌 Pin partagé (toutes les bannières)</h1>
<p class=muted>Un message épinglé, diffusé dans la bannière de TOUS les clients R2/R3 (24 h).</p>
<label>Texte du pin <input id=text maxlength=80 placeholder="ex: traceur #1 du jour — doubleclick.net"></label>
<label>Lien (optionnel) <input id=url maxlength=300 placeholder="https://…"></label>
<button id=save>📌 Épingler</button>
<button class=alt id=top> Utiliser le traceur #1</button>
<button class=danger id=clear>Retirer</button>
<p id=msg></p>
<script>
const $=s=>document.querySelector(s);
fetch('/admin/pin').then(r=>r.json()).then(p=>{$('#text').value=p.text||'';$('#url').value=p.url||'';});
function post(t,u){return fetch('/admin/pin',{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({text:t,url:u})}).then(r=>r.json()).then(()=>{$('#msg').textContent='✓ diffusé';setTimeout(()=>$('#msg').textContent='',1500);});}
$('#save').onclick=()=>post($('#text').value,$('#url').value);
$('#clear').onclick=()=>{$('#text').value='';$('#url').value='';post('','');};
$('#top').onclick=()=>fetch('/admin/social-aggregate?hours=24').then(r=>r.json()).then(d=>{const t=(d.by_tracker_domain||[])[0];if(t){$('#text').value='Traceur #1 : '+t.tracker_domain+' ('+t.hits+' hits)';}else{$('#msg').textContent='aucun traceur';}});
</script></html>"""
return HTMLResponse(content=html)
@router.get("/admin/media")
async def admin_media() -> dict:
"""#570 — DPI media/content-type statistics for the donut UI."""