feat(booth): pin/favorite, multi-select delete, newest-first link board
The standing link board grew from a flat oldest-first list with a per-row × into a manageable board: newest links lead, favorites stay on top, and several dead links can go in one pass. - Ordering: order_for_display() renders pinned rows first, then newest-first within each group (the board is an append log, so newest = most recently posted — the row you usually came to grab). - Pin/favorite: a per-row ★ toggles pinned state via POST /b/<name>/pin. State lives in a .pins sidecar dotfile (one content id per line), NOT inline in links.md — so links.md stays a pure atomic-append log (many sessions post concurrently) and a row's content id never changes just because it was pinned. remove_link_entry drops a removed row's pin; orphaned pins are inert (renderer only stars a live id). - Multi-select delete: checkboxes feed POST /b/<name>/unlink-many (repeated 'sel' content ids), with a select-all box and a live count. The per-row × stays for single removal. - One <form> with formaction buttons, so checkboxes, ×, ★, and bulk delete coexist without nested forms AND all work with JS off; JS only adds select-all and the live count. Per-row × confirm reads desc/url from data-* attrs, so an arbitrary posted description can't break into the JS. - Every action is keyed by content id, never row position — same race-safety the existing × has, extended to the bulk path. - Fixed pre-existing undefined --fg/--bg CSS refs in the board styles. Tests: +19 (pins round-trip, ordering, orphan-inert, remove-unpins, /pin and /unlink-many endpoints, board render + order). Full suite 102 passing. Deployed to nh3-dev booth.service; verified live (newest-first, pin round-trip, bulk delete) against the real 31-row board with no data loss.
This commit is contained in:
@@ -140,6 +140,8 @@ to a safe basename (no path traversal).
|
||||
| `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) |
|
||||
| `POST /b/<name>/unlink` | Remove ONE row from a link board (form field `entry` = content id) |
|
||||
| `POST /b/<name>/unlink-many` | Remove SEVERAL rows — the multi-select delete (repeated form field `sel` = content ids) |
|
||||
| `POST /b/<name>/pin` | Toggle a row's pinned/favorite state (form field `entry` = content id) |
|
||||
| `DELETE /b/<name>` | Wipe a booth (curl/API) |
|
||||
| `GET /healthz` | `{ok, ttl_hours, booths}` — Homepage siteMonitor target |
|
||||
|
||||
@@ -155,6 +157,19 @@ without taking the other thirty with it.
|
||||
It renders as real UI, not a markdown blob: each row shows the description,
|
||||
URL and provenance (who posted it, when), with a copy button and a per-row ×.
|
||||
|
||||
**Order: pinned first, then newest on top.** The board is an append log, so the
|
||||
most recently posted link leads — the one you almost certainly came to grab.
|
||||
Rows you want to keep in view regardless of churn get the **★** (pin/favorite),
|
||||
which floats them to a group at the very top; click it again to unpin. The
|
||||
header shows `N pinned` when any are.
|
||||
|
||||
**Multi-select delete.** Tick the checkbox on any set of rows and hit
|
||||
**🗑 delete** to remove them all in one go (with a count confirmation). The
|
||||
select-all box in the header toggles the lot. The per-row × is still there for
|
||||
a single quick removal. Everything — checkboxes, ×, ★, bulk delete — works with
|
||||
JavaScript off (plain form POSTs via `formaction`); JS only adds select-all and
|
||||
the live count.
|
||||
|
||||
```bash
|
||||
booth links # row number, entry id, raw row
|
||||
booth unlink 3 # by row number
|
||||
@@ -165,15 +180,24 @@ booth unlink 8b40e0a5 # by entry id — what the × posts
|
||||
append-only and multi-writer: another session can post between the moment you
|
||||
list it and the moment you remove a row, so an index would delete a neighbour.
|
||||
An id either matches the row you saw or matches nothing. A row number typed at
|
||||
the CLI is resolved to its id *before* anything is deleted.
|
||||
the CLI is resolved to its id *before* anything is deleted. The multi-select
|
||||
delete (`/unlink-many`) carries the same guarantee per selected id.
|
||||
|
||||
An id is exactly 8 hex characters, which is how the CLI tells ids from row
|
||||
numbers — roughly one id in forty is all digits, so "is it numeric" is not a
|
||||
safe test.
|
||||
|
||||
Appends (`booth link`) and prunes (`booth unlink`, the ×) take the same
|
||||
`flock` on `.links.lock`, so a post cannot be lost inside a prune's
|
||||
read-modify-write window.
|
||||
**Pin state lives in a `.pins` sidecar** (one content id per line), never inline
|
||||
in `links.md`. That keeps `links.md` a pure append log — `booth link` stays a
|
||||
single atomic write, which is what lets many sessions post concurrently — and
|
||||
means pinning a row never changes its content id. A pin whose row is later
|
||||
removed is dropped automatically; a pin orphaned by a hand-edit is inert (the
|
||||
renderer only stars a row a live id still matches). Pins are a UI action; there
|
||||
is no `booth pin` CLI yet.
|
||||
|
||||
Appends (`booth link`) and prunes (`booth unlink`, `unlink-many`, the ×) take
|
||||
the same `flock` on `.links.lock`, and pin toggles take it too, so a post cannot
|
||||
be lost inside a prune's or a toggle's read-modify-write window.
|
||||
|
||||
### Deleting a kept board
|
||||
|
||||
|
||||
Reference in New Issue
Block a user