fix(blur): the heid code-review and bug-hunt panels on r2b merge 1, folded

Both panels ran 4/4 on 5ded5ff. They converged on the board and doc-page
gaps independently.

- A board holding files lost both blur controls (they sat inside the
  board suppression meant for the one-click wipe), while its items'
  "◉ booth" labels pointed at them. Only the wipe is board-suppressed now.
- A blurred doc's own full page rendered clear. Its body is blurred there
  too, with its own reveal and a Reveal all to put the blur back.
- set_booth_blurred followed a planted .blurbooth symlink (`touch`), and
  the new control made that a click away. Anything at the name already
  reads as fogged; otherwise it is created O_CREAT|O_EXCL|O_NOFOLLOW.
- The fog landing echoed `back` unchecked into the 303. It is now built
  from the review ring, as the mark routes do.
- The fog form is its own region, so an in-place save refreshes its
  label. Reveal all stays outside every region: its state lives in the
  tab.
- The review's Space-to-advance no longer swallows Space on a focused
  button or link.
- Top-bar controls stay on one line at phone width.
- Tests tightened:
  - method="post" on the fog forms;
  - exact blur values;
  - a storage READ that throws;
  - an item's own reveal carried across a swap;
  - reveal gated where it can act.

r2b.toml: 26/26 proved. 774 passed.
This commit is contained in:
vh
2026-09-23 18:41:32 -07:00
parent 5ded5ffe55
commit 75623c7dbc
9 changed files with 405 additions and 30 deletions
+19 -2
View File
@@ -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.""" state the caller asked for, exactly as unflagging an unflagged item is."""
marker = booth / BOOTH_BLUR_FILE marker = booth / BOOTH_BLUR_FILE
if on: 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 return True
try: try:
marker.unlink() marker.unlink()
@@ -2143,7 +2157,10 @@ def create_app(
booth = resolve_booth(name) booth = resolve_booth(name)
set_booth_blurred(booth, on not in ("0", "false", "")) set_booth_blurred(booth, on not in ("0", "false", ""))
landing = f"/b/{quote(name, safe='')}/" 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='/')}" landing += f"view?f={quote(back, safe='/')}"
return RedirectResponse(url=landing, status_code=303) return RedirectResponse(url=landing, status_code=303)
+7 -1
View File
@@ -616,6 +616,11 @@
.blur-all button:hover,.reveal-all-btn:hover{color:var(--text-body);border-color:var(--border-strong)} .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)} .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)} .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)} .badge-blur{color:var(--text-muted)}
/* ---- inline docs ------------------------------------------------------ /* ---- inline docs ------------------------------------------------------
@@ -1082,7 +1087,8 @@
var key = 'booth.reveal:' + b; var key = 'booth.reveal:' + b;
function show(btn) { function show(btn) {
var on = d.classList.contains('reveal-all'); 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'); btn.setAttribute('aria-pressed', on ? 'true' : 'false');
} }
ctl.forEach(function (btn) { ctl.forEach(function (btn) {
+11 -5
View File
@@ -106,13 +106,19 @@
{# r2b D2b + D2: the booth-wide blur controls, outside every data-region. {# 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 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 scripts off); its label says what IS. Reveal all lifts it for this tab
only, and is markup only when something here is blurred. #} only, and is markup only when something here is blurred. A BOARD gets
{% if not board %} them too when it holds files: only the one-click wipe is board-suppressed,
<form class="blur-all{% if booth_blurred %} is-on{% endif %}" method="post" action="/b/{{ name_url }}/blurbooth"> and an item's "◉ booth" label points here. #}
{% if all_items %}
{# The fog form IS a region: its label is server state, so an in-place save
refreshes it with everything else (a fog set elsewhere since this page
loaded would otherwise leave it saying "blur booth"). Reveal all is not:
its state lives in this tab, and a swap must never reset it. #}
<span class="region-wrap" data-region="blur-booth"><form class="blur-all{% if booth_blurred %} is-on{% endif %}" method="post" action="/b/{{ name_url }}/blurbooth">
<input type="hidden" name="on" value="{{ '0' if booth_blurred else '1' }}"> <input type="hidden" name="on" value="{{ '0' if booth_blurred else '1' }}">
<button title="{{ 'un-blur the whole booth — per-item blur stays as it was' if booth_blurred else 'blur every image and video in this booth — cosmetic only, the files are still served' }}">{{ '◉ booth blurred' if booth_blurred else '◌ blur booth' }}</button> <button title="{{ 'un-blur the whole booth — per-item blur stays as it was' if booth_blurred else 'blur every image and video in this booth — cosmetic only, the files are still served' }}">{{ '◉ booth blurred' if booth_blurred else '◌ blur booth' }}</button>
</form> </form></span>
{% if all_items | selectattr('blurred') | list %}<button type="button" class="reveal-all-btn" data-reveal-all hidden>👁 reveal all — blur is cosmetic</button>{% endif %} {% if all_items | selectattr('blurred') | list %}<button type="button" class="reveal-all-btn" data-reveal-all hidden title="blur is cosmetic — the files are still served"><span class="ra-label">👁 reveal all</span><span class="ra-note"> — blur is cosmetic</span></button>{% endif %}
{% endif %} {% endif %}
{% if not board %} {% if not board %}
<form class="wipe wipe-lg" method="post" action="/b/{{ name_url }}/delete" <form class="wipe wipe-lg" method="post" action="/b/{{ name_url }}/delete"
+25
View File
@@ -7,6 +7,9 @@
<a class="vbtn vx" href="/b/{{ name_url }}/" title="back to gallery (Esc)">✕</a> <a class="vbtn vx" href="/b/{{ name_url }}/" title="back to gallery (Esc)">✕</a>
<span class="vname">{{ file }}</span> <span class="vname">{{ file }}</span>
<span class="vspacer"></span> <span class="vspacer"></span>
{# Reveal all can lift this page's blur, so this page must be able to put it
back (r2b, heid bug-hunt). #}
{% if blurred %}<button type="button" class="reveal-all-btn" data-reveal-all hidden title="blur is cosmetic — the files are still served"><span class="ra-label">👁 reveal all</span><span class="ra-note"> — blur is cosmetic</span></button>{% endif %}
<a class="vbtn" href="{{ file_url }}?dl=1" title="download {{ file }}">⬇</a> <a class="vbtn" href="{{ file_url }}?dl=1" title="download {{ file }}">⬇</a>
</div> </div>
{# Same record, same reason as the image viewer: the sidecar that says what {# Same record, same reason as the image viewer: the sidecar that says what
@@ -17,12 +20,18 @@
{% for m in marks if m.shape == 'note' %}<pre class="vnote">{{ m.text }}</pre>{% endfor %} {% for m in marks if m.shape == 'note' %}<pre class="vnote">{{ m.text }}</pre>{% endfor %}
</div> </div>
{% endif %} {% 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 <html> class. #}
<div class="docbody{% if blurred %} is-blurred{% endif %}" id="docbody">
{% if blurred %}<button type="button" class="reveal" id="docreveal" hidden>👁 reveal — blur is cosmetic</button>{% endif %}
{% if is_html %} {% if is_html %}
<article class="markdown-body">{{ body|safe }}</article> <article class="markdown-body">{{ body|safe }}</article>
{% else %} {% else %}
<pre class="textview">{{ body }}</pre> <pre class="textview">{{ body }}</pre>
{% endif %} {% endif %}
</div> </div>
</div>
<style> <style>
/* .markdown-body and .textview live in base.html (shared with the inline /* .markdown-body and .textview live in base.html (shared with the inline
gallery view). Only the full-page layout wrapper is page-specific. */ gallery view). Only the full-page layout wrapper is page-specific. */
@@ -32,6 +41,14 @@
.doccap{margin:0 0 20px;padding:8px 14px;font-size:var(--size-body);line-height:var(--leading-body); .doccap{margin:0 0 20px;padding:8px 14px;font-size:var(--size-body);line-height:var(--leading-body);
color:var(--text-body);border-left:3px solid var(--border-strong);white-space:pre-wrap} color:var(--text-body);border-left:3px solid var(--border-strong);white-space:pre-wrap}
.docmarks{display:flex;flex-direction:column;gap:8px;margin:0 0 20px} .docmarks{display:flex;flex-direction:column;gap:8px;margin:0 0 20px}
.docbody{position:relative}
.docbody.is-blurred .markdown-body,.docbody.is-blurred .textview{filter:blur(22px);transition:filter var(--dur-2)}
.docbody.is-blurred.revealed .markdown-body,.docbody.is-blurred.revealed .textview,
.reveal-all .docbody.is-blurred .markdown-body,.reveal-all .docbody.is-blurred .textview{filter:none}
.reveal-all #docreveal{display:none}
#docreveal{position:absolute;top:10px;left:10px;z-index:2;cursor:pointer;font-family:var(--font-mono);
font-size:var(--size-micro);line-height:1;padding:6px 9px;border-radius:var(--radius-md);
border:1px solid rgb(255 255 255 / .16);background:oklch(0.17 0.01 250 / .86);color:oklch(0.91 0.008 216)}
</style> </style>
<script> <script>
(function () { (function () {
@@ -42,6 +59,14 @@
return !!(el && (el.isContentEditable || return !!(el && (el.isContentEditable ||
/^(input|textarea|select)$/i.test(el.tagName || ''))); /^(input|textarea|select)$/i.test(el.tagName || '')));
} }
var rv = document.getElementById('docreveal');
if (rv) {
rv.hidden = false;
rv.addEventListener('click', function () {
var on = document.getElementById('docbody').classList.toggle('revealed');
rv.textContent = on ? '🙈 hide' : '👁 reveal — blur is cosmetic';
});
}
document.addEventListener('keydown', function (e) { document.addEventListener('keydown', function (e) {
if (isEditable(e.target)) return; if (isEditable(e.target)) return;
if (e.key === 'Escape') window.location.href = {{ ('/b/' ~ name_url ~ '/')|tojson }}; if (e.key === 'Escape') window.location.href = {{ ('/b/' ~ name_url ~ '/')|tojson }};
+7 -4
View File
@@ -23,12 +23,12 @@
{% endif %} {% endif %}
{# r2b D2b + D2, in the top bar: outside every data-region, so no swap {# r2b D2b + D2, in the top bar: outside every data-region, so no swap
replaces them. The fog form carries `back` and lands on this item. #} replaces them. The fog form carries `back` and lands on this item. #}
<form class="blur-all{% if booth_blurred %} is-on{% endif %}" method="post" action="/b/{{ name_url }}/blurbooth"> <span class="region-wrap" data-region="blur-booth"><form class="blur-all{% if booth_blurred %} is-on{% endif %}" method="post" action="/b/{{ name_url }}/blurbooth">
<input type="hidden" name="on" value="{{ '0' if booth_blurred else '1' }}"> <input type="hidden" name="on" value="{{ '0' if booth_blurred else '1' }}">
<input type="hidden" name="back" value="{{ file }}"> <input type="hidden" name="back" value="{{ file }}">
<button title="{{ 'un-blur the whole booth' if booth_blurred else 'blur every image and video in this booth — cosmetic only' }}">{{ '◉ booth blurred' if booth_blurred else '◌ blur booth' }}</button> <button title="{{ 'un-blur the whole booth' if booth_blurred else 'blur every image and video in this booth — cosmetic only' }}">{{ '◉ booth blurred' if booth_blurred else '◌ blur booth' }}</button>
</form> </form></span>
{% if film | selectattr('blurred') | list %}<button type="button" class="reveal-all-btn" data-reveal-all hidden>👁 reveal all — blur is cosmetic</button>{% endif %} {% if film | selectattr('blurred') | list %}<button type="button" class="reveal-all-btn" data-reveal-all hidden title="blur is cosmetic — the files are still served"><span class="ra-label">👁 reveal all</span><span class="ra-note"> — blur is cosmetic</span></button>{% endif %}
<a class="vbtn" href="{{ file_url }}" download title="download {{ file }}">⬇</a> <a class="vbtn" href="{{ file_url }}" download title="download {{ file }}">⬇</a>
</div> </div>
@@ -230,7 +230,10 @@
if (e.key === 'Escape') window.location.href = BACK; if (e.key === 'Escape') window.location.href = BACK;
else if (e.key === 'ArrowLeft' && PREV) window.location.href = PREV; else if (e.key === 'ArrowLeft' && PREV) window.location.href = PREV;
else if (e.key === 'ArrowRight' && NEXT) window.location.href = NEXT; else if (e.key === 'ArrowRight' && NEXT) window.location.href = NEXT;
else if (e.key === ' ' && NEXT && e.target !== player) { e.preventDefault(); window.location.href = e.shiftKey && PREV ? PREV : NEXT; } /* ...and never from a focused control: Space is how a keyboard presses a
button or follows a link (r2b, heid bug-hunt — Reveal all and the fog
control could not be pressed). */
else if (e.key === ' ' && NEXT && e.target !== player && !(e.target.closest && e.target.closest('button, a, summary'))) { e.preventDefault(); window.location.href = e.shiftKey && PREV ? PREV : NEXT; }
else if (e.key === 'f' || e.key === 'F') { else if (e.key === 'f' || e.key === 'F') {
var b = document.getElementById('vflag-btn'); /* re-read: the rail may have been swapped */ var b = document.getElementById('vflag-btn'); /* re-read: the rail may have been swapped */
if (b) { e.preventDefault(); b.click(); } if (b) { e.preventDefault(); b.click(); }
@@ -104,11 +104,15 @@ different kind of thing from the lifetime pill (state) and the controls
## D2 — Reveal all (blur ruling A) ## D2 — Reveal all (blur ruling A)
- **One STATE per booth, shown by a control in two places: "👁 reveal all — - **One STATE per booth, shown by a control in two places: "👁 reveal all —
blur is cosmetic" / "🙈 blur again".** One instance is in the booth header, blur is cosmetic" / "🙈 blur again".** It appears in the booth header, the
one in the review's top bar, and both sit OUTSIDE every `data-region`, so no review's top bar and a blurred doc's own top bar. Every instance sits OUTSIDE
in-place swap replaces them. The server puts the control in the markup only every `data-region`, so no in-place swap replaces it: its state lives in the
when at least one item of the booth is blurred (`Item.blurred`), and always tab, and a swap must never reset it. Below 600px it reads "👁 reveal all";
with the `hidden` attribute. The script removes `hidden` and binds it. Without its title still says the blur is cosmetic. The server puts the control in the markup only
where it can act, and always with the `hidden` attribute: in the header when
any item of the booth is blurred (`Item.blurred`), and in the review when any
item of the review RING is (a blurred doc is not on the review page, so a
control there would act on nothing). The script removes `hidden` and binds it. Without
JS it is in the markup but never shown. JS it is in the markup but never shown.
- **State: `sessionStorage["booth.reveal:" + <booth name>] = "1"`.** Per booth, - **State: `sessionStorage["booth.reveal:" + <booth name>] = "1"`.** Per booth,
per tab, gone when the tab closes, so a blurred booth is blurred again next per tab, gone when the tab closes, so a blurred booth is blurred again next
@@ -131,8 +135,15 @@ different kind of thing from the lifetime pill (state) and the controls
- Because every change of page is a full load, the class is re-decided per - Because every change of page is a full load, the class is re-decided per
page, from that page's `data-booth`. Booth A's reveal cannot follow you page, from that page's `data-booth`. Booth A's reveal cannot follow you
into booth B. into booth B.
- The index carries no `data-booth`, so **nothing on the index is revealed - The index's `<html>` carries no `data-booth` (its rows' own `data-booth`
by D2**, the Desk strip included. attributes are unrelated), so **nothing on the index is revealed by D2**,
the Desk strip included.
- **A board holding files gets both controls.** Only the one-click wipe is
board-suppressed; an item's "◉ booth" label points at the header control,
so the control must be there.
- **The full-page doc view is blurred honestly.** A blurred doc's own page
renders its body blurred, with its own JS-only reveal; Reveal all lifts it
by the same `<html>` class.
- **The per-item reveal defers to it, BY STYLESHEET.** Under `.reveal-all` the - **The per-item reveal defers to it, BY STYLESHEET.** Under `.reveal-all` the
per-tile and stage reveal buttons are `display: none`. That is a CSS per-tile and stage reveal buttons are `display: none`. That is a CSS
consequence of the class, so markup swapped in after a save obeys it with no consequence of the class, so markup swapped in after a save obeys it with no
@@ -148,9 +159,22 @@ booth-dev landed the whole-booth marker and its route while the panel was
reading; this is the control the operator uses, which the blur ruling assumed. reading; this is the control the operator uses, which the blur ruling assumed.
- **"◌ blur booth" / "◉ booth blurred" in the booth header and the review's - **"◌ blur booth" / "◉ booth blurred" in the booth header and the review's
tape bar**: a plain `<form method=post action=/b/<name>/blurbooth>` with top bar (`.vbar`)**, each wrapped in its OWN region, `blur-booth`. Its label is
server state, so an in-place save refreshes it with everything else; a fog
set elsewhere since the page loaded would otherwise leave it saying "blur
booth". It is a plain `<form method=post action=/b/<name>/blurbooth>` with
`on=1|0`, so it works with scripts off (INV-2). From the review it carries `on=1|0`, so it works with scripts off (INV-2). From the review it carries
`back=<rel>` and lands back on the same item. `back=<rel>`. The route lands on the review only when `back` is an item of
the review ring, and otherwise on the booth page; the landing is built from
the ring, never echoed.
- **Fogging never writes through a link.** booth-dev's `set_booth_blurred` used
`touch()`, which followed a planted `.blurbooth` symlink: a click of this
control rewrote an outside file's mtime, or created a dangling target. Any
entry already at the name reads as fogged, so nothing is written; otherwise
the marker is created with `O_CREAT | O_EXCL | O_NOFOLLOW`.
- **Space never hijacks a focused control.** The review's Space-to-advance
ignores a focused button, link or summary, so a keyboard can press these
controls.
- **Its state comes from the server**, never from the client: - **Its state comes from the server**, never from the client:
`booth_blurred = is_booth_blurred(booth)` in both contexts. The label says `booth_blurred = is_booth_blurred(booth)` in both contexts. The label says
what IS, and pressing it flips it. what IS, and pressing it flips it.
@@ -235,8 +259,9 @@ reading; this is the control the operator uses, which the blur ruling assumed.
preview image, the text column, the side column, the pill — is identical preview image, the text column, the side column, the pill — is identical
with the cluster present or removed. with the cluster present or removed.
- **INV-4 — blur honesty (r2 INV-8).** Nothing on the index is revealed by D2. - **INV-4 — blur honesty (r2 INV-8).** Nothing on the index is revealed by D2.
- **INV-5 — autoescape.** The booth name reaches the page only as an escaped - **INV-5 — autoescape.** The booth name reaches the reveal-state machinery
attribute value. only as an escaped attribute value (`data-booth`), read by `getAttribute` and
never templated into a script.
- **INV-6 — no flash.** A forced theme and a set reveal are applied before - **INV-6 — no flash.** A forced theme and a set reveal are applied before
first paint. first paint.
+127 -2
View File
@@ -69,9 +69,9 @@ label = "D2 the control shows without JS (server markup not hidden)"
file = "booth/templates/booth.html" file = "booth/templates/booth.html"
test = "tests/test_flow_browser.py::test_reveal_all_never_shows_without_js_and_a_storage_failure_still_applies_the_click" test = "tests/test_flow_browser.py::test_reveal_all_never_shows_without_js_and_a_storage_failure_still_applies_the_click"
old = ''' old = '''
{% if all_items | selectattr('blurred') | list %}<button type="button" class="reveal-all-btn" data-reveal-all hidden>''' {% if all_items | selectattr('blurred') | list %}<button type="button" class="reveal-all-btn" data-reveal-all hidden title='''
new = ''' new = '''
{% if all_items | selectattr('blurred') | list %}<button type="button" class="reveal-all-btn" data-reveal-all>''' {% if all_items | selectattr('blurred') | list %}<button type="button" class="reveal-all-btn" data-reveal-all title='''
[[mutation]] [[mutation]]
label = "D2 the control is markup even when nothing is blurred" label = "D2 the control is markup even when nothing is blurred"
@@ -137,3 +137,128 @@ old = '''
"blurred_self": it.rel in own_blur,''' "blurred_self": it.rel in own_blur,'''
new = ''' new = '''
"blurred_self": it.blurred,''' "blurred_self": it.blurred,'''
# ---- folds: the heid code-review ("BLITZ-2") and bug-hunt ("FENRIR-6") panels on merge 1
[[mutation]]
label = "a board holding files loses the blur controls its labels point at"
file = "booth/templates/booth.html"
test = "tests/test_flow.py::test_a_board_with_files_gets_the_blur_controls_its_labels_point_at"
old = '''
{% if all_items %}
{# The fog form IS a region'''
new = '''
{% if not board %}
{# The fog form IS a region'''
[[mutation]]
label = "a blurred doc's own page renders clear"
file = "booth/templates/doc.html"
test = "tests/test_flow.py::test_a_blurred_docs_own_page_is_blurred_too"
old = '''
<div class="docbody{% if blurred %} is-blurred{% endif %}" id="docbody">'''
new = '''
<div class="docbody" id="docbody">'''
[[mutation]]
label = "the review offers Reveal all when only a doc (off the ring) is blurred"
file = "booth/templates/view.html"
test = "tests/test_flow.py::test_reveal_all_renders_where_it_can_act"
old = '''
{% if film | selectattr('blurred') | list %}<button'''
new = '''
{% if true %}<button'''
[[mutation]]
label = "the fog form is a GET (changes nothing with scripts off)"
file = "booth/templates/booth.html"
test = "tests/test_flow.py::test_the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review"
old = '''<form class="blur-all{% if booth_blurred %} is-on{% endif %}" method="post" action="/b/{{ name_url }}/blurbooth">
<input type="hidden" name="on" value="{{ '0' if booth_blurred else '1' }}">
<button title="{{ 'un-blur the whole booth — per-item'''
new = '''<form class="blur-all{% if booth_blurred %} is-on{% endif %}" method="get" action="/b/{{ name_url }}/blurbooth">
<input type="hidden" name="on" value="{{ '0' if booth_blurred else '1' }}">
<button title="{{ 'un-blur the whole booth — per-item'''
[[mutation]]
label = "the swap stops carrying an item's own reveal"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_an_items_own_reveal_survives_an_in_place_save"
old = '''
['revealed', 'is-closed'].forEach(function (c) {'''
new = '''
['is-closed'].forEach(function (c) {'''
[[mutation]]
label = "a storage READ that throws raises out of the pre-paint script"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_reveal_all_never_shows_without_js_and_a_storage_failure_still_applies_the_click"
old = '''
try {
if (b !== null && sessionStorage.getItem('booth.reveal:' + b) === '1') d.classList.add('reveal-all');
} catch (e) {}'''
new = '''
if (b !== null && sessionStorage.getItem('booth.reveal:' + b) === '1') d.classList.add('reveal-all');'''
[[mutation]]
label = "fogging writes through a planted marker link"
file = "booth/app.py"
test = "tests/test_flow.py::test_fogging_never_writes_through_a_planted_marker_link"
old = '''
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'''
new = '''
marker.touch(exist_ok=True)
return True'''
[[mutation]]
label = "the fog landing echoes `back` unchecked"
file = "booth/app.py"
test = "tests/test_flow.py::test_the_fog_landing_is_built_from_the_ring_never_echoed"
old = '''
if back and back in review_chain(booth_items(booth)):'''
new = '''
if back:'''
[[mutation]]
label = "the fog form is outside every region (a swap leaves its label stale)"
file = "booth/templates/booth.html"
test = "tests/test_flow.py::test_the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review"
old = '''
<span class="region-wrap" data-region="blur-booth"><form class="blur-all'''
new = '''
<span class="region-wrap"><form class="blur-all'''
[[mutation]]
label = "Space on a focused review button moves to the next item"
file = "booth/templates/view.html"
test = "tests/test_flow_browser.py::test_space_on_a_focused_review_button_presses_it_and_does_not_move_on"
old = '''e.target !== player && !(e.target.closest && e.target.closest('button, a, summary'))) {'''
new = '''e.target !== player) {'''
[[mutation]]
label = "the top-bar controls squeeze into multi-line stacks at phone width"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_the_review_and_doc_top_bars_fit_a_phone"
old = '''
.blur-all button,.reveal-all-btn{white-space:nowrap}
@media (max-width:600px){.reveal-all-btn .ra-note{display:none}}'''
new = '''
'''
[[mutation]]
label = "the Desk strip under another booth's reveal is lifted by a whisker (blur(0px) is not blurred)"
file = "booth/templates/base.html"
test = "tests/test_flow_browser.py::test_reveal_all_on_booth_a_does_not_reveal_booth_b_or_the_desk"
old = '''
.blurred-thumb{filter:blur(16px)}'''
new = '''
.blurred-thumb{filter:blur(0px)}'''
+80 -1
View File
@@ -861,8 +861,10 @@ def test_the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review(tmp
c = _client(tmp_path) c = _client(tmp_path)
page = c.get("/b/g/").text page = c.get("/b/g/").text
form = re.search(r'<form[^>]*action="/b/g/blurbooth".*?</form>', page, re.S).group(0) form = re.search(r'<form[^>]*action="/b/g/blurbooth".*?</form>', page, re.S).group(0)
assert 'method="post"' in form, "a GET form would not change anything with scripts off"
assert 'name="on" value="1"' in form and "blur booth" in form assert 'name="on" value="1"' in form and "blur booth" in form
assert all(form not in r for r in _regions(page)), "a swap must never replace it" # a REGION: its label is server state, so an in-place save refreshes it
assert form in _region(page, "blur-booth")
assert "badge-blur" not in re.search(r'data-booth="g".*?</article>', c.get("/").text, re.S).group(0) assert "badge-blur" not in re.search(r'data-booth="g".*?</article>', c.get("/").text, re.S).group(0)
r = c.post("/b/g/blurbooth", data={"on": "1"}, follow_redirects=False) r = c.post("/b/g/blurbooth", data={"on": "1"}, follow_redirects=False)
@@ -876,6 +878,7 @@ def test_the_booth_blur_toggle_works_without_js_and_lands_back_on_the_review(tmp
view = c.get("/b/g/view?f=b.png").text view = c.get("/b/g/view?f=b.png").text
vbar = re.search(r'<div class="vbar">.*?</div>\s*\n', view, re.S).group(0) vbar = re.search(r'<div class="vbar">.*?</div>\s*\n', view, re.S).group(0)
vform = re.search(r'<form[^>]*action="/b/g/blurbooth".*?</form>', vbar, re.S).group(0) vform = re.search(r'<form[^>]*action="/b/g/blurbooth".*?</form>', vbar, re.S).group(0)
assert 'method="post"' in vform
assert 'name="back" value="b.png"' in vform and 'name="on" value="0"' in vform assert 'name="back" value="b.png"' in vform and 'name="on" value="0"' in vform
r = c.post("/b/g/blurbooth", data={"on": "0", "back": "b.png"}, follow_redirects=False) r = c.post("/b/g/blurbooth", data={"on": "0", "back": "b.png"}, follow_redirects=False)
assert r.headers["location"] == "/b/g/view?f=b.png" and not (b / ".blurbooth").exists() assert r.headers["location"] == "/b/g/view?f=b.png" and not (b / ".blurbooth").exists()
@@ -918,3 +921,79 @@ def test_under_a_fogged_booth_each_items_blur_control_tells_the_truth(tmp_path):
assert re.search(r'action="/b/g/blur".*?name="on" value="0".*?◉ blurred', own, re.S) assert re.search(r'action="/b/g/blur".*?name="on" value="0".*?◉ blurred', own, re.S)
assert 'action="/b/g/blur"' not in booth assert 'action="/b/g/blur"' not in booth
assert "◉ booth" in booth assert "◉ booth" in booth
def test_a_board_with_files_gets_the_blur_controls_its_labels_point_at(tmp_path):
"""heid code-review (4/4): both booth-wide controls sat inside the board
suppression meant for the one-click wipe, so a links board holding a fogged
picture showed "◉ booth — un-blur the booth in the header" with no such
control in the header. Only the wipe is board-suppressed."""
b = _booth(tmp_path, "links", {"links.md": b"- [x](https://example.test/)\n", "a.png": PNG})
(b / ".blurbooth").write_bytes(b"")
page = _client(tmp_path).get("/b/links/").text
assert re.search(r'<form[^>]*action="/b/links/blurbooth"', page)
assert re.search(r"<button[^>]*data-reveal-all", page)
def test_reveal_all_renders_where_it_can_act(tmp_path):
"""heid code-review (3/4): the header offers Reveal all when ANY item is
blurred; the review only when an item of the review RING is — a blurred doc
is not on the review page, so a control there would act on nothing."""
from booth.app import set_blurred
b = _booth(tmp_path, "g", {"a.png": PNG, "n.md": b"# n"})
set_blurred(b, "n.md", True)
c = _client(tmp_path)
assert re.search(r"<button[^>]*data-reveal-all", c.get("/b/g/").text)
assert not re.search(r"<button[^>]*data-reveal-all", c.get("/b/g/view?f=a.png").text)
def test_a_blurred_docs_own_page_is_blurred_too(tmp_path):
"""heid code-review (hulda): the full-page doc view never read `blurred`,
so a blurred doc rendered clear at the size where it is most readable.
Its body is blurred there too, with its own JS-only reveal."""
from booth.app import set_blurred
b = _booth(tmp_path, "g", {"n.md": b"# secret", "o.md": b"# open"})
set_blurred(b, "n.md", True)
c = _client(tmp_path)
blurred, clear = c.get("/b/g/view?f=n.md").text, c.get("/b/g/view?f=o.md").text
assert re.search(r'class="docbody is-blurred"', blurred)
assert re.search(r'<button[^>]*id="docreveal"[^>]*hidden', blurred)
assert 'class="docbody"' in clear and not re.search(r'<button[^>]*id="docreveal"', clear)
def test_fogging_never_writes_through_a_planted_marker_link(tmp_path):
"""heid bug-hunt (kimi, hulda): `marker.touch()` followed a planted
`.blurbooth` symlink — a click of the new browser control rewrote an
outside file's mtime, or CREATED a dangling target. The same class
`record_view` was hardened against. A link already there reads as fogged
(is_booth_blurred counts it), so fogging has nothing to write."""
import os
outside = tmp_path / "outside.txt"
outside.write_text("x")
os.utime(outside, (1_000_000, 1_000_000))
b = _booth(tmp_path, "g", {"a.png": PNG})
(b / ".blurbooth").symlink_to(outside)
h = _booth(tmp_path, "h", {"a.png": PNG})
(h / ".blurbooth").symlink_to(tmp_path / "created-by-a-click")
c = _client(tmp_path)
assert c.post("/b/g/blurbooth", data={"on": "1"}, follow_redirects=False).status_code == 303
assert c.post("/b/h/blurbooth", data={"on": "1"}, follow_redirects=False).status_code == 303
assert outside.stat().st_mtime == 1_000_000
assert not (tmp_path / "created-by-a-click").exists()
c.post("/b/g/blurbooth", data={"on": "0"}, follow_redirects=False)
assert not (b / ".blurbooth").is_symlink() and outside.exists() # unlinked, target untouched
def test_the_fog_landing_is_built_from_the_ring_never_echoed(tmp_path):
"""heid bug-hunt (kimi, regin, groa): `back` went into the 303 unchecked, so
a stale or foreign value landed on a 404. Like the mark routes' back=view:
the review only for an item of the review ring, else the booth page."""
_booth(tmp_path, "g", {"a.png": PNG, "n.md": b"# n"})
c = _client(tmp_path)
loc = lambda back: c.post("/b/g/blurbooth", data={"on": "1", "back": back},
follow_redirects=False).headers["location"]
assert loc("a.png") == "/b/g/view?f=a.png"
assert loc("gone.png") == "/b/g/"
assert loc("n.md") == "/b/g/"
assert loc("") == "/b/g/"
+93 -4
View File
@@ -618,8 +618,8 @@ def test_reveal_all_on_booth_a_does_not_reveal_booth_b_or_the_desk(browser, live
desk = page.evaluate("""() => [...document.querySelectorAll('.desk-strip img.blurred-thumb')] desk = page.evaluate("""() => [...document.querySelectorAll('.desk-strip img.blurred-thumb')]
.map(i => getComputedStyle(i).filter)""") .map(i => getComputedStyle(i).filter)""")
page.close() page.close()
assert "blur" in b_tile, b_tile assert b_tile == "blur(22px)", b_tile
assert desk and all("blur" in f for f in desk), desk assert desk and all(f == "blur(16px)" for f in desk), desk # exact: blur(0px) is not blurred
def test_reveal_all_survives_an_in_place_save(browser, live): def test_reveal_all_survives_an_in_place_save(browser, live):
@@ -676,11 +676,100 @@ def test_reveal_all_never_shows_without_js_and_a_storage_failure_still_applies_t
nojs = page.locator("[data-reveal-all]").is_visible() nojs = page.locator("[data-reveal-all]").is_visible()
ctx.close() ctx.close()
page = browser.new_page(viewport={"width": 1400, "height": 900}) page = browser.new_page(viewport={"width": 1400, "height": 900})
page.add_init_script("Storage.prototype.setItem = function () { throw new Error('quota'); };") errors = []
page.on("pageerror", lambda e: errors.append(str(e)))
# READS and WRITES both throw: the pre-paint read must degrade to "not
# revealed" without raising, and the click must still apply.
page.add_init_script("""Storage.prototype.setItem = function () { throw new Error('quota'); };
Storage.prototype.getItem = function () { throw new Error('denied'); };""")
page.goto(f"{base}/b/g/", wait_until="networkidle") page.goto(f"{base}/b/g/", wait_until="networkidle")
_settle(page)
before = page.evaluate(_FILTER, 'figure.item[data-item="a.png"] img')
page.locator("[data-reveal-all]").click() page.locator("[data-reveal-all]").click()
_settle(page) _settle(page)
lifted = page.evaluate(_FILTER, 'figure.item[data-item="a.png"] img') lifted = page.evaluate(_FILTER, 'figure.item[data-item="a.png"] img')
page.close() page.close()
assert not nojs assert not nojs
assert lifted == "none", lifted assert before == "blur(22px)" and lifted == "none", (before, lifted)
assert errors == [], errors
def test_an_items_own_reveal_survives_an_in_place_save(browser, live):
"""heid code-review (groa, kimi): nothing pinned the swap carrying an item's
own `revealed` — deleting it from the carry list left every test green.
Reveal one tile, save something else in place: it stays revealed, and its
button still says so."""
base, root = live
_blurred_set(root, blur=("a.png", "b.png"), flag=())
page = browser.new_page(viewport={"width": 1400, "height": 900})
page.goto(f"{base}/b/g/", wait_until="networkidle")
page.locator('figure.item[data-item="b.png"] .reveal').click()
page.locator('figure.item[data-item="a.png"] .flagtoggle button').click()
page.wait_for_selector('figure.item.is-flagged[data-item="a.png"]', timeout=10000)
_settle(page)
got = [page.evaluate(_FILTER, 'figure.item[data-item="b.png"] img'),
page.locator('figure.item[data-item="b.png"] .reveal').inner_text()]
page.close()
assert got[0] == "none" and "hide" in got[1], got
def test_reveal_all_lifts_a_blurred_docs_own_page(browser, live):
"""The doc page's blur (heid code-review) obeys the same <html> class, and
its own reveal works."""
from booth.app import set_blurred
base, root = live
b = root / "g"
b.mkdir()
(b / "n.md").write_text("# secret\n\nbody")
set_blurred(b, "n.md", True)
page = browser.new_page(viewport={"width": 1200, "height": 800})
page.goto(f"{base}/b/g/view?f=n.md", wait_until="networkidle")
_settle(page)
at_rest = page.evaluate(_FILTER, "#docbody .markdown-body, #docbody .textview")
page.locator("#docreveal").click()
_settle(page)
own = page.evaluate(_FILTER, "#docbody .markdown-body, #docbody .textview")
page.close()
assert at_rest == "blur(22px)" and own == "none", (at_rest, own)
def test_space_on_a_focused_review_button_presses_it_and_does_not_move_on(browser, live):
"""heid bug-hunt (hulda): the review's document-level Space handler moved to
the next item before a focused button could take the key, so a keyboard
user could not press Reveal all or the fog control with Space."""
base, root = live
_blurred_set(root, flag=())
page = browser.new_page(viewport={"width": 1400, "height": 900})
page.goto(f"{base}/b/g/view?f=a.png", wait_until="networkidle")
page.locator("[data-reveal-all]").focus()
page.keyboard.press(" ")
_settle(page)
got = [page.url, page.evaluate("document.documentElement.classList.contains('reveal-all')")]
page.close()
assert got[0].endswith("/b/g/view?f=a.png") and got[1], got
def test_the_review_and_doc_top_bars_fit_a_phone(browser, live):
"""heid bug-hunt (hulda, groa; needs-repro): the top bar gained the fog
control and Reveal all beside the name, the fit toggle and the download.
At 390px no review or doc page scrolls sideways, even with a long name."""
from booth.app import set_blurred
base, root = live
b = _blurred_set(root, flag=())
long = "a-rather-long-picture-name-" * 3 + ".png"
(b / long).write_bytes(PNG)
set_blurred(b, long, True)
(b / "n.md").write_text("# n")
set_blurred(b, "n.md", True)
over = {}
for url in (f"/b/g/view?f={long}", "/b/g/view?f=n.md"):
page = browser.new_page(viewport={"width": 390, "height": 844})
page.goto(f"{base}{url}", wait_until="networkidle")
over[url] = page.evaluate(
"document.documentElement.scrollWidth - document.documentElement.clientWidth")
# and nothing squeezed into a stack: every top-bar control is one line
over[url + " tallest"] = page.evaluate("""() => Math.max(...[...document.querySelectorAll(
'.vbar button, .vbar .vbtn')].filter(e => e.offsetParent).map(e => e.getBoundingClientRect().height)) - 40""")
page.close()
assert all(v <= 0 for v in over.values()), over