Commit Graph
100 Commits
Author SHA1 Message Date
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 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 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
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 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 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
m038 476c2c17ef fix: add whitespace-control to date-range macro declaration 2026-06-24 00:09:56 +02:00
m038andClaude Sonnet 4.6 24ffa42499 refactor: extract date range macro; fix stories.html.twig asset registration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-24 00:07:26 +02:00
m038andClaude Sonnet 4.6 04bcab2d55 refactor: extract stats and cycling panels to Twig macros
Move stats computation and both panel HTML divs out of trip.html.twig
into dedicated macros/stats.html.twig and macros/cycling.html.twig.
trip.html.twig imports both at the top of {% block content %} and
calls them via stats_m.stats_panel() and cycling_m.cycling_panel().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-23 23:57:54 +02:00
m038andClaude Sonnet 4.6 1458c24aa3 fix: remove duplicate inline sort handlers from dailies and stories templates
main.js now registers initSortButton for feed-sort-toggle on all pages;
inline IIFEs caused double-fire reversing to original order.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-23 09:36:49 +02:00
m038andClaude Sonnet 4.6 ede5ce1914 fix: move map_assets block to <head> so map.css registers before assets.css() renders
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-23 09:20:33 +02:00
m038andClaude Sonnet 4.6 0b73cf5048 fix: wrap map init in DOMContentLoaded — map.js loads at bottom via Asset Manager
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-23 09:06:33 +02:00
m038andClaude Sonnet 4.6 10354a0760 refactor: remove CDN tags from feed-map, map, story templates
- feed-map.html.twig: removed MapLibre/toGeoJSON/maplibre-utils CDN tags; added {% do assets.addCss/addJs %} calls directly in partial
- map.html.twig: removed MapLibre/toGeoJSON/maplibre-utils CDN tags; added {% block map_assets %} with Asset Manager registrations
- story.html.twig: removed Scrollama CDN tag (now bundled in main.js as window.scrollama)

All map init and script logic remains unchanged. CDN requests eliminated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 23:39:22 +02:00
m038 632a3028fa refactor: trip.html.twig — remove CDN tags, deduplicate JS, use MapUtils.parseGpxFiles
- Remove PhotoSwipe CSS CDN link (now in css-compiled/main.css via Asset Manager)
- Remove MapLibre GL, toGeoJSON, maplibre-utils CDN/local script tags
- Add {% block map_assets %} to register map.css and map.js via Asset Manager
- Remove filter bar IIFE (now initFilterBar in main.js)
- Remove sort toggle IIFE (now initSortButton in main.js)
- Remove local haversineKm and parseGpxFiles functions (now in MapUtils via map.js)
- Update parseGpxFiles call to MapUtils.parseGpxFiles
- Update haversineKm call to MapUtils.haversineKm in Mode B stats
- Remove makePanelToggle helper + calls (now initPanelToggles in main.js)
- Remove back-to-top DOMContentLoaded block (now initBackToTop in main.js)
- Remove PhotoSwipe <script type="module"> block (now initPhotoSwipe + initPhotoStrip in main.js)
2026-06-22 23:35:58 +02:00
m038 53d873b4c7 feat: register assets via Asset Manager, remove Google Fonts, remove inline photo strip script 2026-06-22 23:31:30 +02:00
m038andClaude Sonnet 4.6 a640294e9b build: add map.js bundle — MapLibre GL, toGeoJSON, MapUtils
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 23:29:06 +02:00
m038 f8a6b9eb96 feat: add parseGpxFiles and export haversineKm from MapUtils 2026-06-22 23:22:48 +02:00
m038andClaude Sonnet 4.6 5923ba431a build: add main.js bundle — fonts, PhotoSwipe, UI utilities
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 23:19:19 +02:00
m038andClaude Sonnet 4.6 75aaa61640 build: add esbuild scaffolding and Docker build-assets target
package.json with esbuild ^0.21 and npm dependencies (maplibre-gl,
photoswipe, scrollama, fontsource). Placeholder src files and
compiled outputs committed. node_modules gitignored.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 23:11:25 +02:00
m038 21c1d22859 fix: PhotoSwipe bg doesn't cover bottom — add !important and pin bg to viewport 2026-06-22 09:11:52 +02:00
m038 68b328dabc feat: enrich Slovenia 2024 Piran entry with coords and weather 2026-06-22 09:07:17 +02:00
m038 817bd17959 feat: split Slovenia 2024 into its own trip
Move Piran entry out of us-canada-mex-2024 into a new slovenia-2024 trip.
Rename entry folder to match the post title convention.
Fix us-canada-mex-2024 date_start to 2024-07-21 (first actual US entry).
2026-06-22 09:06:00 +02:00
m038 77dd99ee2b feat(stories): add mini-map via shared partial, add story card IDs 2026-06-22 01:39:29 +02:00
m038 857f33be54 refactor(dailies): use shared feed-map partial 2026-06-22 01:37:33 +02:00
m038 320a98893a feat: add shared feed-map partial (dailies + stories) 2026-06-22 01:33:26 +02:00
m038andClaude Sonnet 4.6 e07fb3a72a feat(map): exit fullscreen on marker click, then scroll to entry
When fullscreen is active, clicking a marker now triggers fsBtn.click()
to exit cleanly (handles class, body overflow, tripMap.resize + icon),
then waits 450ms for the exit animation before scrolling to the entry
and firing the highlight. Also fixes missing icon-swap CSS for
.home-map-col.is-fullscreen (was only targeting .feed-map-wrap).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 01:12:52 +02:00
m038andClaude Sonnet 4.6 1bb588d1d2 fix(trip): switch panel animation to max-height (grid-template-rows broken)
grid-template-rows: 0fr fails to fully collapse when the direct grid
child has overflow:hidden (creates a BFC that prevents 0-height).
max-height: 0 → 600px with overflow:hidden is simpler and reliable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 01:09:38 +02:00
m038andClaude Sonnet 4.6 aa1cb7411c fix(map): collapse attribution on load; darken fullscreen button
Attribution: MapLibre v4 uses <details> and may open it after load
regardless of compact:true — remove the open attribute in the load
handler to guarantee collapsed state.

