• feat(booth): pin/favorite, multi-select delete, newest-first link board

    vh released this 2026-09-06 02:29:22 -07:00 | 769 commits to main since this release

    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//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//unlink-many (repeated
      'sel' content ids), with a select-all box and a live count. The per-row ×
      stays for single removal.
    • One 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.

    Downloads