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.
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
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
`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
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
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>
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>
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>
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
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
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
Split env config into local vs remote:
- .env: local/shared config, always loaded (docker compose + make test)
- .env.test / .env.prod: full remote config, loaded on demand via ENV
Remote targets now generate -test/-prod variants (e.g. remote-install-prod);
a guard-env prerequisite blocks bare remote targets with no environment set.
Refresh .env.example to document the two-tier layout and add .env.prod/.env.test
to gitignore and the never-read list in CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 cleanup companion — demo-load no longer creates the retired
map/stats sub-pages or copies their now-deleted source .md files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both api and admin2 are bundled with the Grav 2.0 zip and not available
via GPM. Extract and install both during remote-install. Remove the
ad-hoc remote-install-admin2 target — the main install now covers it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
Rewire demo targets to use docker exec for file ops (user/ is owned by
http), point to the new italy-2026-demo source, and reduce demo-reset
to a single rm -rf of the demo trip directory.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
- Update post form parent, Makefile demo targets, and test scripts to use
new trip-scoped paths (01.trips/japan-korea-2026/01.dailies)
- Rename tracker.spec.js → dailies.spec.js and update all /tracker URLs
to /trips/japan-korea-2026/dailies across nav.spec.js, post.spec.js,
helpers.js, and dailies.spec.js
- Add Italy 2025 demo trip to Makefile demo-load/demo-reset targets
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The getgrav/grav image is Debian-based and has no uid 1000 user,
causing Apache to fail switching to APACHE_RUN_USER=#1000 on restart.
fix-perms now creates the uid 1000 user if absent, sets ownership,
then gracefully reloads Apache workers so they run as uid 1000.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
APACHE_RUN_USER/GROUP=#1000 makes PHP/Apache write files owned by
the host user (mischa) instead of http. fix-perms target in setup
ensures ownership is correct after plugin install.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test-config: static YAML validator for post-form.md — checks that the
add-page-by-form action name, pageconfig/pagefrontmatter blocks, and all
required fields are correctly wired. Fast, no server needed. Catches the
class of bug that caused silent post failures.
test-post: end-to-end HTTP test — logs in, submits the form, verifies an
entry.md was created on disk, then cleans up. Requires GRAV_TEST_USER and
GRAV_TEST_PASS in .env (see .env.example).
make test-config # fast, no credentials needed
make test-post # full e2e, needs .env credentials
make test # both
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>