Compare commits
3
Commits
62f940f6ef
...
24867524a1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24867524a1 | ||
|
|
f3816bfc3e | ||
|
|
084f683e19 |
+21
-1
@@ -12,7 +12,7 @@ A **Trip** owns its **Entries** and **Stories**. Exactly one Trip is the **Activ
|
|||||||
A single journey the blog is organised around — the top-level content entity. A Trip aggregates its Entries and Stories and carries its own metadata (title, start/end dates, cover image, route GPX files). Each Trip renders as one consolidated **Trip page** showing an inline map, a filtered feed, and inline stats; the journal, map, stats, and story views are not separate pages.
|
A single journey the blog is organised around — the top-level content entity. A Trip aggregates its Entries and Stories and carries its own metadata (title, start/end dates, cover image, route GPX files). Each Trip renders as one consolidated **Trip page** showing an inline map, a filtered feed, and inline stats; the journal, map, stats, and story views are not separate pages.
|
||||||
|
|
||||||
### Active Trip
|
### Active Trip
|
||||||
The one Trip currently featured — set in site config and read by the home page and the post form. Switching the Active Trip is a deliberate, multi-file change; if the post form's target and the featured Trip fall out of sync, new posts land under the wrong Trip.
|
The one Trip currently featured — set in a single site-config value and read by the home page and the posting pipeline, which derives the write target for new Entries from it at submit time. Switching the Active Trip is that one setting; there is no separate post-form target to keep in sync.
|
||||||
|
|
||||||
### Published / Draft
|
### Published / Draft
|
||||||
A Trip's visibility state. A **Published** Trip is listed publicly and reachable by anyone; a **Draft** Trip is hidden from anonymous visitors in the public trip list, while the signed-in owner still sees it (marked "Draft") and can flip it back. The owner toggles this per Trip from the trip list.
|
A Trip's visibility state. A **Published** Trip is listed publicly and reachable by anyone; a **Draft** Trip is hidden from anonymous visitors in the public trip list, while the signed-in owner still sees it (marked "Draft") and can flip it back. The owner toggles this per Trip from the trip list.
|
||||||
@@ -28,6 +28,26 @@ The Trip's journal section is labelled "Journal" and lives in the Trip's `dailie
|
|||||||
### Story
|
### Story
|
||||||
A long-form, designed narrative piece within a Trip — hero image plus scrollytelling/gallery sections — distinct from the short, dated Entry. Stories are curated set pieces; Entries are the running log.
|
A long-form, designed narrative piece within a Trip — hero image plus scrollytelling/gallery sections — distinct from the short, dated Entry. Stories are curated set pieces; Entries are the running log.
|
||||||
|
|
||||||
|
### Container
|
||||||
|
A Trip's non-routable holder of child pages — one for Entries, one for Stories. A Container's own URL is deliberately inert (it renders no page of its own), while its children stay individually reachable and are aggregated onto the Trip page. Retiring a view must never delete its Container: the folder half is load-bearing data even when the page half is gone.
|
||||||
|
|
||||||
|
## Repos & deployment
|
||||||
|
|
||||||
|
### Content repo
|
||||||
|
The repository holding everything the site serves — pages, configuration, accounts, the theme. It has its own remote and its own release cadence: pushing it triggers production to pull via webhook, independent of the Outer repo.
|
||||||
|
|
||||||
|
### Outer repo
|
||||||
|
The dev-environment repository — tests, docs, scripts, container build — that nests the Content repo and records a Pin to an exact Content-repo commit, expressing "this dev-env state expects this content/theme state."
|
||||||
|
|
||||||
|
### Pin
|
||||||
|
The Outer repo's recorded Content-repo commit (also "pointer bump" for the act of updating it). Routine content churn never moves it; it is bumped once at the end of a cross-repo feature, to a commit already published on the Content repo's main branch. A stale Pin during normal work is expected, not an error.
|
||||||
|
|
||||||
|
### Env tree
|
||||||
|
A server's per-host configuration overlay. Once it exists, Grav's Admin writes **all** config edits there rather than into the shared configuration — so server-side Admin edits are server-only, invisible to content sync, and can hold live secrets. Diagnosing config on a server means checking both the shared configuration and the Env tree, with the Env tree winning at runtime.
|
||||||
|
|
||||||
|
### Remote-only plugin
|
||||||
|
One of the project's three plugin-management categories, alongside GPM-managed (declared in the shared install list and restored by the standard install flow) and custom-in-repo (code tracked in the Content repo). A Remote-only plugin is installed explicitly on servers and restored by **no** standard flow — if its code goes missing it stays missing until someone reinstalls it deliberately, even while its configuration persists in the Env tree.
|
||||||
|
|
||||||
## Flagged ambiguities
|
## Flagged ambiguities
|
||||||
|
|
||||||
- "daily" / "entry" / "journal post" all refer to the same concept (a dated journal post). Canonical term: **Entry**. The section/folder is named "dailies" and the nav label is "Journal" — these name the *collection*, not a different entity.
|
- "daily" / "entry" / "journal post" all refer to the same concept (a dated journal post). Canonical term: **Entry**. The section/folder is named "dailies" and the nav label is "Journal" — these name the *collection*, not a different entity.
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Blueprint Vetting — Research & Recommendation
|
||||||
|
|
||||||
|
**Status:** 📋 Not started
|
||||||
|
**Date:** 2026-07-08
|
||||||
|
**Scope:** All custom Grav blueprints (intotheeast theme page blueprints, theme blueprint, site-config extension). Stock Quark blueprints excluded.
|
||||||
|
|
||||||
|
## Files reviewed
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `user/themes/intotheeast/blueprints/entry.yaml` | Daily journal entry (Admin form) |
|
||||||
|
| `user/themes/intotheeast/blueprints/story.yaml` | Story pages |
|
||||||
|
| `user/themes/intotheeast/blueprints/trip.yaml` | Trip pages |
|
||||||
|
| `user/themes/intotheeast/blueprints/home.yaml` | Home page |
|
||||||
|
| `user/themes/intotheeast/blueprints.yaml` | Theme blueprint (identity only) |
|
||||||
|
| `user/blueprints/config/site.yaml` | Site-config extension (`active_trip`, `travelling`) |
|
||||||
|
|
||||||
|
## What's already good
|
||||||
|
|
||||||
|
- Toggle idiom is correct and consistent everywhere: `options: {1: Yes, 0: No}` + `validate: type: bool`.
|
||||||
|
- `trip.yaml` `autoconnect` keys `'on'`/`'off'` are properly quoted — avoids the YAML 1.1 boolean footgun (`on:` parsing as `true:`). `default: 'on'` is quoted too.
|
||||||
|
- `user/blueprints/config/site.yaml` follows the standard Grav pattern for extending system site config (fields merge into Admin → Configuration → Site); `validation: loose` present; the `pages` field options (`start_route`, `show_root`, `show_slug`) are all real options.
|
||||||
|
- `entry.yaml` correctly uses `@extends: {type: default, context: blueprints://pages}` and adds its fields as a new tab, so entries keep the full standard Admin UI.
|
||||||
|
- Required-field validation on story/home titles and story content is in place.
|
||||||
|
- `weather_temp_c` has sensible min/max bounds (−60…60).
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
### F1 — Only `entry.yaml` extends the default page blueprint (structural)
|
||||||
|
|
||||||
|
`story.yaml`, `trip.yaml`, and `home.yaml` define `form.fields.tabs` from scratch (no `@extends`). In Admin2 those page types show **only** the declared fields — no Options/Advanced tabs, so no slug rename, no ordering, no visibility, no publish dates, no taxonomy from Admin. The custom `header.published` toggles in story/trip partially compensate.
|
||||||
|
|
||||||
|
If the locked-down UI is deliberate, entry is the inconsistent one; if not, story/trip lose real capabilities (they're created repeatedly and may need slug/ordering control).
|
||||||
|
|
||||||
|
**Implementation note if extending:** story/trip use a tab key `content`, which collides with the default blueprint's Content tab — fields merge by key, so the duplicate `header.title`/`content` definitions override rather than duplicate, but the merged result needs a visual check in Admin. Their custom `header.published` toggle also becomes redundant with the default Options-tab toggle — keep one.
|
||||||
|
|
||||||
|
### F2 — `lat`/`lng` are free-text with no validation (data integrity)
|
||||||
|
|
||||||
|
`entry.yaml:27-35` and `story.yaml:64-74` declare latitude/longitude as plain `type: text`. Templates pipe the values straight into `number_format(6, …)` (`user/themes/intotheeast/templates/trip.html.twig:62`, `templates/home.html.twig:56`). PHP casts silently:
|
||||||
|
|
||||||
|
- European decimal comma `"35,0116"` → `35.000000` (marker subtly wrong)
|
||||||
|
- non-numeric garbage → `0.000000` (marker in the Gulf of Guinea)
|
||||||
|
|
||||||
|
No error surfaces anywhere. Fix: `validate: { type: float, min: -90, max: 90 }` for lat, `±180` for lng.
|
||||||
|
|
||||||
|
### F3 — `transport_mode` option drift (copy-paste divergence)
|
||||||
|
|
||||||
|
Entry offers `plane` (`entry.yaml:77`); story doesn't (`story.yaml:80-86`). The field — along with lat/lng, location, `force_connect` — is duplicated between the two blueprints, which is how drift happens. Grav supports shared partials via `import@`; in-repo example: `user/themes/quark/blueprints/blog.yaml:90` importing `partials/blog-bits.yaml`.
|
||||||
|
|
||||||
|
### F4 — `hero_image` UX inconsistency
|
||||||
|
|
||||||
|
Trip uses `pagemediaselect` (dropdown of uploaded media, `trip.yaml:40-44`); entry and story use free-text filename fields (`entry.yaml:60-64`, `story.yaml:33-37`) where a typo silently breaks the hero. `pagemediaselect` keeps the "blank = first image" fallback while removing typo risk.
|
||||||
|
|
||||||
|
### F5 — Minor items
|
||||||
|
|
||||||
|
| Item | Location | Detail |
|
||||||
|
|---|---|---|
|
||||||
|
| `travelling` default mismatch | `user/blueprints/config/site.yaml:15` | `default: false` vs option keys `1`/`0`; works via loose comparison, but `default: 0` matches every other toggle |
|
||||||
|
| Date type drift | `story.yaml:20-31` vs `trip.yaml:28-38` | story: `datetime` + `format: 'Y-m-d'` (the deliberate Admin2 datepicker fix); trip: plain `date`. Pick one convention |
|
||||||
|
| `<br>` in help text | `trip.yaml:61,73` | If Admin2 escapes HTML in help tooltips, users see literal `<br>` tags |
|
||||||
|
| `weather_temp_c` step | `entry.yaml:52-58` | HTML number inputs default to step 1 → `19.5` may be rejected client-side; fine if whole degrees are intended |
|
||||||
|
| `pagemediaselect` accept filter | `trip.yaml:42` | Extension-style `accept: ['.jpg', …]` is the filepicker convention; unverified against Admin2's SPA implementation |
|
||||||
|
|
||||||
|
## Recommendation
|
||||||
|
|
||||||
|
Treat as one small milestone in three parts, in this order:
|
||||||
|
|
||||||
|
### Phase 1 — Data-integrity + drift fixes (no decisions needed, low risk)
|
||||||
|
|
||||||
|
1. **F2:** add `validate: { type: float, min/max }` to all four lat/lng fields (entry + story).
|
||||||
|
2. **F3:** extract a shared theme partial `user/themes/intotheeast/blueprints/partials/` (e.g. `location-bits.yaml`) holding location name/country, lat/lng (with the new validation), `transport_mode` (superset incl. `plane`), and `force_connect`; `import@` it from entry and story. Follow the Quark example.
|
||||||
|
3. **F5 quick fixes:** `travelling` default → `0`; standardize date fields on `datetime` + `format: 'Y-m-d'` (matches the established Admin2 datepicker fix).
|
||||||
|
|
||||||
|
### Phase 2 — Structural decision (needs Mischa's call)
|
||||||
|
|
||||||
|
4. **F1:** recommended: add `@extends: default` to **story and trip** (repeatedly-created content pages that benefit from slug/ordering/options control); leave **home** minimal (singleton whose slug must never change). Resolve the Content-tab merge and duplicate-published-toggle notes above. Verify each Admin form visually after the change.
|
||||||
|
5. **F4:** switch entry + story `hero_image` to `pagemediaselect` (naturally bundles with the Phase 2 Admin verification pass).
|
||||||
|
|
||||||
|
### Verify-once checklist (manual, 5 minutes in Admin2)
|
||||||
|
|
||||||
|
- [ ] Trip page → Cover Image dropdown: do `.gpx` files appear? (If yes, the `accept` filter isn't applying — F5.)
|
||||||
|
- [ ] `use_gpx` / `autoconnect` help tooltips: rendered line breaks or literal `<br>`?
|
||||||
|
- [ ] Decide: whole-degree temperatures OK, or add `step` to `weather_temp_c`?
|
||||||
|
|
||||||
|
### Out of scope
|
||||||
|
|
||||||
|
- Post form (`/post`) field parity — separate surface, not touched by this vetting.
|
||||||
|
- Theme blueprint (`blueprints.yaml`) — minimal but valid; no theme options exist yet, nothing to add.
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
|
||||||
|
1. **F1:** Is the locked-down Admin UI for story/trip/home deliberate? (Recommendation above assumes it isn't for story/trip.)
|
||||||
|
2. Should `transport_mode` for stories include `plane` (superset) or stay intentionally narrower?
|
||||||
@@ -56,33 +56,28 @@ Production is unaffected either way: prod pulls `user/` directly via the content
|
|||||||
|
|
||||||
The payoff. Because `docker-compose.yml` mounts `./user` **relative to the compose file**, and a worktree is a full copy of the outer tree (compose file included), each worktree serves *its own* `user/`. Two worktrees = two independent sites, no gitlink collisions.
|
The payoff. Because `docker-compose.yml` mounts `./user` **relative to the compose file**, and a worktree is a full copy of the outer tree (compose file included), each worktree serves *its own* `user/`. Two worktrees = two independent sites, no gitlink collisions.
|
||||||
|
|
||||||
Set up a feature worktree off `main`:
|
**Use the make targets — don't do the steps by hand.** From the main checkout:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# outer worktree on a new feature branch
|
make worktree-new NAME=<feature> # create + start its own dev server
|
||||||
git worktree add .worktrees/<feature> -b feat/<feature> main
|
make worktree-rm NAME=<feature> # tear down cleanly
|
||||||
cd .worktrees/<feature>
|
|
||||||
|
|
||||||
# populate user/ at the pinned SHA, then branch it for the cross-repo work
|
|
||||||
git submodule update --init user
|
|
||||||
git -C user checkout -b feat/<feature>
|
|
||||||
|
|
||||||
# its own dev server — separate project name + port from the main checkout's :8081
|
|
||||||
docker compose -p itte-<feature> up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`worktree-new` does, in order: `git worktree add .worktrees/<feature> -b feat/<feature> main`, `git submodule update --init user`, branches `user/` onto `feat/<feature>`, writes a git-ignored `.worktree-env` (own compose project name, container name, auto-assigned port `8090+`) so every `make`/compose command run inside that worktree targets its own server, and starts the Grav service. The manual equivalent misses `.worktree-env` — without it, make commands in the worktree hit the main checkout's container on `:8081`.
|
||||||
|
|
||||||
`.worktrees/` is kept out of git via `.git/info/exclude` (local, shared across worktrees — no committed `.gitignore` change needed).
|
`.worktrees/` is kept out of git via `.git/info/exclude` (local, shared across worktrees — no committed `.gitignore` change needed).
|
||||||
|
|
||||||
### Teardown
|
### Teardown
|
||||||
|
|
||||||
A submodule inside a linked worktree stores its git dir under `.git/modules/user/worktrees/<name>`, so removing the outer worktree needs a second cleanup step:
|
A submodule inside a linked worktree stores its git dir under `.git/modules/user/worktrees/<name>`, so teardown needs a submodule-deinit step before the worktree can be removed — skipping it is what leaves orphaned `.worktrees/` dirs. `make worktree-rm NAME=<feature>` runs the full sequence:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -p itte-<feature> down
|
# what worktree-rm does internally
|
||||||
cd "$(git rev-parse --show-toplevel)" # back to the main checkout
|
make -C .worktrees/<feature> stop # compose down (its own server)
|
||||||
git -C .worktrees/<feature> submodule deinit user # detach the submodule worktree
|
git -C .worktrees/<feature> submodule deinit -f user # detach the submodule worktree
|
||||||
git worktree remove .worktrees/<feature> # remove the outer worktree
|
git worktree remove --force .worktrees/<feature>
|
||||||
git branch -d feat/<feature> # if merged
|
git worktree prune
|
||||||
|
git branch -d feat/<feature> # manual, if merged
|
||||||
```
|
```
|
||||||
|
|
||||||
### Landing a commit on main without disturbing the main checkout
|
### Landing a commit on main without disturbing the main checkout
|
||||||
|
|||||||
@@ -143,8 +143,8 @@ The reusable principle, worth internalizing beyond this one repo:
|
|||||||
- **Don't rely on `APACHE_RUN_USER` or compose-level `UID`/`GID` env vars to fix exec ownership** — they don't apply to `docker exec`. `APACHE_RUN_USER` only affects Apache workers; compose `user:`/env vars only affect services wired to consume them.
|
- **Don't rely on `APACHE_RUN_USER` or compose-level `UID`/`GID` env vars to fix exec ownership** — they don't apply to `docker exec`. `APACHE_RUN_USER` only affects Apache workers; compose `user:`/env vars only affect services wired to consume them.
|
||||||
- **You can't just add `user:` to a service whose entrypoint needs root** (to bind privileged ports, set up cron, etc.). Drop privileges per-exec instead of per-container.
|
- **You can't just add `user:` to a service whose entrypoint needs root** (to bind privileged ports, set up cron, etc.). Drop privileges per-exec instead of per-container.
|
||||||
- **If a tool run as non-root needs writable scratch dirs that are root-owned in the image, chown them container-internally first.** That doesn't touch the host.
|
- **If a tool run as non-root needs writable scratch dirs that are root-owned in the image, chown them container-internally first.** That doesn't touch the host.
|
||||||
- **Root-owned files accumulate invisibly.** (session history) Plugin code under `user/plugins/<name>/` is gitignored by project convention (only `cache-on-save` and `story-blocks` are tracked), so root-owned files pile up in the bind mount without ever appearing in `git status` — they only bite at worktree-removal time. Don't wait for `git status` to reveal them; `find ./user -uid 0 | wc -l` is the real detector.
|
- **Root-owned files accumulate invisibly.** (session history) Plugin code under `user/plugins/<name>/` is gitignored by project convention (only `cache-on-save`, `story-blocks`, and `entry-actions` are tracked), so root-owned files pile up in the bind mount without ever appearing in `git status` — they only bite at worktree-removal time. Don't wait for `git status` to reveal them; `find ./user -uid 0 | wc -l` is the real detector.
|
||||||
- **Keep a `make fix-perms` escape hatch** (`find ./user -uid 0 ... chown`) for residual root files — notably first-boot files the base-image entrypoint writes as root (`config/security.yaml`, `data/api-keys.yaml`), which no `-u` on a make target can reach. After this fix it's a rare mop-up, not a routine step.
|
- **Keep a `make fix-perms` escape hatch** (container-internal `chown -R 1000:1000 /var/www/html`) for residual root files — notably first-boot files the base-image entrypoint writes as root (`config/security.yaml`, `data/api-keys.yaml`), which no `-u` on a make target can reach. After this fix it's a rare mop-up, not a routine step.
|
||||||
- **Verification recipe:** `docker exec -u 1000:1000 <container> touch /mnt/f && stat -c '%u' host/f` should print your uid, not `0`.
|
- **Verification recipe:** `docker exec -u 1000:1000 <container> touch /mnt/f && stat -c '%u' host/f` should print your uid, not `0`.
|
||||||
|
|
||||||
This lives in the Makefile because make targets are the only sanctioned container interface in this project — the fix belongs there, not in ad-hoc docker commands.
|
This lives in the Makefile because make targets are the only sanctioned container interface in this project — the fix belongs there, not in ad-hoc docker commands.
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ How the code went missing here is **unconfirmed**. It happened around the git-sy
|
|||||||
|
|
||||||
- **Check the code layer before the config layer.** When a Grav plugin "won't enable" and config toggles do nothing, FIRST verify the code exists: `ls user/plugins/<name>/` on the server. Config-without-code is the failure class; the empty directory is the tell.
|
- **Check the code layer before the config layer.** When a Grav plugin "won't enable" and config toggles do nothing, FIRST verify the code exists: `ls user/plugins/<name>/` on the server. Config-without-code is the failure class; the empty directory is the tell.
|
||||||
- **Enumerate both layers in all locations when diagnosing.** Plugins have a code layer (`user/plugins/<name>/`) and a config layer, and on prod the config can live in the env tree (`user/env/<host>/config/plugins/<name>.yaml`) and persist completely independently of the code. Remember: once `user/env/<host>/` exists, Grav Admin writes ALL config there, so always check both `user/config/...` and the env path (env wins).
|
- **Enumerate both layers in all locations when diagnosing.** Plugins have a code layer (`user/plugins/<name>/`) and a config layer, and on prod the config can live in the env tree (`user/env/<host>/config/plugins/<name>.yaml`) and persist completely independently of the code. Remember: once `user/env/<host>/` exists, Grav Admin writes ALL config there, so always check both `user/config/...` and the env path (env wins).
|
||||||
- **Know which plugins are remote-only.** The 3-category model: GPM-via-`plugins.txt` (admin2 / api / flex-objects), custom-in-repo (cache-on-save / story-blocks), and remote-only (git-sync — never in `plugins.txt`). Remote-only plugins are NOT restored by the standard install/content flows, so reinstall them explicitly via GPM after any operation that could have wiped `user/plugins/`.
|
- **Know which plugins are remote-only.** The 3-category model: GPM-via-`plugins.txt` (admin2 / api / flex-objects), custom-in-repo (cache-on-save / story-blocks / entry-actions), and remote-only (git-sync — never in `plugins.txt`). Remote-only plugins are NOT restored by the standard install/content flows, so reinstall them explicitly via GPM after any operation that could have wiped `user/plugins/`.
|
||||||
- **Diagnose actual state before proposing config fixes.** An `ls` is cheaper than a guess. Establishing that the code was missing would have pointed straight at the reinstall instead of a round of config poking.
|
- **Diagnose actual state before proposing config fixes.** An `ls` is cheaper than a guess. Establishing that the code was missing would have pointed straight at the reinstall instead of a round of config poking.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ date: 2026-07-04
|
|||||||
|
|
||||||
# Front-End Journal Entry Edit - Plan
|
# 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) 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.) **Code-review complete (2026-07-07)** — the multi-agent review of the branch ran and all findings (F1–F8) were applied & verified (20/20 post specs on :8091); the review's own PERF finding confirmed and hardened the once-per-submit cache latch noted above. **Implementation + review are done; the only remaining items are (1) owner-session UI QA and (2) the deliberate landing step (merge `user/`→main, pin bump, `content-push`, deploy).** Both are captured in `docs/working/handovers/2026-07-07-journal-post-form-review-handover-and-qa.md`. Still **not merged / not deployed** — held for owner QA.
|
**Status:** ✅ Complete (2026-07-08) — 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.) **Code-review complete (2026-07-07)** — the multi-agent review of the branch ran and all findings (F1–F8) were applied & verified (20/20 post specs on :8091); the review's own PERF finding confirmed and hardened the once-per-submit cache latch noted above. **Landed 2026-07-08:** merged to `main` in both repos with `feat/journal-post-form`, pin bumped, and content pushed to Gitea → prod (outer pin `f4ab730` == `user/` `main` == `origin/main`). Owner-session UI QA and on-device touch-drag (Part B of `docs/working/handovers/2026-07-07-journal-post-form-review-handover-and-qa.md`) both passed 2026-07-08.
|
||||||
|
|
||||||
## Goal Capsule
|
## Goal Capsule
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ date: 2026-07-05
|
|||||||
|
|
||||||
# Photo Editor for Journal Entries (media-API) — Plan
|
# Photo Editor for Journal Entries (media-API) — Plan
|
||||||
|
|
||||||
**Status:** 🔄 In progress — implemented & server-logic verified (2026-07-05). Server (shared `PhotoRenumberer`, reorder route, guards) + client (own grid, SortableJS, FilePond decommission) landed; `PhotoRenumberer` unit-verified (pad/normalise/swap/gap/crafted-name-safety/10+/idempotent/ext), PHP lints clean, JS/CSS build clean, `/post` + assets serve on :8091. Server-side SVG block deferred to the R6 add/delete fast-follow (see Deferred). **Pending owner-session UI verification** (add incl. HEIC, inline-confirm delete, mouse reorder, combined; feed cover=first; regressions a/b/c) and **on-device touch-drag** — both need the owner login the harness can't obtain.
|
**Status:** ✅ Complete (2026-07-08). Server (shared `PhotoRenumberer`, reorder route, guards) + client (own grid, SortableJS, FilePond decommission) landed; `PhotoRenumberer` unit-verified (pad/normalise/swap/gap/crafted-name-safety/10+/idempotent/ext), PHP lints clean, JS/CSS build clean. Shipped with `feat/journal-post-form` — **merged to `main` in both repos and deployed** (outer pin `f4ab730` == `user/` `main` == `origin/main`; content pushed to Gitea → prod). Owner-session UI QA (add incl. HEIC, inline-confirm delete, mouse reorder, combined, feed cover=first, regressions a/b/c) and on-device touch-drag both passed 2026-07-08. Server-side SVG block deferred to the R6 add/delete fast-follow (see Deferred).
|
||||||
|
|
||||||
## Why this exists (the honest reason)
|
## Why this exists (the honest reason)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user