Commit Graph
100 Commits
Author SHA1 Message Date
m038andClaude Opus 5 a5993b2091 fix(review): strict coordinate parse, close the gate bypass, drop the stale pin
Code-review fixes to the location-override panel.

parseFloat is a prefix parser, so '48abc', '48,85' (comma-decimal paste) and
'35.0116S' (hemisphere suffix, silently flipped to the wrong side of the
equator) all passed the isFinite + range check, cleared the mismatch flag and
were POSTed verbatim — the same silent-corruption class this feature exists to
end. Replaced with a whole-value decimal check, and the valid branch now
normalises to the 6dp the GPS handler and onDragEnd already write, so the pin
and the submitted value cannot disagree.

The submit gate keyed on the .location-field--mismatch class, which was only
ever set by syncFields() on blur / debounced input / toggle-open-success / GPS
— never at init. Three paths therefore reached the server unflagged: a draft
restored by initDraft() (which runs before initLocationDetails() and assigns
.value directly, firing no events), an edit-mode async prefill, and an open
panel whose maplibre chunk failed to load. syncFields() now runs once at the
end of initLocationDetails(), in the toggle handler's catch arm, and after the
edit prefill writes lat/lng.

Blanking both fields left the marker behind, presenting a stale coordinate as
if it were still the entry's — added clearPin() to the map handle and call it.
Removed hasPin(), which had no caller. Corrected two comments that named the
wrong stylesheet and claimed .field-invalid reuse the code does not do.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-24 21:34:17 +02:00
m038andClaude Sonnet 5 13c76b29a8 fix(post): close location-map race, block submit on bad coords
Code review (4 independent reviewers) converged on the same bug: the
maplibre-gl singleton cached its handle only after import() resolved,
so a fast close/reopen of the "More location details" panel could
race two Map instances onto one container. Cache the in-flight promise
synchronously instead, and propagate/handle import rejection so a
failed map load surfaces a hint instead of hanging silently.

Also closes a submit-time hole the adversarial pass found: the
mismatch flag on lat/lng was purely cosmetic and never blocked
form submission, so out-of-range coordinates could be saved. The
flag now gates submit like the other required fields, and clears
itself when both fields are blanked back out instead of sticking.

The geocode fetch gets a 10s timeout via AbortController so a hung
response can't leave the lookup button disabled forever.

Also moves the location-details CSS out of the site-wide style.css
into post-form's own code-split stylesheet, since none of it is used
outside the post form.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 20:03:01 +02:00
m038 797b472a60 feat(post): add "More location details" panel — search, map, sync
Closed-by-default disclosure below City/Country holding a city/country
geocode lookup (Open-Meteo, city-only query with client-side country
ranking), a single-marker draggable MapLibre preview (lazy-loaded via
location-map.js), and the relocated lat/lng fields. GPS button, search
pick, pin drag, and typed values all stay in sync via syncPinFromFields(),
with a visual mismatch flag for unparseable typed input.

Built entirely in JS (mirrors the existing 'More options' disclosure
pattern) so post-form.html.twig needs no template change.
2026-07-24 19:38:24 +02:00
m038 52e9fbadf2 refactor(map): extract MAP_STYLE into a shared map-style.js module
Single source of truth for the MapLibre style URL, shared between
maplibre-utils.js (multi-marker/GPX maps) and the new location-map.js
preview module — no behavior change.
2026-07-24 19:38:10 +02:00
m038 a00690fbde feat(post): unhide lat/lng fields, style the location-details panel
Removes the CSS rule hiding data[lat]/data[lng] and adds styling for the
new 'More location details' disclosure, search results list, map preview
container, and mismatch-flag state.
2026-07-24 19:38:05 +02:00
m038andClaude Opus 4.8 02fa4e94a7 content(theme): rename nav/heading/CTA 'Past Trips' to 'Trips'
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 23:29:05 +02:00
m038andClaude Opus 4.8 df0e00937e content(trip): add taglines for all trips; refine Denmark tagline
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 23:22:11 +02:00
m038 fae364585c Merge branch 'main' of https://git.gorinskat.nl/m038/intotheeast-com-content 2026-07-08 21:18:45 +00:00
m038 3cb50a298a (Grav GitSync) Automatic Commit from into-the-east-grav-sync 2026-07-08 21:18:44 +00:00
m038andClaude Opus 4.8 125f9e6131 content(trip): add Denmark 2026 description
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 23:18:32 +02:00
m038 4a40dc9c18 (Grav GitSync) Automatic Commit from into-the-east-grav-sync 2026-07-08 20:55:32 +00:00
m038andClaude Opus 4.8 f4ab73070b content: feature 3 entries as homepage highlights; publish Denmark 2026
- featured: true on Central Asia 2023, Italy 2025, and US/Canada/Mexico 2024 entries
  (surface in the between-trips homepage highlights grid)
- Denmark 2026 trip: published: true + Vestkystruten GPX track

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-08 22:28:18 +02:00
m038 543e8e3dce Merge feat/journal-post-form: trip publish/unpublish toggle + review fixes
Owner publish/unpublish toggle on the /trips listing (owner-gated API endpoint
mutating trip.md published state with APCu-aware cache invalidation, owner-
visible drafts, home fallback), plus the code-review follow-ups: shared scope-
guard helper, guarded cache-flush, shared api-utils (apiSend/apiErrorMsg with
opt-in timeout), template-asserted publish target, active-trip one-liner, and
the --color-draft-accent token.

Merges cleanly with the denmark-2026 cover content on main (disjoint files).
2026-07-08 17:47:27 +02:00
m038andClaude Opus 4.8 37b669424b fix(trips): remaining publish-toggle review findings
- resolveTripChild now asserts the resolved page uses the trip template, so a
  non-trip direct child of /trips could never be toggled through this endpoint
  (P3 adversarial).
- apiSend gains an optional timeoutMs (AbortController); trip-publish passes 10s
  so a hung toggle can't leave the switch stuck aria-busy. post-form omits it,
  keeping media uploads unbounded (P2 reliability).
- trips.html.twig reuses trip.html.twig's one-line active-trip slug match
  instead of a bespoke 3-branch OR (P2 maintainability).
- Draft-badge amber is now a --color-draft-accent token shared by the trip and
  journal badges instead of a twice-hardcoded #E0A458 literal (P3).

Rebuilt js/trip-publish.js and js/post/post-form.js (shared api-utils change).
PHP lint clean; trip-publish suite 10/10; post suite unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-08 17:31:40 +02:00
m038andClaude Opus 4.8 25cee53718 refactor(theme): share apiSend/apiErrorMsg between post-form and trip-publish
Address the P1 maintainability finding that trip-publish.js reimplemented
post-form.js's apiSend + login-expired error copy verbatim. Extract both into
js/src/api-utils.js and import from both entry points; esbuild inlines the
module into each bundle so there is no runtime coupling. Also drops the stale
data-trip-route reference from trip-publish.js's markup-contract comment.

Rebuilt js/trip-publish.js and js/post/post-form.js.

