feat(booth): close the keep round trip, and add cosmetic per-item blur

Two operator requests.

KEEP, BOTH DIRECTIONS. The kept lane could already release a booth back to
ephemeral, but an ephemeral booth could only be promoted from a shell -- so the
round trip was closed only if you had ssh. The /keep route and the `booth keep`
verb both already existed; only the button was missing. Adds ★ to the ephemeral
card, mirroring × on the other shoulder.

BLUR. Per-item cosmetic censoring: `booth blur <name> <file>...`, a ◌/◉ toggle
in each caption row, and 👁 click-to-reveal. State is `.blurred` in the booth
dir, one booth-relative path per line -- the same filesystem-is-the-state idiom
as .pins and .forever. An empty set deletes the marker rather than leaving a
zero-byte file, so `ls -a` tells the truth.

⚠ BLUR IS NOT ACCESS CONTROL, and the code, the docs and a test all say so on
purpose. A blurred item is still served at its own URL, still in the zip, still
on disk. The Booth has no auth by design. test_blur_is_cosmetic_the_file_is_
still_served asserts the 200 deliberately: if someone later "hardens" this into
a 403 that test fails, and it should, because half-implemented access control is
more dangerous than none.

Reveal is per-viewer and never persisted; a reload re-hides. With JS off an item
stays blurred, which is the safe direction to fail in.

Two things the first pass got wrong, both caught by checking rather than
assuming:

  * The cover thumb. index.html has IDENTICAL markup in the kept and ephemeral
    lanes, so a single-occurrence replace patched only the kept one and the
    ephemeral front page happily displayed the thing someone had hidden. The
    test that caught it was itself wrong first -- it matched the bare string
    "blurred-thumb", which is in base.html's stylesheet on every page and so
    passed in both states. It now asserts the attribute.
  * Inline docs render through their own <figure> branch and were left
    unblurred -- the branch that puts readable text straight on the page, so it
    needed blur more than images do. The suite passed; a live curl caught it.

165 tests pass (154 pre-existing, unchanged).
This commit is contained in:
vh
2026-09-19 23:47:18 -07:00
parent 8305145ce1
commit 2e7fd7157e
7 changed files with 365 additions and 6 deletions
+35
View File
@@ -39,6 +39,41 @@ rsync -a ./out/ nh3-dev:booth-data/my-run/
Then hand the operator `http://10.100.10.50:8090/b/my-run/`.
## Blurring an item (cosmetic censoring)
⚠⚠ **Blur is NOT access control.** A blurred item is still served at its own
URL, still included in the zip, still on disk. It hides a thing from a *glance*
— a shoulder, a screen-share, a scroll past something you did not want
full-size — and nothing else. The Booth has no auth by design: **if a thing must
not be SEEN by whoever can reach port 8090, it must not be in a booth.**
```bash
booth blur <name> <file>... # hide from a glance
booth unblur <name> <file>...
```
Or the ◌ / ◉ toggle in each item's caption row on the booth page.
- **State** is `.blurred` in the booth dir — one booth-relative item path per
line, the same filesystem-is-the-state idiom as `.pins` and `.forever`. An
empty set deletes the file rather than leaving a zero-byte one, so `ls -a`
tells the truth about whether anything here is blurred.
- **Reveal is per-viewer and never persisted.** Click 👁 reveal; a reload
re-hides. With JS off it stays blurred, which is the safe direction to fail.
- **Covers inherit it.** If a booth's cover image is blurred, the index card's
thumb is blurred too — otherwise the front page undoes the censoring.
- **Inline docs are blurred too**, not just images and video. That branch puts
readable text straight on the page, so it needs this more than a picture does.
## Keeping a booth (round trip, both directions)
`★` on an ephemeral card promotes it to the kept lane; `release` in the kept
lane sends it back. Equivalent CLI: `booth keep <name>` / `booth unkeep <name>`.
⚠ Until 2026-09-19 the UI only went one way — the kept lane could release, but
an ephemeral booth could only be kept from a shell. The `/keep` route and the
CLI verb both already existed; only the button was missing.
## Kept boards — the one exception to the 24h rule
A booth containing a **`.forever`** dotfile is **never swept**, and renders in