feat(cli): booth blur <name> with no files fogs the whole booth

The operator: "per booth blurring is now important since we are showing up to 4
images."

The Desk is why. Measured on the live set: 84 images across 22 booths on the
page he opens first, 10 of them blurred. Before the redesign the index showed
one cover per booth; four-up multiplies the exposure by four, and NOTHING POSTED
BEFORE THE REDESIGN OPTED INTO THAT.

The storage landed with the flag; this is the half that makes it usable before
design-dev's control ships. Seventeen handles call this script, so a session
posting sensitive work can self-blur AT POST TIME — which is the durable fix,
because the operator should not have to police 22 booths by hand.

No files named means the whole booth, which is the mental model already: `blur
<name> <file>...` was per item and required two arguments, so one argument could
only ever have been an error. COMPOSES with the per-item list: `unblur <name>`
clears the flag and leaves individual choices exactly as they were, the same
promise the resolver makes.

Also records both rulings routed this turn: x hides with the other Desk
controls, and the theme toggle reaches the chrome inside verbatim pages.

Verified under the system python3 with no venv, which is the only way most
callers ever run it.
This commit is contained in:
vh
2026-09-23 17:25:12 -07:00
parent c1108a1966
commit a9e71108a7
2 changed files with 59 additions and 2 deletions
+22 -2
View File
@@ -240,7 +240,7 @@ sys.stdout.write("N" if name is None else "B:" + name)
} }
usage() { usage() {
echo "usage: booth {new <name> [--why W] [--title T]|add <name> <file>... [--why W] [--title T]|url <name>|ls|rm <name>|keep <name>|unkeep <name>|blur <name> <file>...|unblur <name> <file>...|link <url> [description]|links|unlink <id|index>|ask <name> <id> <prompt> <option>... [--no-notes]|marks <name> [--wait [SECS]]|asks <name> (deprecated alias for marks)|answer <name> <id> [--wait [SECS]]|marks-import <name>|bench add <url> <name>|bench ls|bench state <id|url> <live|promoted|retired>|bench rm <id|url>|bench import [--apply <id>...]}" >&2 echo "usage: booth {new <name> [--why W] [--title T]|add <name> <file>... [--why W] [--title T]|url <name>|ls|rm <name>|keep <name>|unkeep <name>|blur <name> [<file>...]|unblur <name> [<file>...]|link <url> [description]|links|unlink <id|index>|ask <name> <id> <prompt> <option>... [--no-notes]|marks <name> [--wait [SECS]]|asks <name> (deprecated alias for marks)|answer <name> <id> [--wait [SECS]]|marks-import <name>|bench add <url> <name>|bench ls|bench state <id|url> <live|promoted|retired>|bench rm <id|url>|bench import [--apply <id>...]}" >&2
exit 2 exit 2
} }
@@ -303,10 +303,30 @@ case "$cmd" in
# screen-share, a scroll past something you did not want full-size. The # screen-share, a scroll past something you did not want full-size. The
# Booth has no auth by design: if a thing must not be SEEN, it must not be # Booth has no auth by design: if a thing must not be SEEN, it must not be
# in a booth. # in a booth.
[ $# -ge 2 ] || usage [ $# -ge 1 ] || usage
b="$1"; shift b="$1"; shift
[ -d "$DATA/$b" ] || { echo "no such booth: $b" >&2; exit 1; } [ -d "$DATA/$b" ] || { echo "no such booth: $b" >&2; exit 1; }
f="$DATA/$b/$BLUR" f="$DATA/$b/$BLUR"
# NO FILES NAMED = THE WHOLE BOOTH. The Desk shows up to four images from
# every booth on the page the operator opens first, so a booth that should
# not be glanced at needs to say so as a BOOTH, not item by item — and the
# session that posts it is the one that knows.
#
# A marker, and it COMPOSES with the per-item list rather than replacing
# it: `unblur <name>` clears the booth flag and leaves individual choices
# exactly as they were.
if [ $# -eq 0 ]; then
if [ "$cmd" = blur ]; then
touch "$DATA/$b/.blurbooth"
echo "whole booth blurred (cosmetic — still served): $URL/b/$b/"
else
rm -f -- "$DATA/$b/.blurbooth"
echo "whole booth un-blurred (per-item blur kept): $URL/b/$b/"
fi
exit 0
fi
for item in "$@"; do for item in "$@"; do
item="${item#"$DATA/$b/"}"; item="${item#/}" item="${item#"$DATA/$b/"}"; item="${item#/}"
case "$item" in case "$item" in
+37
View File
@@ -659,3 +659,40 @@ def test_the_append_happens_INSIDE_the_lock(booth):
fcntl.flock(lf, fcntl.LOCK_UN) fcntl.flock(lf, fcntl.LOCK_UN)
assert (board / "links.md").read_text() == "", \ assert (board / "links.md").read_text() == "", \
"the row was appended while another writer held the lock" "the row was appended while another writer held the lock"
def test_blur_with_no_files_fogs_the_whole_booth(booth):
"""The Desk shows up to four images from EVERY booth on the page the
operator opens first, so a booth that should not be glanced at has to say
so as a booth — and the session that posts it is the one that knows.
Seventeen handles call this script; a verb here is how they self-blur at
post time without waiting for anyone to click anything."""
data, b = booth
(b / "a.png").write_bytes(b"x")
out = run(data, "blur", "b")
assert out.returncode == 0, out.stderr
assert (b / ".blurbooth").exists()
assert "whole booth blurred" in out.stdout
out = run(data, "unblur", "b")
assert out.returncode == 0, out.stderr
assert not (b / ".blurbooth").exists()
def test_unblurring_the_booth_keeps_per_item_choices(booth):
"""COMPOSES, never overrides — the same promise the resolver makes. An
agent's per-item blur must survive the booth flag being cleared.
Defeating change: `unblur <name>` also clearing `.blurred`."""
data, b = booth
for n in ("a.png", "b.png"):
(b / n).write_bytes(b"x")
run(data, "blur", "b", "a.png")
run(data, "blur", "b")
run(data, "unblur", "b")
assert not (b / ".blurbooth").exists()
assert (b / ".blurred").read_text().strip() == "a.png"