Verified: trip-publish suite 8/8; post suite unchanged (34 pass, same 6
owner-gate environmental fails as baseline — photo-editor specs that exercise
post-form's apiSend/apiErrorMsg all green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-08 17:14:00 +02:00
m038andClaude Opus 4.8 5b4e31678e refactor(entry-actions): dedup scope guard + guard publish cache-flush
Address code-review findings on the trip publish/unpublish toggle:

- Extract EntryScopeGuard::resolveChildOf() so resolveActiveDailyChild and
  resolveTripChild share one find() + parent-route-assert body instead of two
  copies that could drift (P1 maintainability).
- Wrap setTripPublished's post-save cache invalidation in try/catch. save() has
  already persisted the published flag to disk, so a flush failure now logs a
  loud reconciliation warning (and still returns success + the audit line)
  rather than bubbling to a bare 500 that reads as "nothing happened"
  (P2 reliability / adversarial).

Behavior-preserving; PHP lint clean; trip-publish Playwright suite 8/8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-08 17:06:17 +02:00
m038andClaude Opus 4.8 064f0f0c52 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
2026-07-08 14:50:05 +02:00
m038andClaude Opus 4.8 4cc0a18aaf content(denmark-2026): add trip cover image
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
2026-07-08 12:59:32 +02:00
m038 087e05affa (Grav GitSync) Automatic Commit from into-the-east-grav-sync 2026-07-08 10:51:37 +00:00
m038andClaude Opus 4.8 55da834396 feat(gpx-manager): list unpublished trips too, newest-first
Owner-only page, so drop the .published() filter and order by header.date_start
desc so the current/next trip is on top.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-08 10:29:05 +02:00
m038andClaude Opus 4.8 c46bbd952f fix(entry-actions): invalidate page-tree index on delete
deleteEntry did cache.deleteAll() but not Cache::invalidateCache(), so under
cache.check.method: folder the deleted entry lingered in the pages index and the
feed re-rendered it (image-less) on reload. Mirror the create-path fix. See
docs/solutions/integration-issues/grav-deleteall-doesnt-invalidate-page-tree-index.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-08 10:29:05 +02:00
m038 1dc9c9854c Merge commit 'b3b4f77'
# Conflicts:
#	themes/intotheeast/templates/partials/trip-feed-col.html.twig
#	themes/intotheeast/templates/trip.html.twig
2026-07-08 00:08:37 +02:00
m038andClaude Opus 4.8 d4fcbecaf1 content(italy-2025): rename to Tuscany Gravel 2025, fix dates + stories template
Trip title Cycling Tuscany 2025 -> Tuscany Gravel 2025, dates Oct->Sep,
and stories container uses the stories template (was default).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-08 00:03:58 +02:00
m038andClaude Opus 4.8 b3b4f774d0 feat(nav): add New Post + GPX Manager links for logged-in users
Both links render only when grav.user.authenticated; anonymous nav
(Home, Past Trips) is unchanged. Active-page highlight via aria-current.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-07 23:44:20 +02:00
m038andClaude Opus 4.8 034db29e66 fix(post-form): commit photo thumbnail grid + overlay styling
Renders FilePond items as a 3-up square-thumbnail grid on create+edit,
disables native image-drag so FilePond owns reorder, and hides the
file-info overlay on already-uploaded (idle) items. This is the styling
QA'd and approved on the worktree dev server; it was previously left
uncommitted (mis-tagged as unrelated WIP in earlier handovers).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-07 23:44:20 +02:00
m038andClaude Opus 4.8 536ca2085f fix(trip-cover): crop-to-fill cover strip + all-or-nothing retina
Banners/cards used cropResize (fit-inside), so a portrait fallback
source was handed back as a narrow sliver that object-fit:cover then
upscaled into a blur (reported on us-canada-mex-2024). Switch to
cropZoom (crop-to-fill) so the derivative is a real w×h cover strip.

Emit the 2x srcset descriptor only when the source is genuinely >=2w
wide (cover.width >= 2w), else 1x-only — no upscaling, no odd
intermediate widths. Imported pixelfed photos cap at ~1440px wide, so
auto-picked covers are usually 1x-only (see backlog: full-res reimport).

Also drop the no-photos-demo test fixture — it surfaced as stray demo
content in the trip list; AE4 (no cover + no images -> no banner) is a
trivial else-branch of the shared macro's cover guard, covered by
construction alongside the R7/AE3 fallback tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
2026-07-07 23:39:04 +02:00
m038andClaude Opus 4.8 21f0c8fdd5 test(fixture): add no-photos-demo trip for AE4 coverage
A minimal demo trip with no cover_image and a single published journal entry
that has no images — the fixture the trip-page banner AE4 assertion needs
(macro fallback loop finds an entry but no image -> null cover -> no banner
strip). Lives under docs/demo/trips/ like italy-2026-demo, so it is loaded into
the pages tree only for tests and never reaches live content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
2026-07-07 09:13:48 +02:00
m038andClaude Opus 4.8 ac88e8c003 fix(trip-cover): restrict cover picker to images + guard non-image selection
The header.cover_image pagemediaselect field had no accept filter, so the
Admin media picker listed every file in the trip page folder — including the
GPX files placed there by the GPX manager. On a typical trip page (photos live
on the journal entries, not the trip page) the picker offered *only* GPX, and
selecting one routed a non-image Medium into cropResize, rendering a broken
<img> on both the trips list and the trip banner.

- Blueprint: add `accept: ['.jpg','.jpeg','.png','.webp','.gif']` so the picker
  only offers images (prevention at source).
- Macro: resolve cover_image against `media.images` instead of all media, so a
  non-image or unresolvable selection falls through to the entry-photo
  auto-pick (defence-in-depth; also hardens the R11 fallback).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
2026-07-07 08:35:32 +02:00
m038andClaude Opus 4.8 7f6bf9e3f3 refactor(post-form): derive disclosure default from rendered markup
initDisclosure hardcoded blueprint defaults into a field-name regex
(/\[published\]$/) to decide which toggle state counts as a deviation worth
auto-expanding "More options". Read each toggle's default from the HTML
`checked` attribute instead — Grav's toggle template stamps it on the default
option, and prefill/edit only ever set the live `.checked` property — so a
future default-ON advanced toggle Just Works. Rebuilt bundle.

Code review F4 (maintainability).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-07 08:31:59 +02:00
m038andClaude Opus 4.8 8db3ffeafc fix(cache-on-save): latch page-cache invalidation to once per submit
onFormProcessed fires once per process action (add_page/upload/message/
reset), so the deleteAll() + Cache::invalidateCache() pair ran 4x per post.
Gate it behind a $cacheInvalidated latch (same pattern as $photosReconciled)
so the store wipe + system.yaml touch happen exactly once, and log the step.

Code review F1 (perf) + F7 (observability).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-07 08:31:50 +02:00
m038andClaude Opus 4.8 13656e311a test(demo): give demo trip a tagline + description fixture
The italy-2026-demo trip is the UI-test fixture; add a tagline and a
multi-paragraph markdown description so the trip one-liner, expandable
description, and banner specs run against real content.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
2026-07-06 00:02:25 +02:00
m038andClaude Opus 4.8 ad72739b99 feat(trip): one-liner, description & retina cover on trip list + page
Give trips an optional one-liner (header.tagline) and description (markdown
content), surface them where they help, and fix the soft cover image — all
editable from admin.

- U1: header.cover_image blueprint field → pagemediaselect media picker.
- U2: new macros/cover.html.twig — single source for cover resolution
  (author-selected → first journal image → none; missing file falls back)
  and retina rendering (1x/2x cropResize + srcset). Merged resolve+render
  into one macro since Twig macros can't return a Medium object.
- U3: trip-list card renders the one-liner (when set) and the retina cover.
- U4: trip-page in-column header gains the one-liner, an expandable
  description, and a thin banner strip — gated behind a trip_header_extras
  partial flag (default off) so the shared home active-trip view is
  unchanged (R12/KTD4).
- U5: styles for the card/header one-liner, collapsible description
  (max-height preview, not line-clamp, so it holds across paragraphs) and
  the banner, with a mobile banner-height reduction.

Covers R1–R15. Verified with new Playwright specs + full trip/home/maps
regression against an isolated worktree dev server.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
2026-07-06 00:02:19 +02:00
m038andClaude Opus 4.8 a7bda6ed39 fix(post-form): only auto-expand More options when a field deviates from default
initDisclosure auto-opened the More-options panel whenever any advanced field
'had a value', but the published toggle defaults ON, so a plain create form
tripped it every load. A toggle now counts only when it deviates from its
blueprint default (published: OFF is notable; force_connect/featured: ON is),
so the panel stays collapsed on create. Edit mode still force-opens it
separately. Rebuilt bundle via make build-assets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-05 23:42:31 +02:00
m038andClaude Opus 4.8 7775a4ed6d fix(cache-on-save): invalidate page index on save so new entries are editable
deleteAll() only clears the Doctrine store; the page-tree index is keyed on
md5(dirs + folderHash + config->checksum() + lang) (Pages::buildRegularPages).
With cache.check.method:folder a freshly-created entry could survive in that
index and stay invisible to GET /api/v1/pages{route} (404), so opening a
just-posted entry for editing showed "this entry no longer exists" ~2/3 of the
time. Add Cache::invalidateCache() (touches system.yaml -> bumps config
checksum) so the index key changes and the tree rebuilds next request. Chosen
over clearCache('standard'), which would nuke compiled Twig + assets on every
post. Fixes the create->edit round-trip (Playwright ES1) and the DEL1 flake.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
2026-07-05 23:42:11 +02:00
m038andClaude Opus 4.8 7ffd75e8ec fix(review): surface auth-expiry, harden add-batch rollback, add audit log
Follow-up to the ce-code-review deferred items on the photo editor:

- Photo editor fetches now REJECT with a status-bearing error (apiSend)
  instead of the boolean apiOk that swallowed the HTTP code. Reorder,
  delete and add paths tell a lapsed login (401/403) apart from a generic
  failure and prompt the owner to sign in again rather than "try again".
- Add-batch rollback: the per-file cleanup DELETEs no longer swallow
  individual failures. If any rollback DELETE doesn't land (a stray
  stock-named file could steal the lexicographic cover slot), the owner is
  told cleanup was incomplete and to reload — instead of a false "rolled
  back cleanly".
