Phase 5h slice 4 — adds the star button on each tile that flips
Pinned on the projax item via POST /dashboard/pin.
Backend:
- store.SetPinned(ids, pinned bool) — minimal-write helper that
mirrors SetPublic, only touching the pinned column.
- web/dashboard_pin.go — handleDashboardPin parses id + pin from
form, calls SetPinned, invalidates the entire dashboard cache (pin
affects sort order across every view/scope/filter combo), then
re-renders by delegating to handleDashboard so HTMX receives the
updated #dashboard-section HTML.
- Route: POST /dashboard/pin (sibling of /dashboard/task/*).
Frontend:
- Tile template now leads with a <form class="tile-pin-form"> that
POSTs id + the inverted pin state. Button glyph is ☆ when unpinned,
★ when pinned; aria-label flips accordingly.
- HTMX swaps the entire #dashboard-section so the tile moves to the
pinned-first position (or back to alphabetical) without a full reload.
- CSS: .tile-pin (transparent button, muted color, accent on hover);
.tile-pin.pinned for the filled-star state.
Test helper: server_test.go gains a post() helper paired with the
existing get() — form-encoded POSTs for writeback tests.
Tests (dashboard_pin_test.go):
- TestDashboardPinTogglesItem — POST pin=true flips the row, and the
re-render shows the .tile-pinned class on the tile <article>.
- TestDashboardPinUnpinsItem — POST pin=false on a pinned row unpins.
- TestDashboardPinRequiresID — missing id returns 400.
- TestDashboardPinInvalidatesCache — primes with unpinned cache,
POSTs pin, asserts the next GET reflects the pinned class (proving
the prior cache entry was busted).