Button: switch from teal to --color-canvas (#22201B) so it sits quietly
against the dark map; icon reads in --color-ink (warm cream).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 01:05:12 +02:00
m038andClaude Sonnet 4.6 5fe8c015f1 fix(map): theme fullscreen button with accent colour
Replace plain white with --color-accent/--color-accent-on so the button
reads as a site control rather than a stray MapLibre element.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 01:00:50 +02:00
m038andClaude Sonnet 4.6 6f9538053c feat(map): mobile fullscreen button on trip page map
Button in bottom-right of #trip-map (z-index:1000), hidden ≥769px.
Attribution moved to bottom-left to free the corner. Clicking toggles
.is-fullscreen on .home-map-col (position:fixed, 100dvh), locks body
scroll, and calls tripMap.resize() for MapLibre to re-render.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 00:56:57 +02:00
m038andClaude Sonnet 4.6 5e503cf3a5 fix(map): fullscreen btn inside map div, attribution moved to bottom-left
Button is back inside #feed-map with z-index:1000 to clear all MapLibre
layers. Attribution control disabled in constructor and re-added to
bottom-left so bottom-right is free for the fullscreen button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 00:20:23 +02:00
m038andClaude Sonnet 4.6 ce860cfef9 fix(map): move fullscreen button outside feed-map div, top-right corner
MapLibre's attribution button occupies bottom-right of the container.
Moving our button out of the map div avoids MapLibre's DOM entirely,
and top-right is clear of all default MapLibre controls.
Position anchor moves to feed-map-wrap (position:relative).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:54:13 +02:00
m038andClaude Sonnet 4.6 989755d33c feat(map): mobile fullscreen button for feed mini-map
Button in the bottom-right corner of the map, hidden ≥769px. Clicking
it toggles .is-fullscreen on .feed-map-wrap (position:fixed, full
viewport), locks body scroll, and calls feedMap.resize() so MapLibre
re-renders at the new size. Icon swaps between expand SVG and ✕.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:44:09 +02:00
m038andClaude Sonnet 4.6 9ddf52c635 fix(trip): raise stat-value clamp floor to 2rem (32px)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:35:32 +02:00
m038andClaude Sonnet 4.6 9b62f79301 fix(trip): raise stat-value clamp floor to --text-xl (1.75rem)
22px floor was too close to the preferred at 375px (6vw=22.5px), so
values were pinned near the minimum. 28px floor makes values pop more
on small screens while long values like ~12,366 still wrap gracefully.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:33:06 +02:00
m038andClaude Sonnet 4.6 64aa9ec023 revert(trip): restore stat-label to --text-xs
Small label is intentional — the contrast with the larger value is the
visual hierarchy. Revert the sm bump from the previous commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:30:24 +02:00
m038andClaude Sonnet 4.6 9bfd96af2c fix(trip): bump stat-label to --text-sm, widen stat-value fluid range
Label: xs (12px) → sm (14px) for clearer hierarchy below the value.
Value preferred: 5.5vw → 6vw so short values stay bold on mid phones.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:28:42 +02:00
m038andClaude Sonnet 4.6 000af6934f fix(trip): raise stat-value clamp floor to --text-lg for visual hierarchy
14px floor was too close to the 12px label size. 1.375rem keeps the
value visually dominant over the label even at minimum size.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:23:39 +02:00
m038andClaude Sonnet 4.6 c94e36a861 fix(trip): fluid stat-value font size with clamp()
Replaces fixed 3rem with clamp(--text-sm, 5.5vw, --text-3xl) so long
values like "4:32:15" scale down on mobile instead of overflowing.
Desktop (≥870px viewport) is unchanged at 3rem.

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