feat(booth): kept boards can be deleted from the UI; document the TTL-reset trap

Kept boards had no delete path in the UI at all. The kept lane deliberately
omits the wipe control -- a one-click wipe next to the durable stuff is a
footgun -- but "deliberate" had been implemented as "impossible": the only
routes out were ssh or a hand-written API call.

Now it is two deliberate acts. A `release` control on kept cards drops the
sentinel, the board moves to the ephemeral lane, and the existing x wipes it
from there. Release is reversible -- POST /b/<name>/keep pins it again.

  POST /b/<name>/unkeep   release the pin
  POST /b/<name>/keep     pin it (round-trip, so release is not a one-way door)

FOUND WHILE TESTING, and it invalidates the previously-documented workaround:
removing the sentinel BUMPS the booth directory's mtime, and booth age is the
newest mtime in the tree -- so a released board's clock RESETS from 10,000s to
0s and it survives another full TTL. The old comment said "remove the sentinel
first (it rejoins the sweep)", which is true but means the board lives another
24h, not that it gets reaped. Unkeep-and-wait is a delay, not a delete.

test_releasing_a_board_RESETS_its_ttl_clock pins that behaviour deliberately so
nobody re-derives the workaround. Release is what unlocks the x; the x is what
deletes.

CLI: `booth rm` already worked on kept boards but said nothing about it. It now
announces "(was KEPT -- durable board)" so wiping something durable can never
look identical to wiping run output. Not a block -- a CLI user naming a booth
is being explicit.

5 new tests (67 pass). Verified live on nh3-dev: release renders on all four
kept boards, the ephemeral lane keeps its x, and the links board is untouched
with its sentinel intact.
This commit is contained in:
vh
2026-08-23 10:42:32 -07:00
parent 299b52458b
commit d88f235688
6 changed files with 187 additions and 5 deletions
+24 -1
View File
@@ -48,7 +48,8 @@ still ephemeral, so nobody inherits a cleanup chore they didn't ask for.
```bash
booth keep my-board # drop the sentinel — exempt from the sweep, forever
booth unkeep my-board # remove it — the board rejoins the sweep
booth unkeep my-board # release the pin — the board rejoins the sweep
booth rm my-board # delete it NOW (works on kept boards; says so when it was kept)
```
It is just a file, so the manual forms work identically and are the honest
@@ -132,9 +133,31 @@ to a safe basename (no path traversal).
| `GET /b/<name>/<file>` | Serve a file out of the booth |
| `POST /upload` | Upload files → new pickup booth; 303-redirects to `/b/<id>/` (id in `Location`) |
| `POST /b/<name>/delete` | Wipe a booth (the UI's "Wipe now" button) |
| `POST /b/<name>/keep` | Pin a booth — exempt from the sweep |
| `POST /b/<name>/unkeep` | Release the pin (the UI's "release" button on kept cards) |
| `DELETE /b/<name>` | Wipe a booth (curl/API) |
| `GET /healthz` | `{ok, ttl_hours, booths}` — Homepage siteMonitor target |
### Deleting a kept board
Kept boards have no × in the UI on purpose — a one-click wipe next to the
durable stuff is a footgun. But *deliberate* must not mean *impossible*, which
is what it meant until 2026-08-23: the only routes out were ssh or a
hand-written API call.
Now it is two deliberate steps. **Release** on the kept card drops the
sentinel and the board moves to the ephemeral lane, where the × already lives;
wipe it from there. Release is reversible — press keep again and nothing was
lost. From the CLI, `booth rm <name>` deletes a kept board immediately and
tells you it was kept.
**Do not "unkeep and let it expire."** Removing the sentinel *bumps the booth
directory's mtime*, and a booth's age is the newest mtime in its tree — so a
released board's clock **resets** and it survives another full TTL.
Unkeep-and-wait is a 24-hour delay, not a delete. Use the × or `booth rm` when
you mean now.
## Ops
Runs as a **user-level** systemd service on nh3-dev (no root, no Docker),