feat(trips): owner publish/unpublish toggle on /trips listing

Add an owner-only publish switch to each /trips card. It POSTs to a new
entry-actions route that mutates trip.md `published` and invalidates the
page-tree cache so the listing, nav and home reflect the change on the
next load. Owner sees drafts (Draft badge); anon/non-owner unchanged.

- U1 EntryScopeGuard::resolveTripChild — resolve a slug to a direct child
  of /trips (drafts included, for republish)
- U2 POST /api/v1/trip/{slug}/publish (setTripPublished) — owner-gated
  write, strict is_bool body, header-mutation save, audit log
- U3 trip-publish-toggle partial + CSS (role=switch, Draft badge, visible
  failure toast, ≥44px target)
- U4 owner-aware /trips listing + card restructure (toggle overlays cover
  as a non-anchor sibling; works for coverless drafts)
- U5 home active-trip branch falls back when the active trip is unpublished
- U6 trip-publish.js (confirm/pending/optimistic/revert) + esbuild wiring

Cache note: an in-place frontmatter edit keeps the folder-check cache id,
and driver:auto uses APCu in web memory, so deleteAll()+invalidateCache()
is insufficient — the endpoint also calls apcu_clear_cache().

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
This commit is contained in:
2026-07-08 14:50:05 +02:00
co-authored by Claude Opus 4.8
parent 55da834396
commit 064f0f0c52
10 changed files with 457 additions and 3 deletions
@@ -0,0 +1,27 @@
{#
Owner-only publish/unpublish switch for a /trips listing card (U3, R10/R11).
Rendered by trips.html.twig as a SIBLING of the navigating card <a> (never a
child of it), overlaid top-right on the cover, so a click on it toggles publish
state rather than following the card link (KTD6). trip-publish.js binds the
button by `.trip-publish-toggle` and drives it from the data-* below.
Params:
trip Page — the trip whose published state this controls
is_active bool — true when this trip is site.active_trip; drives the
"home page loses it" confirm in the JS (R12)
#}
{% set is_active = is_active ?? false %}
<div class="trip-publish-overlay">
<span class="trip-draft-badge"{% if trip.published %} hidden{% endif %}>Draft</span>
<button type="button"
class="trip-publish-toggle"
role="switch"
aria-checked="{{ trip.published ? 'true' : 'false' }}"
aria-label="Published — {{ trip.title }}"
data-trip-slug="{{ trip.slug }}"
data-published="{{ trip.published ? 'true' : 'false' }}"
data-active="{{ is_active ? 'true' : 'false' }}">
<span class="trip-publish-track" aria-hidden="true"><span class="trip-publish-knob"></span></span>
</button>
</div>