- entry-actions delete + reorder routes now emit an owner-attributed audit
  log line, so a destructive mutation is traceable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:43:41 +02:00
m038andClaude Opus 4.8 361a6b4d67 fix(review): harden photo reorder against data loss + failure-path drift
Addresses ce-code-review findings on the photo-editor media-API work:

- P0 (#1): PhotoRenumberer now renumbers EVERY on-disk image, using the
  client manifest only as preferred ORDER and appending any omitted image
  at the end. A stale/incomplete `order` (e.g. a second browser tab)
  previously left an unlisted photo at a target slot for phase-2's
  rename() to silently overwrite — verified data loss, now impossible.
  The reorder route inherits the guard; create/reconcile is unchanged.
- P2 (#3): unique per-call token in the .reorder-tmp-* name so two
  concurrent renumbers on one folder can't collide and clobber bytes.
- P3 (#7): de-duplicate the manifest so a repeated name can't shift/drop
  a photo.
- P2 (#2): applyReorder + doDelete split the two failure stages — a failed
  refresh AFTER a committed reorder/delete no longer reverts to a stale or
  ghost state, it reconciles to disk. A DELETE 404 is treated as success
  so a retried ghost cell converges.
- P2 (#4): both custom routes call requirePermission('api.pages.write')
  so the GHSA-x7hm API-key scope cap applies (owner already holds it, so
  the owner-only behaviour is unchanged).
- P3 (#8): refresh stale comments (photo-01..NN; drop editLoadPhotos ref).

PhotoRenumberer's 7-case unit suite still passes and the data-loss repro
now preserves all bytes. Assets rebuilt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:14:41 +02:00
m038 b317640dd0 (Grav GitSync) Automatic Commit from into-the-east-grav-sync 2026-07-05 18:12:16 +00:00
m038 9bcb0fe4db Back to told name 2026-07-05 20:08:35 +02:00
m038 b5f169f25f (Grav GitSync) Automatic Commit from into-the-east-grav-sync 2026-07-05 18:04:56 +00:00
m038andClaude Opus 4.8 a4432d897e feat(post-form): live photo editor on entry edit (media API + SortableJS)
Replace the FilePond photo path in edit mode with our own thumbnail grid that
talks straight to the media API (the gpx-manager pattern). Add/delete/reorder
each persist immediately, decoupled from the form's text-field Save:

- Add: HEIC->JPEG client-side, stock POST .../media per file, then ONE reorder
  after the batch (renumber photo-01..NN). On a failed reorder: auto-retry
  (idempotent), else roll the just-uploaded files back so no orphan stock-named
  image breaks cover=first. Upload progress shown per file.
- Delete: inline 'Delete? [Confirm] [Cancel]' (Confirm disabled in flight),
  stock DELETE, then renumber the survivors.
- Reorder: SortableJS drag -> POST /entry/<slug>/photos/order. On failure the
  move reverts to last-known-good; the shown grid never disagrees with disk
  without an inline error.
- Loading + empty states; first cell badged Cover; photo-NN URLs cache-busted
  since reorder reuses them for different bytes.

FilePond is fully decommissioned in edit mode (initPhotoConversion early-returns
under EDIT_MODE): no stale photo_order manifest is posted on text Save, so
cache-on-save can't delete a live-added photo. Create-mode FilePond is untouched.

Adds sortablejs (bundled into js/post via the post-form entry). SVG excluded in
the file-input accept; the server-side SVG block is a documented fast-follow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 19:19:57 +02:00
m038andClaude Opus 4.8 4fea522d5c feat(entry-actions): add owner-scoped photo reorder route
POST /api/v1/entry/{slug}/photos/order renames an entry's image files to
photo-01..NN in the client-supplied order so the feed cover (media.images|first)
follows the drag — no stock endpoint can express this. Same R6 guard chain as the
delete route (site OWNER + direct child of the active trip's dailies), then the
shared PhotoRenumberer does the two-phase rename and the cache is cleared.

Filename safety is layered: unsafe 'order' entries (/, ..) are dropped here and
PhotoRenumberer only renames real image files, so a crafted body can never touch
the entry .md, a .gpx or a .meta.yaml. Registers behind the API route-map cache,
so a deploy cache-clear is required (same as the existing DELETE /entry/{slug}).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:58:43 +02:00
m038andClaude Opus 4.8 fcf52a0e44 refactor(photos): extract shared zero-padded PhotoRenumberer helper
Factor cache-on-save's renumberPhotos into a shared PhotoRenumberer class
(Grav\Plugin\Shared), the single owner of the photo-NN naming invariant used
by both the create/edit reconcile and the upcoming live reorder route, so their
numbering can't diverge.

Changes vs the old private method:
- Zero-pads to photo-01..NN (pad width grows with the set) so lexicographic
  media order equals numeric order past 9 photos — cover = images|first stays
  correct for 10+ photos. Normalises pre-existing un-padded photo-N on first pass.
- Image-extension guard moved into the helper: only real image files on disk are
  renamed, so a crafted manifest naming the entry .md, a .gpx or a .meta.yaml is
  skipped by every caller, not just cache-on-save.

Create-mode entries now also emit photo-01..NN — an intentional, accepted side
effect of sharing one helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 18:56:59 +02:00
m038andClaude Opus 4.8 32c3d8c065 content: add tagline to Denmark 2026 trip
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 16:52:15 +02:00
m038andClaude Opus 4.8 05db592836 fix(cache): revalidate pages/media instead of 7-day browser cache
system.pages.expires was 604800 (Grav default), so browsers cached feed HTML and
entry media for a week with no revalidation — deleted entries lingered as ghost
cards (images 404), and edited/new photos didn't appear until a hard refresh. Set
expires: 0 → Cache-Control: no-cache, must-revalidate. With etag already on, an
unchanged page returns a cheap 304; changed content shows immediately. Also fixes
reorder staleness (photo-N.jpg is reused across reorders at the same URL). The
server-side page cache (cache.enabled) is untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 14:55:05 +02:00
m038andClaude Opus 4.8 ff31683075 fix(post-form): render photo thumbnails on edit + clearer load errors
QA feedback fixes for the front-end journal edit form:

- Photos on edit showed a filename with no thumbnail. addFile(url,{type:'local'})
  routed through the form plugin's FilePond server.load, which returned HTML (not
  the image bytes), so image-preview had nothing to render. Fetch each image as a
  Blob and add it as a File (ordered) — the thumbnail renders, and type:'local'
  still means it is never re-uploaded and its filename rides the photo_order
  manifest. Verified: fileType image/jpeg, previews render, reorder/remove unchanged.

- Distinguish a deleted/missing entry (API 404 → "this entry no longer exists")
  from a transient load failure ("check your connection") in the prefill catch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 14:55:05 +02:00
m038andClaude Opus 4.8 a9ec49e164 content: scaffold Denmark 2026 trip (Vestkustryten, prep)
New trip page tree at /trips/denmark-2026 — trip.md (10-20 Jul 2026)
plus inert dailies/stories containers. Not set active; prep only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 14:50:14 +02:00
m038andClaude Opus 4.8 03323bcadf feat(post-form): enable adding photos when editing an entry (M2/R9)
Re-enable the FilePond browse/drop affordance in edit mode. On submit, existing
locals + new uploads ride the photo_order manifest and cache-on-save reconciles
the folder (delete dropped, renumber survivors photo-1..N, first = cover), so an
entry's photos can now be added, removed and reordered from the front-end edit
form. Verified end-to-end: add, remove, reorder, and all three in one save, plus
create-with-photos and edit remove/reorder regressions.

Depends on a local fix to add-page-by-form (its Grav-2.0 edit-mode header cast
fatals on a new upload); that plugin is git-ignored, so the fix ships as a
tracked patch in the superproject (deploy/patches/) rather than here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 13:42:55 +02:00
m038 cb759746d0 Merge remote-tracking branch 'origin/main' into feat/journal-post-form 2026-07-05 11:32:23 +02:00
m038andClaude Opus 4.8 525b684493 feat(post-form): M2 photo edit — load, remove & reorder from the edit form
Editing an entry now loads its existing photos into FilePond so the owner can
remove and reorder them; the first photo is the cover. Adding NEW photos on edit
is intentionally suppressed (see below).

post-form.js (U7):
- On ?edit=, load the entry's current images into FilePond as LOCAL items (via
  the session media API, gpx-manager pattern). They display for remove/reorder
  and ride the existing photo_order manifest on submit, but are never re-uploaded.
- Exclude the FilePond field from the D1 prefill disable-sweep — FilePond reads
  its input's disabled state at init and never re-enables, which had removed its
  controls in edit mode.
- Suppress the add affordance in edit mode (allowBrowse/allowDrop off): a new
  upload on edit hits add-page-by-form's Grav-2.0 edit-merge fatal
  ((array)$page->header() yields mangled protected keys → array_merge(null,…)).
  That plugin is stock/GPM/git-ignored (no fork), so adding photos on edit is
  deferred to the form-to-page/image-upload rework.

cache-on-save.php (U8):
- reconcilePhotos(): on edit, resolve the entry folder via the shared scope guard
  (not the fuzzy create-path finder), delete any image dropped from the manifest,
  then renumber survivors photo-1..N in the submitted order (cover = first).
- Run reconciliation ONCE per submit: onFormProcessed fires per process action
  (4×); a 2nd pass deleted the just-renamed photo-N files as "unlisted".
- Empty manifest reconciles nothing (fail-safe: never wipes photos on a missing
  photo_order).

Verified on the container: existing photos load (V9); remove + reorder persist to
disk with cover=first (V10); reconcile helpers covered by a reflection unit test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 11:25:01 +02:00
m038andClaude Opus 4.8 f8f03faa1d chore: gitignore stray tester.yaml test account
Prevents git add -A from sweeping a local browser-test account (with its
password hash) into the content repo, matching the existing testrunner.yaml
ignore.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FrCYNq6RXdGYbn5PFrhM
2026-07-05 01:11:53 +02:00
m038andClaude Opus 4.8 6e8eadbab5 security: stop tracking the per-host env/ secret tree
git-sync auto-commit stages everything not gitignored (git add -A over
user/), NOT just the pages/config/themes folders. Because /env/ was not
gitignored, prod's git-sync pushed the per-host env tree — including
api-private.php (JWT), security-private.php (CSRF salt), and the
git-sync token — to Gitea in commit 9337003. Gitignore /env/ and untrack
it so it can never round-trip again. (Working-tree files kept via
--cached; prod still needs them to run.) Leaked secrets rotated
separately; history purge TBD.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
2026-07-05 00:52:42 +02:00
m038andClaude Opus 4.8 ca15bd8bd7 merge: integrate prod git-sync auto-commit 9337003
Brings prod's merged post-form.md and account timestamp. The leaked
env/ secret tree it also carried is removed + gitignored in the
follow-up commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
2026-07-05 00:52:10 +02:00
m038andClaude Opus 4.8 1418dd275b config(api): exclude own IP from popularity page-view tracking
Adds 83.135.64.30 to popularity.exclude_ips so local/manual testing
against prod doesn't inflate real-visitor page-view stats. IP is
dynamic — a stopgap; smoke tests should target the test instance.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
2026-07-05 00:44:33 +02:00
m038 16a570ca3b Merge remote-tracking branch 'origin/main' into feat/journal-post-form
# Conflicts:
#	pages/02.post/post-form.md
2026-07-05 00:42:17 +02:00
m038 93370033ea (Grav GitSync) Automatic Commit from into-the-east-grav-sync 2026-07-04 22:31:12 +00:00
m038andClaude Opus 4.8 5752877e66 refactor(content): migrate demo source journal entries entry.md -> entry.en.md
Same rename as the real trips, applied to the italy-2026-demo seed source so
demo-load produces entry.en.md entries. Keeps the demo consistent with how the
edit form writes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FrCYNq6RXdGYbn5PFrhM
2026-07-05 00:29:17 +02:00
m038andClaude Opus 4.8 6abd08af40 refactor(content): migrate journal entries entry.md -> entry.en.md
The site runs languages.supported:[en] with include_default_lang_file_extension:
true, so add-page-by-form (and thus the new edit-in-place save) writes entry.en.md.
The 36 seeded journal entries used the plain entry.md, so an edit would leave a
stale orphan entry.md shadowed by the new entry.en.md. Rename them to the
config-canonical entry.en.md so edits overwrite in place with no orphan. Folder
names (which carry the entry URL) are unchanged; only the page file is renamed.
Stories are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FrCYNq6RXdGYbn5PFrhM
2026-07-05 00:29:00 +02:00
m038andClaude Opus 4.8 2840018372 chore: stop tracking per-install secrets in config
- gitignore config/security-private.php (CSRF/nonce + rate-limit signing
  salt); it is a per-install secret Grav auto-generates and its own header
  says not to commit. Each environment now keeps its own uncommitted salt.
- drop the runtime-generated popularity.salt from api.yaml; Grav re-derives
  it per install.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
2026-07-05 00:26:41 +02:00
m038andClaude Opus 4.8 b8daea217d feat(post-form): U6 — owner-scoped delete API route + card wiring
New custom-in-repo plugin entry-actions (un-ignored in .gitignore, NOT in
plugins.txt) registers DELETE /api/v1/entry/{slug} via onApiRegisterRoutes
(KTD5). The handler requires the authenticated site OWNER (not any login/admin),
rejects unsafe slugs (400), resolves the target through the page tree, asserts it
is a direct child of the active trip's dailies container, deletes the folder and
clears the cache — sharing EntryScopeGuard with the save path so R6 can't diverge.
A lazy per-namespace autoloader loads the controller on cached-route requests
(the router dispatches from route.cache without re-firing onApiRegisterRoutes).
EntryScopeGuard gains isOwnerUser() (API user comes from the request, not
$grav['user']) and enablePages() before find() (pages are lazily disabled in the
API context).

feed-actions.js (new, built via make build-assets; loaded on the trip/home feed
only when owner_can_edit) wires the inline Delete → Cancel/Confirm swap: on
Confirm it locks both buttons (D2, no double-DELETE), fetches the route
(credentials:include), removes the card, moves focus to the next card, and
announces via a page-level aria-live region (D4); on failure it restores the
control with an inline message (D7). Adds .sr-only + .entry-action[hidden] CSS.

Verified on the 2.0.4 container — API matrix 8/8 (anon 401, non-owner 403, bad
slug 400, out-of-scope 404, owner 204 + folder removed; V3/V5) and the delete UI
in a headless browser (confirm swap, card removal, disk deletion, live announce).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FrCYNq6RXdGYbn5PFrhM
2026-07-05 00:19:15 +02:00
m038andClaude Opus 4.8 265a06a972 feat(post-form): U5 — edit prefill + edit-mode form behaviour
The card Edit link opens /post?edit=<route>. post-form.js now (KTD4/KTD9, D1/D6/D7):
- On ?edit=, disables the form and shows 'Loading entry…' before the fetch (D1),
  so slow-connection typing can't be overwritten by the incoming prefill.
- GETs /api/v1/pages<route> (credentials:include, the gpx-manager session pattern)
  and populates every field from data.header.* / data.content: title, date
  (space→T for datetime-local), content (EasyMDE), lat, lng, city, country,
  weather select, temp, transport, featured, force_connect, published toggle.
- Sets the hidden edit_path to <route>/entry.md so cache-on-save toggles
  overwrite_mode:edit and the save writes back in place (stable URL).
- Hides the photos section and skips the ≥1-photo rule (photos untouched in M1).
- Switches chrome to 'Edit entry' / 'Save changes' (D6); reveals More options.
- On fetch failure, shows an inline banner and keeps the form disabled (D7).
- Skips draft restore in edit mode; carries edit+return on the form action for a
  future re-render (D3/D5). Rebuilt via make build-assets.

Verified in a headless browser: all fields prefill correctly incl. edit_path and
the published toggle, photos hidden, chrome correct (V6); fetch-failure banner
shown with fields left disabled (D7).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FrCYNq6RXdGYbn5PFrhM
2026-07-05 00:03:33 +02:00
m038andClaude Opus 4.8 c4228e9027 feat(post-form): U4 — Draft badge + owner Edit/Delete controls on cards
Render on each journal card, gated by owner_can_edit (R1/R3/R5/R6, KTD8):
- Draft badge (amber) when the entry is unpublished.
- Edit link -> /post?edit=<route>&return=<feed-url> (D5: a save from the home
  feed returns to home, not always the trip page); feed_return_url threaded from
  trip.html.twig/home.html.twig (page.url).
- Delete control with inline Cancel/Confirm button-swap (no browser dialog),
  data-entry-route carried for U6's delete JS, plus an aria-live status slot.
- 44x44px min tap targets (D8); titlerow wraps on narrow phones.

Verified on the container: anon sees no controls/badge; owner sees Edit+Delete on
all 13 cards with a Draft badge on the unpublished one; a non-owner authenticated
session (testrunner vs owner mischa) sees none and no drafts (V5). Browser
screenshot verification unavailable (no local Playwright); did a code-level
layout review instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FrCYNq6RXdGYbn5PFrhM
2026-07-04 23:48:57 +02:00
m038andClaude Opus 4.8 a2d4f81bfe feat(post-form): U3 — auth-aware feed collection; drafts owner-only
Owner-aware draft visibility (R5, KTD7/KTD8) on trip.html.twig and home.html.twig
(active-trip branch):
- Compute owner_can_edit = authenticated AND username == site.owner_username AND
  (this is the active trip). The super-admin tester authenticates too, so the gate
  is owner identity, not mere login.
- The feed list (all_items) uses an owner-aware journal collection: owner sees
  drafts, everyone else (and every non-active-trip view) sees published only.
- journal_entries stays published-only — it feeds stats/counts. map_entries now
  filters on item.page.published, so drafts get a feed card but no marker and no
  stat contribution.
- Thread owner_can_edit into trip-feed-col (defaults false) for the U4 controls.
Between-trips home grid stays published-only. Stories untouched (journal drafts only).

Verified on the container: draft entry with coords shows in the owner's feed but
not the map or count; absent entirely for anonymous (V4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FrCYNq6RXdGYbn5PFrhM
2026-07-04 23:44:00 +02:00
m038andClaude Opus 4.8 4e91492f65 feat(post-form): U2 — server-side active-trip + owner scope guard on edit save
Enforce R6 on the save path (KTD6): in cache-on-save's onFormValidationProcessed,
when a hidden edit_path is present, require the site owner (not merely any login —
the super-admin tester also authenticates) AND that the target resolves through
the page tree to a direct child of the active trip's dailies container. Fail
closed with a ValidationException so add_page never runs. Create (empty edit_path)
is left untouched.

New shared EntryScopeGuard (classes/EntryScopeGuard.php) is the single source of
truth for both R6 enforcement points — this save guard and U6's delete route call
the same isOwner()/resolveActiveDailyChild()/segment helpers, so they cannot
diverge (KTD5). Resolution is via $pages->find() + a parent-route assertion, never
raw path concatenation, closing the traversal hole (basename(dirname()) yields the
same target add-page-by-form writes to).

Verified on the 2.0.4 container: non-owner edit, out-of-scope edit_path, unsafe
'..' segment, and non-dailies-child targets are all rejected; owner in-place edit
succeeds (V3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FrCYNq6RXdGYbn5PFrhM
2026-07-04 23:40:04 +02:00
m038andClaude Opus 4.8 4aa9ae9b23 chore: stop tracking config/versions.yaml (per-install Grav state)
versions.yaml records each install's Grav schema-migration state and
differs per environment (dev 2.0.4, prod 2.0.7). Tracking it in the
shared content repo caused cross-env drift; with git-sync enabled it
would push one env's version stamp to all others. Gitignore + untrack so
each install owns its own (Grav regenerates it). Mirrors the existing
treatment of security.yaml / api-private.php.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
2026-07-04 23:32:53 +02:00
m038andClaude Opus 4.8 aa5d34717e feat(post-form): U1 — shared create/edit form + published field + owner_username
One /post form now drives both create and edit (KTD1, Alt B): cache-on-save
toggles add-page-by-form's overwrite_mode per submit — 'edit' when the new hidden
edit_path field is filled (write back in place, stable URL), 'false' when empty
(fresh dated folder via slug_field). add-page-by-form stays stock (no fork);
the toggle lives in cache-on-save, which reads edit_path and mutates the /post
page header's pageconfig before add-page-by-form's onFormProcessed runs.

- post-form.md: overwrite_mode:false (create-safe fallback), hidden edit_path,
  authoritative published toggle (default on), removed the static
  pagefrontmatter.published so the field wins on every submit (KTD3).
- cache-on-save.php: editPathFromForm + setOverwriteMode helpers.
- site.yaml: owner_username (single source of truth for the KTD8 owner gate).

Verified against the 2.0.4 container: create writes a fresh dated folder with
published:true/false as a real boolean (V1, V2); edit writes in place with the
slug/URL unchanged; save-as-draft-on-create works.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FrCYNq6RXdGYbn5PFrhM
2026-07-04 23:31:25 +02:00
m038 fff5358ce2 Merge remote-tracking branch 'origin/main' into feat/journal-post-form 2026-07-04 22:53:45 +02:00
m038andClaude Opus 4.8 b8a7bedcda config: track API plugin functional config so it deploys
The API plugin's enabled/route/session_enabled/cors/rate_limit config
lived only in the untracked user/plugins/api/api.yaml, so a fresh install
(prod) had no working /api — Admin2, which authenticates via /api/v1,
could not log in. Move the functional config into the tracked override
user/config/plugins/api.yaml (merged over the plugin defaults). Secrets
(JWT) stay in the gitignored api-private.php; the auto-generated
popularity salt is intentionally not committed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
2026-07-04 22:16:45 +02:00
m038andClaude Opus 4.8 8cf1145e6f feat(post-form): reverse-geocode City/Country + drop hero-image field
Get Location now reverse-geocodes the captured coordinates into City +
Country via BigDataCloud's free client endpoint, filling only blank fields
(never clobbering a manual entry) and appending the resolved place to the
location status. Best-effort — a failure leaves the coordinates intact.

Removes the redundant hero_image field: journal entries render their hero
from the first uploaded photo (entry-journal uses entry.media.images|first),
so an explicit hero filename served no purpose. Stories still use hero_image
but are not posted through this form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 21:06:01 +02:00
m038andClaude Opus 4.8 8b037511fd feat(post-form): require 1–6 photos per entry
Raise the FilePond limit from 4 to 6 and enforce a minimum of one photo.
The photo field is first in the form, so initValidation checks it first: an
empty picker blocks submit, reveals the (possibly collapsed) photo section,
and shows "Add at least one photo." under its header. Labels updated to
"Photos (1–6)".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:59:01 +02:00
m038andClaude Opus 4.8 1cde137daa feat(post-form): native datetime-local picker + require date client-side
The stock Grav `datetime` field template is deprecated and falls back to a
plain text box, so `type: datetime` + `default: now` rendered a raw input
showing the literal word "now" — unusable. Add a theme override at
templates/forms/fields/datetime/datetime.html.twig that renders a native
<input type="datetime-local"> (real calendar+clock, great on mobile), drop
the `default: now`, and prefill the current local time from post-form.js.

Also add `date` to the existing client-side validator. Together with the
picker (which can't hold an invalid value) this stops a bad/empty date from
round-tripping to the server — which was the trigger that made Grav re-render
the managed FilePond field from the session flash as filename-only inputs and
resurrect a photo the user had removed. Grav still reformats the submitted
value to the blueprint `format: 'Y-m-d H:i'` on save, so stored dates and
folder slugs are unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:41:49 +02:00
m038andClaude Opus 4.8 d17d1256ba feat(post-form): dark-theme the photo widget + drag-to-reorder that sticks
Styling: repaint FilePond's default light drop zone/thumbnails/actions with
the Field Notes dark tokens so the picker matches the site palette.

Reordering: enable FilePond drag-reorder (allowReorder, itemInsertLocation
'after'). FilePond does NOT re-sequence its submitted data[photos][] inputs on
reorder, so post-form.js sends the visual order as a top-level `photo_order`
POST key on submit. cache-on-save reads it from $_POST (after add-page-by-form
copies the files, priority -100) and renames them photo-1..N in that order —
which the entry honours since it lists media by filename and treats the first
as hero. The order key is top-level (not data[...]), so it never lands in the
entry frontmatter. Best-effort + self-idempotent: locates the new entry folder
by the uploaded filenames and no-ops if they're already renamed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:25:56 +02:00
m038andClaude Opus 4.8 2e32a8559c config: migrate strict_mode to Twig 3 compat flags for Grav 2.0
Grav 2.0's schema migration replaces the 1.7-era twig_compat with
twig2_compat/twig3_compat. Matching the repo to what the upgraded server
already generated so a content reset doesn't revert it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
2026-07-04 20:13:18 +02:00
m038andClaude Opus 4.8 87da3e4b5b feat(post-form): photos-first order, auto-collapsing photo summary, success-only view
- Reorder blueprint to Photos → Title → Content; drop title autofocus so the
  picker leads (it anchors what you write).
- After upload the photo section auto-collapses to a live summary bar
  ("✓ N photos ready — tap to review") and re-expands on tap; the field's
  own .form-label is hidden so the <summary> is the sole header. Trailing
  refreshes settle the summary past FilePond's event/DOM timing gap.
- Replace FilePond's murky completed-thumbnail overlay with a clean green ✓
  badge (remove action left intact).
- On a successful post, hide the reset form + location/weather controls so
  only the "✓ Saved" confirmation + View-journal CTA remain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 19:46:49 +02:00
m038 d3a520426a fix(post-form): load FilePond CSS in head + real success confirmation
Three field-reported bugs, all root-caused on the isolated test server:

1+2. FilePond's stylesheet never loaded — the filepond field registers it via
   assets.addCss() during body render, too late for the theme's head-only
   {{ assets.css() }}. Photo tiles rendered as giant unstyled boxes that
   stacked and overlapped the rest of the form (Get Location/Weather, Submit),
   making it unusable and looking like upload errors. Load filepond.min.css +
   image-preview CSS in the head_assets block; hide the PQINA credit.

3. The success notice rendered at the top of a long, reset form (off-screen
   after submitting from the bottom) and .notices was unstyled on the dark
   theme. Style .notices; on load, scroll the confirmation into view and inject
   a 'View your journal' link (to site.active_trip) + 'Post another' CTA.

Verified in a browser: 3 photos render compact without overlap; post-submit
shows the confirmation + working view link.
2026-07-04 19:23:02 +02:00
m038 c70e96879b fix(post-form): convert HEIC via FilePond beforeAddFile, not a custom uploader (U4)
Runtime verification showed the custom picker uploaded to Grav's flash but
never attached photos to the entry — attachment needs FilePond's exact
(undocumented) submit contract. Reverting to type:filepond and hooking its
beforeAddFile: a HEIC item is rejected, converted to JPEG via the lazy heic-to
chunk, then re-added with pond.addFile() so FilePond owns upload + page-attach
(the proven path). Web-format photos pass through; conversion failures fail
closed (inline status, original never added). Removes the custom photos field
template + AJAX uploader.

Verified end-to-end in a browser: HEIC posts as JPEG, corrupt HEIC is skipped,
Submit gated while converting, draft photos-reselect hint intact.

Refs R8, R9, R16, R17, AE1, AE4, KTD4.
2026-07-04 17:51:58 +02:00
m038andClaude Opus 4.8 2b48e427fd chore: gitignore api plugin private-key file (api-private.php)
The API plugin (>=1.0.7) relocates jwt_secret from config/plugins/api.yaml
into a generated config/plugins/api-private.php. Ignore it like the other
per-instance secrets (git-sync.yaml, security.yaml) so it is never committed
or pushed by git-sync.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
2026-07-04 17:49:59 +02:00
m038 9a96d0f19e feat(post-form): text-only draft persistence (U6)
Mirror text field values (incl. EasyMDE content) to localStorage on every
edit and restore them on load. Clear the draft only when the server confirms
a successful post (.notices.success) — the invariant that guarantees text
survives validation failures, save errors, and session expiry (login form
shown → form absent → draft left intact for post-reauth restore). Photos are
not persisted (File/Blob can't serialize); on restore an inline hint says
they need re-selecting.

Refs R19 (preserve-on-failure), R20, KTD6.
2026-07-04 16:45:28 +02:00
m038 7f021ab114 feat(post-form): Field Notes styling, disclosure, feedback states (U5)
Migrate Get Location/Weather + required-field validation out of the
template's inline scripts into the bundle: generalize the field lookup so
weather_desc (now a <select>) is found, gate Get Weather until coords exist,
and add idle/loading/success/error states. Build the 'More options' <details>
(native, KTD5) around the advanced trio, auto-open when any has a value.
Add Field Notes CSS: select/number styling, EasyMDE dark theme, disclosure,
button spinner, and server form-message states.

Refs R5, R11, R12, R18, R19, AE3, KTD5.
2026-07-04 16:42:58 +02:00
m038andClaude Opus 4.8 c4e9b2f6bf config: switch GPM release channel testing -> stable
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
2026-07-04 16:38:28 +02:00
m038 bf5b5c2f3c feat(post-form): client-side HEIC->JPEG photo picker (U4)
Replace /post's managed filepond field with a controlled 'photos' field
(theme forms/fields/photos) + picker logic in post-form.js: magic-byte
sniff (ISO-BMFF ftyp brands, not filename/MIME), lazy import('heic-to')
only for real HEIC (deferred 3MB chunk via ESM splitting), per-thumbnail
converting/uploading/done/error states, in-flight counter gating Submit,
and fail-closed skip on conversion failure. Converted JPEGs POST to Grav's
AJAX file-upload route into the form flash (the only path copyFiles reads),
so add-page-by-form attaches them on submit. Web-format photos pass through.

Refs R8, R9, R16, R17, AE1, AE4, KTD4.
2026-07-04 16:38:13 +02:00
m038 6282528dd8 feat(post-form): EasyMDE editor + /post-scoped ESM bundle (U3)
Add a page-scoped js/src/post-form.js bundle (built ESM + code-splitting
so U4's heic-to becomes a deferred chunk) loaded only by post-form.html.twig
via a new base head_assets block. EasyMDE replaces the bare textarea with a
minimal toolbar (bold/italic/list/link/preview), FA-free via CSS glyphs, and
syncs codemirror.save() on change + capture-phase submit so the inline
required-field validator and payload see the live value.

Refs R7, R15, KTD2, KTD3.
2026-07-04 15:40:28 +02:00
m038 87e216594e feat(post-form): expose all entry fields + weather picker (U2)
weather_desc hidden->select (emoji conditions mirroring entry.yaml),
weather_temp_c hidden->editable number, add transport_mode select and
the advanced trio (hero_image, force_connect, featured) tagged
advanced-field for U5's More options disclosure.

Refs R3, R4, R6, AE3.
2026-07-04 15:32:46 +02:00
m038 817c20c24a feat(post-form): inject active-trip parent server-side (U1)
Derive the entry write target from site.active_trip at submit time in
cache-on-save's onFormValidationProcessed handler, and fail closed
(ValidationException) when no active trip is set. Removes the hardcoded
pageconfig.parent that had to be hand-synced with active_trip.

Refs R1, R2, AE2, KTD1.
2026-07-04 15:29:07 +02:00
m038andClaude Opus 4.8 924cfc18e2 fix: point entry/story back button at the trip page
The back pills use history.back() when history exists, but fall back to
href=page.parent().url for direct-landing visitors (shared link, new tab).
After Phase 1 that fallback pointed at the retired /dailies (404) and
/stories (redirect) containers. Point it at the trip page (grandparent)
instead, which is where the feed now lives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 13:58:16 +02:00
m038andClaude Opus 4.8 e2ba8f413a refactor: extract shared entry-map partial (Phase 2)
trip.html.twig and home.html.twig hand-wrote near-identical map-column
markup + a MapUtils.initEntryMap() invocation. Extract both into
partials/entry-map.html.twig, parameterised (map id, global, entries,
gpx config, story markers). Callers now pass resolved header values.

The map engine (js/map.js initEntryMap) is unchanged; emitted config is
byte-equivalent to the previous inline scripts. Verified: trip-map,
home-map and window.tripMap/homeMap Playwright tests pass; both pages 200.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 13:42:56 +02:00
m038andClaude Opus 4.8 19a8fbcbf0 content: align dailies container title to 'Journal' across trips
Match the demo's naming (was 'The Journey' on real trips). These
containers are inert folders now; the title just labels what's below.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 13:32:17 +02:00
m038andClaude Opus 4.8 d79a67cbe6 refactor: retire standalone map/stats/dailies/stories sub-pages
Phase 1 of the standalone sub-page cleanup. The trip page consolidated
these views (inline map + filter bar + inline stats), leaving the
standalone pages unreachable and the last consumers of the old map
code path (feed-map inline script + map.html renderGpxJourney variant).

- Delete templates: map, stats, dailies, stories + feed-map partial
- Delete 02.map/03.stats page folders across all trips
- Keep 01.dailies/04.stories as inert data containers (routable:false)
- Fix demo source + Makefile demo-load so reload stays consistent
- Drop dead map-page body class in base.html.twig

Site converges on MapUtils.initEntryMap(); child entries/stories remain
reachable (verified: keepers 200, retired 404, no Twig errors).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 13:22:58 +02:00
m038andClaude Opus 4.8 29535e39b8 refactor(theme): consolidate map init into MapUtils.initEntryMap
Extract the duplicated MapLibre init orchestration (map construction,
marker/popup loop, bounds fit, GPX journey, fullscreen toggle) into one
config-driven MapUtils.initEntryMap(opts) in maplibre-utils.js, bundled
into map.js. Convert trip.html.twig and both home.html.twig branches to
call it; home active gains the flash-highlight + a fullscreen button to
match trip, and home highlights' marker click now navigates to the
article. Adds a markLatest opt (false for highlights) and exposes the
map as window.tripMap/window.homeMap (used by existing Playwright specs).
feed-map.html.twig and map.html.twig left on their inline init (deferred).

Plan: docs/working/plans/2026-06-27-map-init-consolidation.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BftDn9vu9SonFAY4vxu4uk
2026-06-28 00:11:34 +02:00
m038andClaude Opus 4.8 75da83d6e1 refactor(theme): extract home-predeparture partial; trip-feed-col single-purpose
Addresses code-review finding: the pre_departure branch made trip-feed-col
dual-purpose. Pre-departure landing now lives in its own partial; home picks
it via {% if all_items|length == 0 %}, and trip-feed-col drops the
pre_departure param to be shared 1:1 by both callers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BftDn9vu9SonFAY4vxu4uk
2026-06-27 21:03:26 +02:00
m038andClaude Opus 4.8 fa1d77f444 feat(theme): home-active reuses trip-feed-col partial with stats + pre-departure state
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BftDn9vu9SonFAY4vxu4uk
2026-06-27 20:33:54 +02:00
m038andClaude Opus 4.8 827429c551 refactor(theme): trip.html.twig uses shared trip-feed-col partial + initTripStats
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BftDn9vu9SonFAY4vxu4uk
2026-06-27 20:30:41 +02:00
m038andClaude Opus 4.8 fd41512ba8 feat(theme): add shared trip-feed-col partial
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BftDn9vu9SonFAY4vxu4uk
2026-06-27 20:29:23 +02:00
m038andClaude Opus 4.8 b92111b853 feat(theme): add shared initTripStats() for trip+home stats panels
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BftDn9vu9SonFAY4vxu4uk
2026-06-27 20:28:57 +02:00
m038andClaude Opus 4.8 d7a772ae02 feat(header): mobile hamburger menu + alignment & typography pass
- Header: offset the 4px green ::before bar so content centres in the
  visible dark area, not the geometric box (fixes wordmark/nav/icon
  sitting ~2px high — the root cause behind the whole alignment saga)
- Mobile (≤768px): collapse nav into a slide-down hamburger panel
  (standalone js/nav.js); inline nav retained on desktop
- Nav: use the display serif in sentence case so it shares the
  wordmark's anatomy and aligns naturally (no optical nudge needed)
- Typography tokens: three-tier display tracking (--tracking-display,
  --tracking-display-sm), --tracking-caps, --color-error, paper-glass
  overlays, --text-story/--leading-story; applied across components
- trip-dates: move inline styles to a class

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BftDn9vu9SonFAY4vxu4uk
2026-06-27 18:54:56 +02:00
m038andClaude Sonnet 4.6 ac38ae2a54 fix(home): wrap map init in DOMContentLoaded to fix broken homepage map
After the CDN elimination refactor, maplibregl moved from an inline head
script to the bottom-group bundle — but home.html.twig still called
new maplibregl.Map() immediately, before map.js had loaded. Wraps both
the active-trip and between-trips script blocks in DOMContentLoaded,
matching the pattern already used in trip/map/feed-map templates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 18:34:17 +02:00
m038 9fa1988211 chore: remove 39 ui-test entries from italy-2026-demo dailies 2026-06-25 07:29:14 +02:00
m038 f6ee4944f9 feat: complete CDN elimination + SVG icon build pipeline
CDN cleanup:
- entry.html.twig: remove PhotoSwipe CDN + duplicate JS (main.js handles it)
- dailies.html.twig: remove PhotoSwipe CDN + duplicate JS
- home.html.twig: remove PhotoSwipe CDN + MapLibre CDN + maplibre-utils.js
  script tags; add {% block map_assets %} to register map bundle

SVG build pipeline:
- Add lucide-static devDependency; gen-weather-icons.js reads icon SVGs
  from node_modules and emits templates/partials/weather-icons.html.twig
- package.json build script now runs gen-weather-icons.js before esbuild
- entry-journal.html.twig includes generated partial instead of hardcoded map
- Rebuild: main.js 83.7kb, map.js 860.7kb, all fonts; zero CDN requests remain
2026-06-25 00:13:17 +02:00
m038 b017d2ff64 fix: weather icon case mismatch + pin/weather icon alignment
- Lowercase all weather_icons map keys to match stored entry values
  ('sunny', 'partly cloudy', etc.); add |lower filter as safety net
- Remove vertical-align hack from location pin SVG
- Add display:inline-flex + align-items:center to .journal-post-location
  and .journal-post-weather so icons and text share a true center axis
2026-06-24 23:59:11 +02:00
m038 3345c076a9 style: replace weather emoji with Lucide SVG glyph icons
8 conditions mapped to stroke-based 14×14 SVGs (sun, cloud-sun, cloud,
cloud-fog, cloud-drizzle, cloud-rain, cloud-snow, cloud-lightning).
Icons use currentColor and aria-hidden; text label retained alongside.
2026-06-24 23:52:40 +02:00
m038andClaude Sonnet 4.6 ed25907545 fix: DOMContentLoaded wrapper + correct asset registration in map templates
- feed-map.html.twig: remove addCss/addJs calls (too late for <head>); merge
  two <script> blocks into one wrapped in DOMContentLoaded
- map.html.twig: move {% block map_assets %} above {% block content %} so
  map.css reaches <head>; add DOMContentLoaded around map init
- dailies.html.twig: add {% block map_assets %} override so map.css and
  map.js are registered for the feed-map partial

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-24 00:16:57 +02:00