Also corrects the green-run line: "test-post 6/6" is the scripts/test-post.sh
shell suite, not the Playwright specs under tests/ui/post/ — conflating the
two made the Playwright post specs look covered when they were never run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lightbox-dims.spec.js hardcoded ../../../user, so a run against a checkout
detached from the served tree planted its fixture in a different user/ than
Grav renders and LD1 failed as an opaque "card never appeared" timeout.
Take USER_DIR from helpers instead, which honours GRAV_USER_DIR.
The three specs in this folder that fail do so for real, pre-existing
reasons, and both files' headers implied otherwise:
- UG1/UG2 specify a submit gate that is not implemented. post-form.js's
only create-form guard is `converting > 0` (pre-FilePond HEIC
conversion); it never inspects FilePond item state at submit time, and
.photo-convert-status is created lazily by photoStatusEl() only from the
HEIC paths — so for a plain JPEG the element never exists and both
expectations fail as "element(s) not found". UG2 is the one that matters:
a failed upload keeping its thumbnail is unguarded silent data loss.
- LD1's header described its root cause in the past tense, reading as
fixed. entry-journal.html.twig:48-49 still emits {{ img.width }} /
{{ img.height }}, so EXIF-rotated photos still declare pre-rotation dims
and PhotoSwipe still squeezes them.
Left failing rather than skipped, per retries:0 — a red test here is a real
defect, and hiding these would lose both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A ui-test entry had survived into the active trip's dailies. Three independent
failures had to line up for that, and all three were real:
1. cleanupEntry() used host-side fs.rmSync. Grav's Apache workers run as root,
so every entry the form creates is root-owned and recursive removal needs
write permission on that directory — which the host user lacks. Cleanup had
never worked for form-created entries; it just threw inside a path nothing
checked. It now falls back to `docker exec … rm -rf` in the container that
actually serves USER_DIR.
2. globalTeardown's dailies sweep keyed off a `parent:` in post-form.md — a key
deliberately removed (the write target comes from site.yaml active_trip, and
CLAUDE.md forbids re-adding a static parent). The regex could never match, so
dailiesDir was always null and the sweep silently did nothing. It now reuses
helpers' own resolution instead of keeping a divergent copy.
3. Nothing pinned the suite to this checkout's server. playwright.config.js
defaults to :8081, so a worktree run hit the MAIN checkout — entries created
in one content tree while the specs asserted and cleaned up in another.
test-ui now passes GRAV_BASE_URL from GRAV_PORT, and globalSetup hard-fails
when the server's bind mount disagrees with the tree the specs read.
Also fixed, found on the way to a green run:
- test-account interpolated the password into an `sh -c` string, so a password
containing a shell metacharacter was re-parsed by the container's shell
(`sh: 2: <fragment>: not found`, no account, every UI run dead). It now
travels via `docker exec -e`, making the recipe indifferent to its contents.
- `make start` in a worktree always failed: travel-memories declares
`env_file: .env` and worktree-new creates none. It degrades to start-grav
there — a worktree with no server is what sent runs to :8081 in the first
place.
- test-form-config asserted a hero_image field that 8cf1145 deliberately
removed; it had been failing ever since.
Verified: config 22/22, post 6/6, location-override 20/20, and a full UI run
now leaves zero ui-test entries behind. The remaining UI failures are
pre-existing on main — site.yaml pins owner_username to a real account while
the suite logs in as testrunner, so owner-only controls never render for it.
Only trip-publish.spec.js patches that; delete-flow, edit-mode and anon-view
do not. Left for a separate branch.
R8 and R13 both described behaviour that changed in review, and R13 rested on a
server-side cleanCoordinate() that had never been committed. Both now describe
what actually ships, with the revision called out inline rather than silently
rewritten. The plan's Status keeps ✅ Complete but now records what the review
changed and the two things still open before merge.
Bumps the `user` gitlink to e873a9c (the review fixes). The submodule is
deliberately NOT pushed: git-sync would propagate it to production. So this pin
still references a commit that exists only locally — push `user/` and re-point
before this branch merges.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The U5 guard asserted only `.notices` toHaveCount(0) and toHaveURL(/\/post/).
Both pass instantly, and both also hold for a *successful* submit — the form
posts to /post and only renders its notice after the round trip, and the click
is dispatched via evaluate(el => el.click()), which skips Playwright's
navigation-aware waiting. So the one test standing between a bad coordinate
and the server could not fail. It now proves the negative on disk via
findEntry() after a settling interval, registers the tag for cleanup before
the click, and asserts the flag and value survived.
CLAUDE.md's "one map path" section stated flatly that a single map code path
exists, which location-map.js now contradicts. Recorded it as the one
sanctioned exception (an editor, not a display map; lazy-imported; shares only
MAP_STYLE) rather than leaving the doctrine wrong.
The `user` gitlink is deliberately NOT bumped here: its pin already points at
an unpushed submodule commit, which must be pushed and re-pointed before this
branch merges.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds Playwright coverage for the four cross-reviewer-confirmed bugs
fixed in the user/ submodule (map-load race on rapid reopen, mismatch
flag clearing on blank, and submit blocked on unresolved mismatch),
and bumps the user/ pointer to the commit with those fixes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mocks the Open-Meteo geocoding endpoint via page.route() so the suite is
hermetic. Covers the panel's closed-by-default state, search happy path,
Paris/Texas disambiguation ranking, no-match/network-failure/in-flight
states, XSS-safe rendering, map canvas singleton behavior, drag sync, the
mismatch flag, the maplibre-gl lazy-load boundary, and a full submit
round-tripping lat/lng into the entry's frontmatter.
Adds the implementation plan for the location-override feature and folds in
ce-doc-review findings: a panel-open sync gap (pin didn't render on reopen
with pre-existing coordinates), keyboard/ARIA accessibility gaps in the
search-results list and mismatch flag, a shared MAP_STYLE module to remove
duplication drift risk, and a corrected Open-Meteo risk/mitigation split.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses the actual root cause behind the Denmark 2026 corrupted-coordinate
bug: there was no supported way to set an entry's location to somewhere other
than the current GPS position, forcing hand-typed/pasted raw coordinates
through Admin2's fragile text field. Backend sanitization (cache-on-save)
already guards against silent corruption; this spec adds a frontend way to
avoid needing that path at all.
Regression specs for the two 2026-07-09 prod bugs (fixed in user/ e17a5dc):
- upload-gate.spec.js — UG1/UG2: create submit is blocked with a visible
message while a photo upload is in flight or after it FAILED; nothing may
land on disk. The form plugin's own guard misses LOADING and
PROCESSING_ERROR, which silently dropped a photo on a fast save.
- lightbox-dims.spec.js — LD1: a slide's data-pswp-* must equal the
browser-rendered natural size of the linked image. Fixture is an 800x600
JPEG with EXIF Orientation=6 (renders 600x800 portrait), planted on disk
in the demo trip (the active trip may be an unpublished draft that 404s).
New fixture: tests/fixtures/test-photo-exif-portrait.jpg.
Note: the suite currently needs GRAV_TEST_USER/GRAV_TEST_PASS overrides —
the .env GRAV_TEST_PASS contains shell-special chars that break `make
test-account` (see the Makefile comment requiring a plain password).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195b3cDdMeize2Mm1FgC2aU
Close the remaining root-owned bind-mount vector: build-assets (a docker
run, missed by the docker-exec fix in 209b804) now runs as the host
uid/gid with HOME=/tmp for npm's cache. Verified: build completes clean,
zero root-owned files under user/themes, bundles byte-identical.
Solution doc updated from "still open" to fixed; CLAUDE.md stack section
now matches the Dockerfile's Grav 2.0.7.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195b3cDdMeize2Mm1FgC2aU
Refresh audit of all 13 docs/solutions learnings against the current
codebase. Core guidance verified accurate everywhere; three docs had
reference drift:
- dual-repo-submodule-workflow: point worktree setup/teardown at the
make worktree-new/worktree-rm targets (manual procedure misses
.worktree-env isolation)
- docker-exec-root-owned-bind-mount-files: tracked-plugin list now
includes entry-actions; fix-perms description matches actual target
- grav-plugin-config-without-code-wont-enable: 3-category model's
custom-in-repo list now includes entry-actions
CONCEPTS.md: add Container, Content repo, Outer repo, Pin, Env tree,
Remote-only plugin; refresh Active Trip (switching is one setting now).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195b3cDdMeize2Mm1FgC2aU
Both shipped with feat/journal-post-form (merged + deployed to prod) and
passed owner UI/touch-drag QA on 2026-07-08. Corrected the stale
"not merged / not deployed" language and fixed a duplicate Status marker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The install-plugins fix (209b804) only covered docker exec. build-assets
runs `docker run node:20-alpine` without --user, so it still writes
root-owned node_modules + esbuild bundles into user/themes/ — which is what
blocked `git worktree remove` at teardown. Broaden the doc and prevention
rule to cover docker run, with the --user fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
Land the owner trip publish/unpublish toggle to local main: Playwright specs
(TP1-TP8), the ce-compound solution doc + CONCEPTS.md Published/Draft concept,
and the plan/spec docs. Bumps the user submodule pin to 543e8e3 (the merged
user/ main containing the feature + denmark-2026 cover content).
Local landing only — nothing pushed.
Document the trip publish-toggle cache-invalidation finding: an in-place
trip.md `published` edit under cache.check.method: folder + APCu driver stays
stale because the folder checksum is unchanged AND the web APCu store is
unreachable by a CLI clearcache — fixed with apcu_clear_cache() from the web
request. Cross-link the sibling grav-deleteall doc (the create/delete case) as
necessary-but-not-sufficient here, and add the Published/Draft trip status
concept to CONCEPTS.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
Add the three cases the code review flagged as uncovered:
- TP7 (R15): a failed POST reverts the switch and surfaces the visible toast.
- TP8 (R13): the in-flight lock suppresses a concurrent second submit (exactly
one POST fires while the switch is aria-busy/disabled).
- TP5 leg: a MISSING published key -> 400 (the array_key_exists branch, distinct
from the is_bool branch already covered).
All 10 trip-publish specs green (serial, worktree container).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
TP1/TP1b/TP2–TP6 cover the owner gate, coverless drafts, cache-correct
hide/restore, the active-trip confirm, backend authz (401/403/400), and
the home fallback. The suite pins site.owner_username to the authenticated
test user (restore on teardown) and runs serially — it mutates global
config and clears the shared cache, so it collides with parallel readers.
Bumps the user/ pin to the finished trip-publish-toggle content (064f0f0)
and marks the plan Complete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
A `reset --hard` content deploy leaves Grav's compiled-Twig/page cache
stale, so the first visitor pays the recompile. `remote-warmup` clears
the cache then crawls the public site (homepage + trips listing + every
trip page linked from it) to pre-render pages. Grav has no native warmup
command, so it's an HTTP crawl — which also doubles as a smoke test
(non-2xx pages flagged). Wired into REMOTE_TARGETS (-test/-prod variants)
and added as the final step in both deploy-cycle.md phases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
New learning: docker exec defaults to root, so make targets writing into
the ./user bind mount (esp. install-plugins -> gpm) created root-owned
files (11,624 accumulated), breaking worktree-rm. Fix: HOST_UID/HOST_GID +
`-u` on file-writing execs while the grav container still boots as root.
Cross-linked reciprocally with the sibling docker-dev-env upgrade doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
`docker exec` defaults to root, so `make install-plugins` wrote plugins into
the ./user bind mount as root — un-removable on the host without a root
container (exactly what blocked the 2.0.4 worktree cleanup). The grav service
can't simply run `user: 1000` because the base image entrypoint needs root to
bind :80 and set up cron, so drop only the file-CREATING CLI to the host user:
- HOST_UID/HOST_GID from id -u / id -g
- install-plugins makes cache/tmp writable (container-internal, never touches
the host) then runs gpm as the host user, so plugins land owned by you — no
post-hoc chown, no root files, no root rm needed at teardown
Verified: gpm reinstall as uid 1000 leaves 0 root-owned files under ./user
(was 11624), site healthy (/ and /admin 200), plugin patches reapplied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
Encode the dual-repo worktree SOP as make targets so no step is skipped:
worktree-new creates the outer worktree off main, inits its own user/
submodule, branches both repos, and starts an isolated Grav dev server on an
auto-picked free port (8090+) whose identity is persisted in a git-ignored
.worktree-env; worktree-rm tears it all down including the submodule deinit
that, when skipped by hand, leaves orphaned .worktrees/ dirs.
docker-compose.yml container_name + ports are parametrized as ${VAR:-default}
so the main checkout is byte-identical, and the 11 hardcoded intotheeast_grav
refs in local targets now use $(GRAV_CONTAINER). CLAUDE.md points at the
commands instead of the manual steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
New ui-bugs learning: cropResize fits-inside (returns a source-aspect
sliver), cropZoom crops-to-fill. Using cropResize for a cover/banner
strip hands the browser a portrait sliver that object-fit:cover then
upscales into a blur. Documents the fix, the empirical op comparison,
and prevention (verify Medium op dimensions; guard retina upscaling;
regression-test composition, not just the URL).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
- trip-header/trips-list specs now assert the cropZoom wide-strip aspect
and the all-or-nothing retina rule (1x-only for sub-2w sources), and a
new regression on us-canada-mex-2024 covers the reported portrait-blur
- remove the orphaned AE4 test (no-photos-demo fixture was deleted)
- backlog: full-res pixelfed reimport (luxury quality item)
- plan: record the cover-quality follow-up + AE4 fixture removal
- bump user pin -> 536ca20 (trip-description-hero cover fix)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
demo-load / demo-reset now loop over every fixture under docs/demo/trips/
rather than hardcoding each trip, so new fixtures (no-photos-demo and any
future one) are picked up with no Makefile edit. The only per-trip specific —
the source `dailies/` folder mapping to `01.dailies/` in the pages tree — is
uniform across fixtures, so one loop covers all. chown is scoped per fixture
dir (never the whole 01.trips tree) and all copies stay `|| true`.
Verified on the worktree server: a full reset then reload restores both
italy-2026-demo (7 gpx + 12 entries + stories) and no-photos-demo; trip suite
green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
- trip-header.spec.js: new U4/R9/AE4 test — a trip with no cover_image and no
entry images renders its text header with no .trip-header-banner strip.
- Makefile: demo-load/demo-reset now also (un)load the no-photos-demo fixture,
guarded with `|| true` so runs whose user/ predates the fixture skip it
instead of failing globalSetup.
- plan: post-review follow-up notes the applied fixes and records the two
intentionally-deferred findings (macro-owned resolution per KTD2; inline
toggle script per U4's Approach) so they are not re-flagged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
Add a review handover (Claude→future Claude: branch state, commit map,
dual-session isolation facts, and the owner-triggered landing procedure) plus
an owner UI QA checklist for the flows the harness can't verify (touch-drag,
interactive photo add/delete/reorder). Update the frontend-entry-edit plan
status: implementation + code-review done; only owner QA + landing remain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
- trip-header.spec.js: replace the vacuous `toContainText('finally made
sense')` (the tail text is in the DOM even while collapsed) with real
clamp/un-clamp assertions — clientHeight < scrollHeight when collapsed,
clientHeight >= scrollHeight once expanded — so the test actually proves the
toggle changes visibility.
- trips-list.spec.js: the header comment claimed R11 coverage no fixture
provided. Note that R11 (set-but-unresolvable cover_image) shares the exact
else-branch the R7/AE3 fallback test exercises, so it's covered by
construction in the shared cover macro.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
Document why deleteAll() alone left a freshly-posted entry 404-ing on its
edit-prefill API lookup (regular-pages index keyed on config->checksum, which
survives a create under cache.check.method:folder) and why the fix adds
Cache::invalidateCache(). Note the H1/M8 travelling:true coverage gap as a
tracked residual. Cross-link from bugs-and-fixes.md.
Code review F8 (learnings) + F5 (residual-gap tracking).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
Hoist the duplicated per-spec createEntry photo-fixture into a single
createPhotoEntry() in helpers.js (used by delete-flow, edit-mode, and the
anon-view draft). Register the tag for cleanup BEFORE the awaited 15s
success-toast assertion, so a create that lands on disk but whose toast
assertion times out no longer leaks an untracked entry. Add AE3b covering the
disclosure deviation branch (a non-default toggle auto-expands More options).
Code review F2 (leak), F3 (duplication), F6 (coverage).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
Code review flagged the shared partial's CLAUDE.md contract table as stale:
trip-feed-col.html.twig gained a trip_header_extras flag (set at :254, wired
from trip.html.twig) but the parameter table the trip/home callers rely on to
stay in sync had no row for it. Add the row + a note on what it gates and why
home omits it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
- trips-list.spec.js: one-liner presence/absence, retina srcset, cover
fallback, alt text (U3).
- trip-header.spec.js: HTD stacking order, expandable description, banner
fallback, split intact (U4).
- home.spec.js: AE7 — the gated trip-page extras never leak onto the home
route (asserted mode-independently so it can't race the home-highlights
suite that toggles travelling in a parallel worker).
Also marks the plan complete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
H1 (home journal feed) and M8 (home journey map source) only apply when
config.site.travelling is true — home.html.twig otherwise renders the
between-trips highlights grid, which has neither. They now detect that mode
(.home-highlights-title) and test.skip() with an explicit reason instead of
failing misleadingly, so they still run and validate whenever travelling is on.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
New specs: edit-mode (ES1 save round-trip + ES2/ES3 prefill 404/500 states),
delete-flow (DEL1-3 happy/cancel/failed), anon-view (AN1 no owner controls,
AN2 draft hidden from anon), photo-editor (live add/delete/reorder). Existing:
P3-P8 now attach a photo to satisfy the create photo-gate; V3 picker cap 4->6.
Full post suite 38/38, stable across parallel (3-worker) runs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
The create path now routes through the shared PhotoRenumberer, which
zero-pads to photo-01..NN; update the Playwright assertions and title to
match the new naming.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update the git-sync secret-exposure solution doc with today's operational
lesson: untracking an already-committed secret under a live bidirectional
sync. Covers the direction:both force-push-revert trap, the freeze-every-
server-first sequence, audit-before-reset (authoritative secret in env/),
the stale origin/main ref + sparse-checkout gotchas, and the ignore:-field
mechanism.
Add Makefile targets that supported the fix:
- remote-secrets-audit: secret-safe (existence + size + git ls-files, never
contents) audit of config/ vs env/<host>/config secret locations
- remote-content-status: also show the .gitignore diff git-sync regenerates
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record implementation completion and the verification split: PhotoRenumberer
unit-verified, build/lint clean, /post + assets serve locally; owner-session UI
verification and on-device touch-drag reserved for the user. Note the server-side
SVG block deferred to the R6 add/delete fast-follow (config/security.yaml is
gitignored).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
remote-seed-api-salt writes a per-host popularity salt into the env
override tree (user/env/<host>/config/plugins/api.yaml) so the api plugin
reads it there instead of appending one to the git-tracked
config/plugins/api.yaml. That appended salt kept the content working tree
perpetually dirty, which aborted git-sync's auto-merge on webhook and
silently broke prod auto-deploy. Salt is generated server-side, never
committed, and re-runs are idempotent.
remote-content-status now prints HEAD so a webhook auto-pull can be
verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The post form no longer hardcodes pageconfig.parent — cache-on-save derives the
write target from site.active_trip at submit time. Update the "Switching to a new
trip" section and the Trip-architecture note accordingly, and point at the Admin
Site → Active Trip page-picker. Drops the stale post-form.md / system.yaml sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adding a new photo while editing an entry 500s: add-page-by-form's edit branch
reads existing frontmatter via `(array)$page->header()`, but Grav 2.0's
Grav\Common\Page\Header keeps data in a protected `items`, so the cast mangles
keys and `$original_frontmatter['photos']` is never set → array_merge(null,…)
TypeError. Fix: use Header::toArray() (clean keys, stdClass fallback) + guard the
per-field merge. Grav 2.0.7 does not change this — only the plugin fix does.
add-page-by-form is abandoned upstream (last release 2023-09) and its dir is
git-ignored/GPM-managed, so the fix is tracked as deploy/patches/*.patch and
re-applied after any GPM install/update:
- make apply-plugin-patches (local) — chained into install-plugins
- make remote-apply-plugin-patches-{test,prod} — piped over SSH into
`patch -p1 --forward`; chained into remote-install-plugins / remote-update-plugins
Content syncs don't touch user/plugins/, so the patch survives them; only a GPM
op wipes it (now auto-restored). Runbook + README document the step and a
verify check. Remove once the plugin is forked.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New learning: docs/solutions/tooling-decisions/upgrade-local-grav-core-rebuild-docker-image.md
— the local Grav core is baked into the Docker image (only ./user is bind-mounted),
so it upgrades by a Dockerfile URL bump + image rebuild + `docker rm -f` recreate,
not the `gpm self-upgrade` the servers use (non-durable in-container).
Refreshed three docs this exposed as stale/incomplete:
- local-setup.md: rewrote the stale "newer Grav RC" section with the durable
rebuild procedure (recreate gotcha, verify, plugin refresh, non-durability note).
- deploy-cycle.md: Phase 0 now upgrades the local core; state-model notes the
image as a fourth surface beyond the three server layers.
- stale-grav-version-blocks-api-plugin-install.md: version-authority surfaces
3 -> 4 (hardcoded Dockerfile URL); clarified .env* GRAV_VERSION governs fresh
remote installs only, never the local Docker core.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Local Docker image was still baked at 2.0.4 while test and prod had
self-upgraded to 2.0.7. Bump the Dockerfile release zip to 2.0.7 and
align the GRAV_VERSION pins so a fresh remote-install lands on 2.0.7
directly instead of installing an older base and needing a self-upgrade.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Update Status: M2 partially delivered (U7 load existing photos + remove +
reorder verified end-to-end on :8091, V9/V10 pass). R9 (add new photos on edit)
deferred to the form-to-page/image-upload rework — a new upload on edit fatals
in stock add-page-by-form on Grav 2.0 ((array)$header mangled keys →
array_merge(null) TypeError); that plugin is GPM/git-ignored so it can't be
forked here. Add affordance suppressed in edit mode meanwhile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test had no user/env/<host>/ tree because deploy/env/test/ never
existed, so `make remote-apply-env-test` hit its missing-source guard
and was silently skipped — test ran with twig cache off and without the
gzip/Content-Encoding fix, hiding the exact class of gotcha that only
surfaced in prod.
Make deploy/env/test/system.yaml a symlink to deploy/env/prod/system.yaml
so both deployed hosts share one override and can never drift; genericize
the prod file header to say it now serves test + prod.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
ce-compound-refresh (scope: stale-grav-version-blocks-api-plugin-install).
Update (not consolidate — distinct triggers, moderate overlap): add a Related
back-link to the new grav-plugin-config-without-code-wont-enable sibling and
broaden the "verify ls user/plugins/<name>" prevention bullet to note the
config-without-code desync as a second non-functional-plugin case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Distills the 2026-07 Grav 2.0.4→2.0.7 cutover into a repeatable procedure:
the three-layer state model (plugin code / repo config / host env tree),
ordered per-phase make-target sequences, a smoke checklist that catches the
code-vs-config, stale-version, garbage-page, and git-sync-boomerang gotchas,
plus rollback and one-line invariants. Linked from CLAUDE.md Remote operations.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Documents the git-sync prod incident where the plugin was inert despite
`enabled: true`, because its CODE was missing from user/plugins/git-sync/
while its CONFIG survived in the per-host env tree. Fix: GPM reinstall
(make remote-gpm-install-prod PKG=git-sync) + clearcache. Captures the
diagnostic (ls the plugin dir before touching config) and the remote-only
plugin caveat (not in plugins.txt, not restored by the standard flows).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
The prior model claimed git-sync's add-set is scoped to the configured
`folders` (pages/config/themes), so accounts/ and user/env/ were "safe by
construction." That was wrong: prod auto-commit 9337003 pushed the whole
user/env/<host>/config tree (JWT secret, CSRF salt, git-sync token + webhook
secret), accounts/mischa.yaml, and system.yaml to Gitea — all outside the
configured folders. git-sync stages everything under user/ not gitignored;
.gitignore is the only reliable exclusion.
- Rewrite the architecture-patterns doc around the corrected predicate and
document the incident + remediation (gitignore /env/, rotate token+webhook).
- Correct git-sync-notes.md: env/ is NOT safe on folder scope; drop the
"never reaches Gitea" claim; fix the secret-files table intro + add /env/.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Disproven end-to-end on this branch (Grav 2.0.4 + form 9.1.10): file-upload
returns 200, photo persists to disk, POST /post returns 200 with no error
page. The form plugin's upload path is byte-identical 9.1.6->9.1.10, so the
version was never the cause. The original failures were a test artifact --
upload fixtures named as dotfiles (.real-photo.jpg) are rejected by the form
as 'Bad filename'.
The note was also mis-scoped: a 'we're working on it, don't touch' reminder
from the upgrade session belonged in that session's commit/plan, not as a
standing project-wide directive that then told THIS rework branch to stand
down.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FrCYNq6RXdGYbn5PFrhM
Prod was actually deployed (LIVE at intotheeast.com, Grav 2.0.7); the
plan previously said Phase 3 was documentation-only/not-executed. Record
the three deploy gotchas (docs/solutions/) and remaining follow-ups.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Knowledge-track: functional Grav plugin config must live in the tracked
user/config/plugins/<name>.yaml override, not the gitignored (and
gpm-update-wiped) user/plugins/<name>/<name>.yaml. Secrets stay in
gitignored *-private.php. Root of the api plugin being unconfigured on
the fresh prod install. Cross-linked with the api/rc.10 install-failure
doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Fresh prod install ran Grav rc.10 (stale .env.prod GRAV_VERSION), so GPM
would not serve the api plugin (needs >=2.0.4); admin2 (auth via /api/v1)
then 404'd login silently. Documents the dead ends (deploying api config,
gpm index -f, same-channel assumption) and the fix (self-upgrade core +
reinstall + bump .env.prod). Adds reciprocal 'same 2026-07-04 cutover'
cross-links across the three sibling deploy gotchas.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Non-FastCGI host + Apache mod_deflate: Grav's shutdown early-close emits
Content-Encoding: identity while mod_deflate adds gzip -> two conflicting
headers -> browsers render raw gzip bytes. Fix: debugger.shutdown.close_
connection:false in the prod env override. Documents the dead-end
(cache.gzip/allow_webserver_gzip take the same code path) and the
browser-style curl + screenshot verification.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Once user/env/<host>/ exists, Grav Admin saves ALL config (system +
plugin) into that env tree, not user/config/ — so prod's git-sync.yaml
lives at the env path, and Admin-on-server config edits are server-only
(not committed, not synced). Documented the footgun in CLAUDE.md §1
(session-loaded) and rewrote git-sync-notes.md: env-path location,
v3.4.4 folders format, gitignore table (incl. versions.yaml +
security-private.php TODO), and fresh-server enable procedure.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Grav Admin saves plugin config into the active environment's config tree
(user/env/<host>/config/plugins/) when an env override dir exists — so
git-sync.yaml landed there, not in user/config/plugins/. Update the
toggle script to take a WEBROOT and search both locations (env path
first), and update remote-git-sync-disable/enable to pass WEBROOT.
remote-diag now surfaces git-sync config (secrets redacted) from either
location.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
On this non-FastCGI host Grav's shutdown early-connection-close can't
fastcgi_finish_request(), so it emits 'Content-Encoding: identity' to ask
the webserver not to compress — but Apache mod_deflate compresses anyway
and adds 'Content-Encoding: gzip'. Two conflicting headers => browsers
render raw gzip bytes (garbage page). Surfaced when prod flipped to
debug:false today (activated Grav's full output path).
Fix in the prod env override: debugger.shutdown.close_connection:false
skips the identity-header path so mod_deflate compresses cleanly (single
header). Verified: one 'content-encoding: gzip', page renders correctly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Prod needs twig.cache:true / debug:false / auto_reload:false, but those
values break local dev, so they must not live in the committed
system.yaml. Add a per-environment override deployed only to the server
via Grav's environment://config (user/env/<host>/config/system.yaml):
- deploy/env/prod/system.yaml — version-controlled source of truth.
- make remote-apply-env-prod — writes it to the server + clears cache;
resolves the host in-recipe (WEB_HOST || REMOTE_HOST) to avoid the
recursive-make empty-export trap.
- remote-diag now shows the deployed override + whether twig cache is
populating, so prod-mode can be verified not assumed.
- CLAUDE.md §1 rewritten: never flip committed system.yaml; use the
override. Backlog updated (twig prod-mode + /post login-gate done;
note stale .env.prod GRAV_VERSION and pending git-sync).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
- remote-install-plugins now runs 'gpm index -f' before install so a
fresh server doesn't rely on the stale package index bundled in the
grav-admin zip (which can miss recently-published plugins).
- Add read-only remote-diag target: Grav version, installed plugin
versions, GPM view of the api plugin, api override keys, and log tail.
Surfaced the prod rc.10 vs 2.0.4 mismatch that blocked the api plugin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Two-milestone plan (M1 edit/delete/publish from feed cards, M2 editable
FilePond photos). Enriched from the ce-brainstorm Product Contract to
implementation-ready, then hardened through a ce-doc-review pass:
- KTD1 committed to patching add-page-by-form's edit branch (the
overwrite_mode:edit create path is unreachable as-is; verified in code)
- owner-username server guards + shared scope-guard helper for both the
edit save path and the new entry-actions delete route (traversal-safe,
page-tree resolution)
- missing edit/delete UX states (loading lock, in-flight guards, focus
management, save-failure preservation) filled in
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1FrCYNq6RXdGYbn5PFrhM
The hero_image blueprint field was dropped (journal heroes come from the
first uploaded photo), so AE3 now asserts the More-options disclosure
toggles open/closed rather than probing the hero input's visibility.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a test for the ≥1-photo requirement (submit blocked with a photo-section
error) and the max of 6 (FilePond maxFiles). Rework AE4: a corrupt HEIC is the
only "photo", so fail-closed now means submit is blocked rather than posting a
text-only entry. Give the success-CTA test a photo so it can post under the new
rule.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Covers the theme datetime override: the date field renders as
<input type="datetime-local">, is prefilled with the current local time in
the native value format, and clearing it blocks submit client-side (no
success notice) — the guard that keeps an invalid date from wiping the
FilePond photo list on a server re-render.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
End-of-feature pointer bump for the Grav 2.0.4 upgrade. Pins user/ to
the reconciled Twig 3 strict_mode config, already published to Gitea
main. Aligns the superproject with the finished content-repo state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Covers the reorder pipeline: two uploads produce photo-1.jpg/photo-2.jpg on the
posted entry, and the top-level photo_order POST key never appears in the entry
frontmatter. Adds a second image fixture so both slots are exercised.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Test env upgraded to Grav 2.0.7, plugins to stable, smoke-tested, git-sync
re-enabled. Documents the strict_mode Twig 3 migration gotcha (fresh-image
local build skips the schema migration that self-upgrade runs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
On Admin2-only installs, login new-user auto-detects the admin permission
type to api-only, so accounts lack admin.login and silently fail on
access.admin.login-gated pages like /gpx-manager. Document the root cause,
the --admin-type both fix baked into make test-account, and prevention.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Retarget AE1's completed-item assertion (the item is now hidden once the
section auto-collapses) to the "✓ N photo ready" summary, and add a spec
covering collapse-after-upload + re-expand on tap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Update CLAUDE.md + architecture.md stack to Grav 2.0.4 stable, admin2 2.0.10,
gpm.releases: stable, and the three-category plugin-management model
(GPM-managed / custom-in-repo / remote-only git-sync). Add execution outcome,
Phase 3 prod fresh-install runbook (documentation-only), and mark plan Status
complete. Note the Form 9.1.10 filepond regression as a known issue owned by
the separate form-to-page rework.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
Add a dedicated, gitignored testrunner account auto-created by the suite
(make test-account, also invoked from global-setup) so `make test` no longer
needs the real account in .env. The target uses --admin-type both so the
account has admin.login for the gpx-manager specs. test-post.sh switches its
login POST to --data-urlencode to survive special chars in credentials.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Np4cMQLF77i664CAQXySzU
waitForPhotoUpload waits on FilePond processing-complete; AE1/AE4 use
input.filepond--browser + the .photo-convert-status error status; V3/V4 back
to FilePond limit + non-image rejection; reauth hint -> .photo-reauth-hint;
test-form-config asserts type:filepond. Verified against a live server via a
browser smoke run (HEIC->JPEG attach, corrupt fail-closed, draft restore).
- test-form-config.sh: assert parent is NOT hardcoded (injected server-side),
active_trip set in site.yaml, and the new fields incl. custom 'photos' type.
- helpers.js: resolve active trip from site.active_trip (parent coupling gone);
fillEditor() drives EasyMDE via window.postFormEditor; waitForPhotoUpload()
waits on the new picker.
- post.spec / validation.spec: content via the editor, filepond selectors ->
the photo picker, P8 checks editor value, V3/V4 exercise the picker cap +
fail-closed non-image.
- post-form-ux.spec.js (new): AE3 disclosure, AE1 HEIC->JPEG, AE4 corrupt-HEIC
fail-closed, R18 weather gating, R20 draft restore.
- fixtures: real + corrupt .heic.
- test-post.sh: resolve dailies dir from active_trip.
Refs AE1-AE4, R18, R20, U7.