Commit Graph
365 Commits
Author SHA1 Message Date
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