feat(blur): a booth can be fogged as a whole, composing with per-item blur
The operator ruled booth-level blur in and chose reading A for the reveal
("A is fine"). design-dev specced the semantics and owns the controls; this is
the storage half.
COMPOSES, NEVER OVERRIDES. An item is blurred iff the booth is blurred OR it is
in .blurred, so turning booth blur off leaves an agent's per-item choice exactly
as the poster left it. An override would need a per-item "unblurred" exception
list, which is state nobody can see.
Resolved in booth_items, so every surface inherits it for free — Desk strip,
tiles, flag tray, filmstrip, stage all already read Item.blurred and none of
them learns the booth flag exists (INV-1). Images and video only; audio has
nothing to hide from a glance.
A MARKER, deliberately not JSON. `.seen` is JSON because it holds rels that must
round-trip exactly; a boolean has nothing to round-trip, and matching `.forever`
means the two whole-booth flags read the same way. We told design-dev it would
be JSON and it should not be — said so rather than quietly shipping the other
thing.
is_booth_blurred mirrors is_kept's lstat shape WITH THE SAFETY INVERTED, and the
inversion is the point: is_kept fails toward keeping because a failed read must
not authorise a delete; this fails toward HIDING, because a failed read must not
reveal something a poster asked to fog. Both are "the failure does not cause the
loss".
Also records the operator's 2026-09-23 ruling that there is NO 1.0 yet, and adds
.blurbooth to CLAUDE.md's dotfile list. 766 green.
This commit is contained in:
@@ -80,6 +80,7 @@ TEMPLATES_DIR = Path(__file__).parent / "templates"
|
||||
from booth.items import ( # noqa: E402,F401
|
||||
AUDIO_EXTS,
|
||||
BLUR_FILE,
|
||||
BOOTH_BLUR_FILE,
|
||||
CAPTION_MAX,
|
||||
DOC_MAX_BYTES,
|
||||
IMAGE_EXTS,
|
||||
@@ -96,6 +97,7 @@ from booth.items import ( # noqa: E402,F401
|
||||
REVIEW_KINDS,
|
||||
SEEN_FILE,
|
||||
read_seen,
|
||||
is_booth_blurred,
|
||||
read_blurred,
|
||||
render_doc,
|
||||
render_doc_body,
|
||||
@@ -123,6 +125,30 @@ VIEW_MARKER = ".viewed"
|
||||
# Anyone who reads this marker as protection has misread it.
|
||||
|
||||
|
||||
def set_booth_blurred(booth: Path, on: bool) -> bool:
|
||||
"""Fog or unfog a whole booth. Returns the state it is now in.
|
||||
|
||||
A marker, created and removed rather than written — so there is no window in
|
||||
which the file exists holding a half-written "off", which is the whole
|
||||
reason `.forever` is a marker too.
|
||||
|
||||
Never raises on the remove path: unfogging something already unfogged is the
|
||||
state the caller asked for, exactly as unflagging an unflagged item is."""
|
||||
marker = booth / BOOTH_BLUR_FILE
|
||||
if on:
|
||||
marker.touch(exist_ok=True)
|
||||
return True
|
||||
try:
|
||||
marker.unlink()
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
except OSError:
|
||||
# Cannot remove it, so it is still there and the booth is still blurred.
|
||||
# Saying "off" here would be a lie the next render contradicts.
|
||||
return is_booth_blurred(booth)
|
||||
return False
|
||||
|
||||
|
||||
def set_blurred(booth: Path, rel: str, on: bool) -> set[str]:
|
||||
"""Add or remove one item from the blur set. Atomic replace, so a crash
|
||||
mid-write cannot leave a half-file that read_blurred would parse as a
|
||||
@@ -2079,6 +2105,22 @@ def create_app(
|
||||
record_view(booth)
|
||||
return RedirectResponse(url=_safe_next(next), status_code=303)
|
||||
|
||||
@app.post("/b/{name}/blurbooth")
|
||||
def booth_blur_all(name: str, on: str = Form("1"), back: str = Form("")):
|
||||
"""Toggle blur for the WHOLE booth — the operator's header control, and
|
||||
what an agent sets at post time by dropping the marker in the folder.
|
||||
|
||||
COMPOSES with per-item blur and never overrides it: turning this off
|
||||
leaves `.blurred` exactly as the poster left it. Reversible and
|
||||
cosmetic, so no confirmation — and, like per-item blur, it hides from a
|
||||
glance and does not protect anything."""
|
||||
booth = resolve_booth(name)
|
||||
set_booth_blurred(booth, on not in ("0", "false", ""))
|
||||
landing = f"/b/{quote(name, safe='')}/"
|
||||
if back:
|
||||
landing += f"view?f={quote(back, safe='/')}"
|
||||
return RedirectResponse(url=landing, status_code=303)
|
||||
|
||||
@app.post("/b/{name}/blur")
|
||||
def booth_blur(name: str, f: str = Form(...), on: str = Form("1")):
|
||||
"""Toggle one item's blur. Reversible and cosmetic, so no confirmation.
|
||||
|
||||
Reference in New Issue
Block a user