diff --git a/booth/app.py b/booth/app.py index 5f88a82..b72f794 100644 --- a/booth/app.py +++ b/booth/app.py @@ -136,7 +136,21 @@ def set_booth_blurred(booth: Path, on: bool) -> bool: state the caller asked for, exactly as unflagging an unflagged item is.""" marker = booth / BOOTH_BLUR_FILE if on: - marker.touch(exist_ok=True) + # NEVER through a link (r2b, heid bug-hunt). `touch()` followed a + # planted `.blurbooth` symlink: a click rewrote an outside file's + # mtime, or created a dangling target — the class `record_view` was + # hardened against. Anything already at the name, a link included, + # already reads as fogged (`is_booth_blurred`), so there is nothing to + # write; otherwise create exclusively, never following a link. + try: + os.lstat(marker) + return True + except FileNotFoundError: + pass + try: + os.close(os.open(marker, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o644)) + except FileExistsError: + pass # lost a race to another fog: still fogged return True try: marker.unlink() @@ -2143,7 +2157,10 @@ def create_app( booth = resolve_booth(name) set_booth_blurred(booth, on not in ("0", "false", "")) landing = f"/b/{quote(name, safe='')}/" - if back: + # The review only for an item of the review ring (r2b, heid bug-hunt), + # exactly as the mark routes' back=view: a stale or foreign `back` + # would otherwise land on a 404. Built from the ring, never echoed. + if back and back in review_chain(booth_items(booth)): landing += f"view?f={quote(back, safe='/')}" return RedirectResponse(url=landing, status_code=303) diff --git a/booth/templates/base.html b/booth/templates/base.html index cbc6e78..a1ccf47 100644 --- a/booth/templates/base.html +++ b/booth/templates/base.html @@ -616,6 +616,11 @@ .blur-all button:hover,.reveal-all-btn:hover{color:var(--text-body);border-color:var(--border-strong)} .blur-all.is-on button{color:var(--text-body);border-color:var(--border-strong)} .reveal-all-btn[aria-pressed="true"]{color:var(--text-body);border-color:var(--border-strong)} + /* One line each, always: a top bar at phone width squeezed these into + four-line stacks. Below 600px the reveal drops its "— blur is cosmetic" + tail; the title still says it, as does every per-item reveal. */ + .blur-all button,.reveal-all-btn{white-space:nowrap} + @media (max-width:600px){.reveal-all-btn .ra-note{display:none}} .badge-blur{color:var(--text-muted)} /* ---- inline docs ------------------------------------------------------ @@ -1082,7 +1087,8 @@ var key = 'booth.reveal:' + b; function show(btn) { var on = d.classList.contains('reveal-all'); - btn.textContent = on ? '🙈 blur again' : '👁 reveal all — blur is cosmetic'; + btn.querySelector('.ra-label').textContent = on ? '🙈 blur again' : '👁 reveal all'; + btn.querySelector('.ra-note').hidden = on; btn.setAttribute('aria-pressed', on ? 'true' : 'false'); } ctl.forEach(function (btn) { diff --git a/booth/templates/booth.html b/booth/templates/booth.html index 2591a88..d6cd1ff 100644 --- a/booth/templates/booth.html +++ b/booth/templates/booth.html @@ -106,13 +106,19 @@ {# r2b D2b + D2: the booth-wide blur controls, outside every data-region. The fog is server state for every viewer and a plain form (works with scripts off); its label says what IS. Reveal all lifts it for this tab - only, and is markup only when something here is blurred. #} - {% if not board %} -
+ {% if all_items | selectattr('blurred') | list %}{% endif %} {% endif %} {% if not board %}{{ m.text }}{% endfor %}
{% endif %}
+ {# Blur honesty reaches the full page too (r2b, heid code-review): a blurred
+ doc's own page rendered clear. Its reveal is per-page and JS-only, like the
+ review stage's; Reveal all lifts it by the same class. #}
+