diff --git a/Makefile b/Makefile index 43af199..3188d46 100644 --- a/Makefile +++ b/Makefile @@ -27,7 +27,8 @@ REMOTE_TARGETS := remote-env-setup remote-env-remove remote-wipe remote-install remote-fetch remote-fetch-content remote-install-plugins remote-update-plugins \ remote-upgrade-grav remote-git-sync-disable remote-git-sync-enable \ remote-content-status remote-clean remote-diag remote-apply-env \ - remote-gpm-install remote-maintenance-on remote-maintenance-off + remote-gpm-install remote-maintenance-on remote-maintenance-off \ + remote-apply-plugin-patches ENVS := test prod guard-env: @@ -89,6 +90,19 @@ fix-perms: install-plugins: docker exec -w /var/www/html intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y + $(MAKE) apply-plugin-patches + +# Re-apply local fixes to git-ignored, GPM-managed third-party plugins. Run this +# AFTER install-plugins (which overwrites them). See deploy/patches/README.md. +apply-plugin-patches: + @for p in deploy/patches/*.patch; do \ + [ -f "$$p" ] || continue; \ + if git apply --check "$$p" >/dev/null 2>&1; then \ + git apply "$$p" && echo "applied $$p"; \ + else \ + echo "skipped $$p (already applied or does not match)"; \ + fi; \ + done # ── Demo content ────────────────────────────────────────────────────────────── @@ -159,9 +173,24 @@ remote-fetch-content: guard-env remote-install-plugins: guard-env $(SSH) "cd $(WEBROOT) && php bin/gpm index -f && php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y" + $(MAKE) remote-apply-plugin-patches remote-update-plugins: guard-env $(SSH) "cd $(WEBROOT) && php bin/gpm update -y && php bin/grav cache" + $(MAKE) remote-apply-plugin-patches + +# Re-apply local fixes to git-ignored, GPM-managed third-party plugins on the +# remote (pristine after a GPM install/update). Piped over SSH like the git-sync +# scripts — no scp. `--forward` makes it a no-op when already applied. Runs +# automatically after remote-install-plugins / remote-update-plugins; safe to run +# standalone. See deploy/patches/README.md. +remote-apply-plugin-patches: guard-env + @for p in deploy/patches/*.patch; do \ + [ -f "$$p" ] || continue; \ + echo "remote-apply $$p"; \ + $(SSH) "cd $(WEBROOT) && patch -p1 --forward -r - --no-backup-if-mismatch" < "$$p" || echo " (already applied or no-op)"; \ + done + $(SSH) "cd $(WEBROOT) && php bin/grav clearcache" remote-upgrade-grav: guard-env $(SSH) "cd $(WEBROOT) && php bin/gpm self-upgrade -y && php bin/grav cache" diff --git a/deploy/patches/README.md b/deploy/patches/README.md new file mode 100644 index 0000000..3f713d8 --- /dev/null +++ b/deploy/patches/README.md @@ -0,0 +1,51 @@ +# Local plugin patches + +Patches for **third-party, GPM-managed plugins** that live under +`user/plugins/` — which is **git-ignored** (see `user/.gitignore`), so these +edits do **not** travel with the content repo and are **overwritten by +`make install-plugins`** / a fresh image build. Keep the fix here (tracked) and +re-apply it after any plugin (re)install, until the plugin is forked upstream. + +### Local (dev) + +```sh +make apply-plugin-patches # git apply, idempotent (skips if applied) +``` + +`make install-plugins` runs this automatically as its last step. + +### Remote (test / prod) + +```sh +make remote-apply-plugin-patches-test +make remote-apply-plugin-patches-prod +``` + +Each patch is piped over SSH into `patch -p1 --forward` at the webroot (no scp), +so it is a no-op when already applied. **Runs automatically** as the last step of +`remote-install-plugins-*` and `remote-update-plugins-*` — GPM lays down pristine +plugins, so the patch must follow every GPM install/update. Content pulls +(git-sync / `remote-fetch-content`) do **not** touch `user/plugins/`, so the patch +survives ordinary content syncs. Requires the `patch` tool on the server. + +Verify a patch is live on a server: +`grep -c toArray user/plugins/add-page-by-form/add-page-by-form.php` (≥1 = applied). + +## add-page-by-form-grav2-header.patch + +Fixes a fatal when **adding a new photo while editing an entry** (front-end +journal edit, milestone M2 / R9). + +- **Plugin:** `add-page-by-form` 3.3.0 (abandoned upstream — last release Sept 2023). +- **Bug:** the edit-mode branch reads existing frontmatter with + `(array)$pages->get($folder)->header()`. On Grav 2.0 `header()` returns a + `Grav\Common\Page\Header` object whose data sits in a **protected** `items` + property, so the `(array)` cast produces mangled keys (`\0*\0items`) and + `$original_frontmatter['photos']` is never set → `array_merge(null, …)` + throws a `TypeError` (PHP 8) on any edit that uploads a new file. +- **Fix:** use `Header::toArray()` (clean keys) with a fallback to the cast for + classic stdClass headers, and guard the per-field merge against a + missing/non-array original. + +Remove this patch once `add-page-by-form` is forked and the fix lands in the +fork (then pin the fork instead of the GPM package). diff --git a/deploy/patches/add-page-by-form-grav2-header.patch b/deploy/patches/add-page-by-form-grav2-header.patch new file mode 100644 index 0000000..77f6221 --- /dev/null +++ b/deploy/patches/add-page-by-form-grav2-header.patch @@ -0,0 +1,38 @@ +--- a/b/user/plugins/add-page-by-form/add-page-by-form.php 2026-07-05 12:03:55.849015242 +0200 ++++ b/user/plugins/add-page-by-form/add-page-by-form.php 2026-07-05 11:55:06.175609339 +0200 +@@ -619,7 +619,19 @@ + if ($overwrite_mode !== 'false') { + if (file_exists($new_page_folder)) { + if ($overwrite_mode === 'edit') { +- $original_frontmatter = (array)$pages->get($new_page_folder)->header(); ++ // intotheeast patch (temporary, pending upstream fork): ++ // On Grav 2.0 header() returns a Grav\Common\Page\Header ++ // object whose data sits in a PROTECTED `items` property, ++ // so the original `(array)$header` yields mangled keys ++ // (\0*\0items) and every frontmatter lookup below misses — ++ // `array_merge($original_frontmatter['photos'], …)` then ++ // fatals under PHP 8. Use toArray() (clean keys) when the ++ // Header exposes it; fall back to the cast for a plain ++ // stdClass (classic pages). ++ $__header = $pages->get($new_page_folder)->header(); ++ $original_frontmatter = (is_object($__header) && method_exists($__header, 'toArray')) ++ ? $__header->toArray() ++ : (array)$__header; + } else { + Folder::delete($new_page_folder); + } +@@ -708,7 +720,13 @@ + + $file_fields_updated = array(); + foreach ($file_fields as $file_field => $uploads) { +- $file_fields_updated[$file_field] = array_merge($original_frontmatter[$file_field], $uploads); ++ // intotheeast patch: entries that render from folder-scanned ++ // media carry no matching frontmatter key, so fall back to [] ++ // rather than fatal array_merge() on a missing/null original. ++ $existing = (isset($original_frontmatter[$file_field]) && is_array($original_frontmatter[$file_field])) ++ ? $original_frontmatter[$file_field] ++ : array(); ++ $file_fields_updated[$file_field] = array_merge($existing, $uploads); + + // Get any (uploaded and then) deleted files + foreach ($copy_files['deleted'] as $file_to_delete) { diff --git a/docs/guides/deploy-cycle.md b/docs/guides/deploy-cycle.md index ba26b63..b663b44 100644 --- a/docs/guides/deploy-cycle.md +++ b/docs/guides/deploy-cycle.md @@ -64,18 +64,20 @@ Referenced gotcha docs: ``` make remote-fetch-content-test # 1. clean-reset synced folders to repo state make remote-upgrade-grav-test # 2. gpm self-upgrade (rewrites schema — expect drift) -make remote-update-plugins-test # 3. gpm update the plugins.txt set +make remote-update-plugins-test # 3. gpm update the plugins.txt set (auto-applies deploy/patches/) make remote-gpm-install-test PKG=git-sync # 4. EXPLICITLY (re)install each remote-only plugin make remote-apply-env-test # 5. re-deploy the env override (not synced; gone after install) ``` Why each matters: +- **Step 3** re-applies `deploy/patches/*.patch` automatically (it chains `remote-apply-plugin-patches`). GPM install/update lays down **pristine** third-party plugins, wiping local fixes to git-ignored `user/plugins/` — the patch step restores them. Content pulls (step 1) do **not** touch `plugins/`, so the patch only needs re-applying after a GPM op, not after every sync. Run `make remote-apply-plugin-patches-test` standalone if you ever GPM-install outside this sequence. Requires the `patch` tool on the server. See `deploy/patches/README.md`. - **Step 4** is non-optional even if git-sync "was already there" — remote-only plugins are not in `plugins.txt`, so nothing in steps 1–3 restores them. If the code is missing, the plugin is inert despite valid config. - **Step 5** re-writes `user/env//config/…` from `deploy/env//`. The env tree is not synced by anything, so a fresh install loses it until you re-apply. ### Verify (smoke checklist — this is the payoff) - **Code present, not just config:** `ls user/plugins//` for every expected plugin (especially `git-sync`). An empty/absent dir = reinstall (step 4). *(Do this via an ssh one-liner you run, or `make remote-diag-test`.)* +- **Plugin patches applied:** confirm the add-page-by-form fix survived the GPM op — `grep -c toArray user/plugins/add-page-by-form/add-page-by-form.php` should be ≥1 (0 = pristine, re-run `make remote-apply-plugin-patches-test`). Functional check: edit a journal entry and add a photo — a pristine plugin 500s on save. - **HTTP:** `/` → 200, `/admin` → 200, `/api/v1/pages` → 401, `/gpx-manager` → 200. Watch for the double-`Content-Encoding` garbage page (fix: `debugger.shutdown.close_connection: false` in the env override — already in `deploy/env/prod/system.yaml`). - **Post smoke test:** submit one entry via `/post` and confirm it appears in the trip feed immediately. This proves the `cache-on-save` plugin works with prod caching on. - **Config drift:** `make remote-diag-test` — diff server config against the repo. Fold any *intended* schema migration (e.g. the Twig-3 `strict_mode` flags a `self-upgrade` writes) back into `user/config/system.yaml`, or the next `fetch-content` reverts it. diff --git a/docs/working/plans/2026-07-04-frontend-entry-edit.md b/docs/working/plans/2026-07-04-frontend-entry-edit.md index f895261..5209d42 100644 --- a/docs/working/plans/2026-07-04-frontend-entry-edit.md +++ b/docs/working/plans/2026-07-04-frontend-entry-edit.md @@ -9,7 +9,7 @@ date: 2026-07-04 # Front-End Journal Entry Edit - Plan -**Status:** 🔄 In progress — M1 (U1–U6) complete & verified (V1–V7). M2 **partially delivered** (2026-07-05): **U7 (load existing photos into FilePond) + remove + reorder** are implemented and verified end-to-end on the :8091 container — V9 (photos load, cover-ordered) and V10 (remove a photo, reorder so a different image is the cover; on-disk `photo-1..N` renumber) both pass; reconcile helpers also covered by a reflection unit test (4 cases). One real bug found & fixed en route: `onFormProcessed` fires once per `process:` action (4×), so photo reconciliation is now latched to run **once** (a 2nd pass deleted the just-renamed `photo-N` files). Changes are in `cache-on-save.php` (edit-aware reconcile) + `post-form.js` (U7 load, D1 disable-sweep excludes the FilePond field). **R9 (add NEW photos on edit) is DEFERRED to the form-to-page/image-upload rework** — not by choice: a new upload on edit runs add-page-by-form's edit-mode file merge, which fatals on Grav 2.0 (`(array)$page->header()` yields mangled protected-property keys, so `$original_frontmatter['photos']` is never set → `array_merge(null,…)` TypeError). add-page-by-form is a **stock GPM-managed, git-ignored** plugin (Alt B: no fork), so this can't be patched here; the add affordance is suppressed in edit mode (`allowBrowse/allowDrop` off) so remove/reorder ship cleanly without exposing the fatal. (The earlier Form 9.1.10 filepond "regression" was a separate, already-disproven test artifact — commit `06d9629`.) **Held on the branch for review — not merged / not deployed.** +**Status:** 🔄 In progress — M1 (U1–U6) complete & verified (V1–V7). M2 **partially delivered** (2026-07-05): **U7 (load existing photos into FilePond) + remove + reorder** are implemented and verified end-to-end on the :8091 container — V9 (photos load, cover-ordered) and V10 (remove a photo, reorder so a different image is the cover; on-disk `photo-1..N` renumber) both pass; reconcile helpers also covered by a reflection unit test (4 cases). One real bug found & fixed en route: `onFormProcessed` fires once per `process:` action (4×), so photo reconciliation is now latched to run **once** (a 2nd pass deleted the just-renamed `photo-N` files). Changes are in `cache-on-save.php` (edit-aware reconcile) + `post-form.js` (U7 load, D1 disable-sweep excludes the FilePond field). **R9 (add NEW photos on edit) now WORKS (2026-07-05)** via a local patch to add-page-by-form. Root cause: its edit-mode merge read existing frontmatter with `(array)$page->header()`, but Grav 2.0's `Grav\Common\Page\Header` keeps data in a protected `items`, so the cast mangled keys (`\0*\0items`) and `$original_frontmatter['photos']` was never set → `array_merge(null,…)` TypeError on any edit that uploads a file. Fix: use `Header::toArray()` (clean keys) + guard the per-field merge. add-page-by-form is abandoned upstream (last release Sept 2023) and its dir is **git-ignored/GPM-managed**, so the patch is tracked as `deploy/patches/add-page-by-form-grav2-header.patch` and re-applied via `make apply-plugin-patches` after any plugin reinstall — until the plugin is forked. Verified end-to-end on :8091: add a photo, remove one, reorder, and all three combined in one save (cover=first, existing preserved, dropped removed); create-with-photos and edit remove/reorder regressions still pass. (Grav 2.0.7 does **not** fix this on its own — the Header object is unchanged across the patch; only the plugin fix does.) **Held on the branch for review — not merged / not deployed.** ## Goal Capsule