diff --git a/CLAUDE.md b/CLAUDE.md index 3f23182..fab90e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,24 @@ - **./**: Grav CMS dev environment for intotheeast travel blog - **scripts/**: Server install and maintenance scripts - **user/**: Site content, config, pages, and theme (standalone git repo — do not modify from here) +- **docs/**: All plans, specs, and project documentation (moved here from `user/docs/` on 2026-06-19) + +### Current stack + +- **Grav:** 2.0.0-rc.9 (installed manually — see §3 below) +- **Admin:** Admin2 v2.0.0-rc.15 (plugin slug: `admin2`, NOT `admin`) +- **Docker image:** `getgrav/grav` with `GRAV_CHANNEL=beta` +- **PHP session:** `session.save_path = /tmp` set in `php/php-local.ini` + +### Trip entity architecture + +The site is structured around Trip entities. Key facts: +- Active trip is set in `user/config/site.yaml` → `active_trip: japan-korea-2026` +- Trip pages live at `user/pages/01.trips//` +- Each trip has: `01.dailies/`, `02.map/`, `03.stats/`, `04.stories/` +- Nav in `base.html.twig` derives all links from `config.site.active_trip` +- Post form parent (`post-form.md` → `pageconfig.parent`) **must be kept in sync** with `active_trip` +- GPX route files live as media on the trip page itself, served via leaflet-gpx CDN ### Environment @@ -23,6 +41,8 @@ Always use `make` commands for anything on the production server (`make remote-i - `make content-push` — commit and push `user/` to Gitea (triggers production pull via webhook) - `make content-pull` — pull latest from Gitea to local - `plugins.txt` is manually maintained — installing a plugin via Admin does NOT update it +- `make demo-load` — load demo entries for both trips (Japan/Korea 2026 + Italy 2025 with real GPX) +- `make demo-reset` — remove demo entries (keeps trip page structure, removes entries only) ### User repo gitignore @@ -52,7 +72,7 @@ Before going live, change in `user/config/system.yaml`: |---|---|---| | `twig.cache` | `true` | Templates compiled once and reused; safe because theme files don't change at runtime | -**Pre-launch smoke test required:** with `twig.cache: true`, submit one post via `/post` and confirm the entry appears in `/tracker` immediately. This verifies the cache-on-save plugin (BUG-001 fix) works correctly with caching enabled. +**Pre-launch smoke test required:** with `twig.cache: true`, submit one post via `/post` and confirm the entry appears in `/trips/japan-korea-2026/dailies` immediately. This verifies the cache-on-save plugin (BUG-001 fix) works correctly with caching enabled. ### What the cache-on-save plugin handles @@ -77,6 +97,58 @@ run `make fix-perms`. This creates uid 1000 in the container, chowns `/var/www/h and reloads Apache. Always run `make setup` (not just `make start`) after `docker compose down && up` to ensure permissions are correct. +### Grav 2.0 upgrade (local) + +GPM (`php bin/gpm selfupgrade`) does **not** serve Grav 2.0 RC — it still reports 1.7.x as latest even on the `testing` channel. To upgrade locally: + +```bash +# Download grav-admin bundle (includes Grav core + admin2 plugin) +docker exec -w /tmp intotheeast_grav bash -c " + curl -sL 'https://getgrav.org/download/core/grav-admin/2.0.0-rc.9?testing' -o grav-admin.zip && \ + unzip -q grav-admin.zip +" +# Copy core files only (not user/) +docker exec -w /tmp intotheeast_grav bash -c " + cp -rf grav-admin/{assets,bin,system,vendor,webserver-configs,index.php,composer.json,composer.lock,robots.txt,CHANGELOG.md,LICENSE.txt} /var/www/html/ +" +# Install Admin2 from the bundle (it's named admin2, not admin) +docker exec -w /tmp intotheeast_grav bash -c " + cp -rf grav-admin/user/plugins/admin2 /var/www/html/user/plugins/admin2 +" +make fix-perms +docker exec -w /var/www/html intotheeast_grav php bin/grav cache --all +# Cleanup +docker exec intotheeast_grav rm -rf /tmp/grav-admin /tmp/grav-admin.zip +``` + +After upgrading, ensure these settings in `user/config/system.yaml`: +```yaml +accounts: + type: flex # required for Admin2 API +pages: + type: flex # required for Admin2 pages API +``` + +And ensure the admin user account has `api.*` permissions (Admin2 uses a new permission namespace): +```yaml +# user/accounts/.yaml +access: + admin: + login: true + super: true + api: + super: true + access: true +``` + +**Disable the old `admin` plugin** once `admin2` is installed — both route to `/admin` and conflict: +```bash +# In user/plugins/admin/admin.yaml: +enabled: false +``` + +**JWT secret:** Leave `jwt_secret: ''` in `user/plugins/api/api.yaml` — it works for local dev and production installs generate a secure secret automatically. + ### Language URL prefix If Grav redirects to `/en/...` URLs, ensure `user/config/system.yaml` contains: diff --git a/docs/bugs-and-fixes.md b/docs/bugs-and-fixes.md new file mode 100644 index 0000000..cc128f4 --- /dev/null +++ b/docs/bugs-and-fixes.md @@ -0,0 +1,204 @@ +# Bugs & Fixes + +Backlog of confirmed bugs with root cause analysis and implementation spec for the fix. + +--- + +## BUG-001 — New entry not visible after form submission + +**Status:** fixed 2026-06-18 +**Reported:** 2026-06-18 + +### Symptom + +After submitting a new post via `/post`, the entry page file is created correctly on disk but does not appear in the `/trips//dailies` feed or in the Grav Admin panel until the cache is manually flushed. + +### Root cause + +Grav's page-tree cache (`cache/doctrine/`) is not invalidated when `add-page-by-form` writes a new page to disk. The tracker template uses `page.children`, which Grav serves from cache — so the new child page is invisible until the cache is cleared. + +### Workaround (manual) + +Run in terminal after each submission: + +```bash +docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache" +``` + +### Fix spec + +Wire cache-clear into the form process so it happens automatically on every successful submission. + +**Approach — custom Grav plugin event hook:** + +1. Create a small plugin `user/plugins/cache-on-save/` with one event listener: + - Listen on `onFormProcessed` + - When the form name is `new-entry`, call `$this->grav['cache']->deleteAll()` (note: `clear()` does not exist on `Grav\Common\Cache` in Grav 1.7) +2. Enable the plugin in `user/config/plugins/cache-on-save.yaml` + +This is the cleanest approach: it fires exactly once per successful submission, requires no changes to `post-form.md`, and works for any future forms too. + +**Alternative — disable page cache entirely:** + +Set `cache: { enabled: false }` in `system.yaml`. Simpler but degrades frontend performance; not recommended for production. + +### Files to create/modify + +| File | Change | +|------|--------| +| `user/plugins/cache-on-save/cache-on-save.php` | New plugin, ~30 lines | +| `user/plugins/cache-on-save/cache-on-save.yaml` | Plugin manifest, enabled: true | +| `user/config/plugins/cache-on-save.yaml` | Runtime config, enabled: true | + +### Acceptance criteria + +1. Submit a new post via `/post` +2. Navigate to `/trips//dailies` — the new entry is visible immediately, no manual cache flush needed +3. Grav Admin also shows the new page immediately + +--- + +## BUG-002 — Stale Twig cache after theme file changes + +**Status:** fixed 2026-06-18 +**Reported:** 2026-06-18 + +### Symptom + +After theme template files are added or modified (e.g., creating `partials/base.html.twig`), Grav's Twig compiled-template cache still holds the old compiled version. Pages that extend the changed file throw 500 errors like "Template partials/base.html.twig is not defined" even though the file exists on disk. + +### Root cause + +Grav caches compiled Twig templates in `cache/twig/`. When a new file is added, existing templates that reference it don't know to recompile — their cache entries are still valid from their own mtime perspective. + +### Workaround (manual) + +Run after any theme file is added or changed: + +```bash +docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache" +``` + +### Fix spec + +Disable Twig template caching in development via `user/config/system.yaml`: + +```yaml +twig: + cache: false +``` + +Acceptable for a single-user dev setup — eliminates both BUG-001's side-effect and this bug entirely. Performance cost is negligible at one-user scale. On production, leave Twig cache enabled (it's fine there because template files don't change at runtime). + +**Files to change:** + +| File | Change | +|------|--------| +| `user/config/system.yaml` | Add `twig: { cache: false }` under development section | + +### Acceptance criteria + +1. Add a new theme template file +2. Reload any page — no 500 error, template works immediately without manual cache flush + +--- + +## BUG-003 — One post per day limit; silent failure on duplicate date + +**Status:** fixed 2026-06-18 +**Reported:** 2026-06-18 + +### Symptom + +Submitting a second post with the same date as an existing entry shows "Entry posted successfully!" but creates no file. The user's post is silently discarded. + +### Root cause + +The `add-page-by-form` plugin built the page slug from date only (`Y-m-d`), producing folder names like `2026-06-18.entry`. With `overwrite_mode: false`, if that folder already exists the plugin skips page creation but does not abort — the `message` process step runs regardless, showing a false success. + +### Fix + +Change the slug template in `user/pages/02.post/post-form.md` to include time and title: + +```twig +{{ form.value.date|date('Y-m-d-Hi') }}-{{ form.value.title|lower|regex_replace('/[^a-z0-9]+/', '-')|trim('-') }} +``` + +Example: title "Arrived in Tokyo" at 14:30 on 2026-06-18 → `2026-06-18-1430-arrived-in-tokyo` + +The slug is locked at creation time. Renaming the title afterwards does not change the URL. + +### Acceptance criteria + +1. Submit two posts on the same day with different times or titles — both appear in `/trips//dailies` as separate entries +2. Renaming a post's title in the frontmatter does not break its URL + +--- + +## BUG-004 — Admin2 shows empty dashboard after Grav 2.0 upgrade + +**Status:** fixed 2026-06-19 + +### Symptom + +After installing Grav 2.0 + Admin2, logging in shows an empty dashboard with no sidebar navigation (only a Settings item visible). Pages and content are not accessible. + +### Root causes (three separate issues) + +**A) Wrong user account type.** `system.yaml` had `accounts.type: regular` (old file-based system). Admin2's API plugin uses the Flex user collection to look up accounts. With `regular`, the API saw zero users and entered setup-wizard mode. + +**B) Wrong pages type.** `system.yaml` had `pages.type: regular`. Admin2's pages API requires `pages.type: flex` to serve the page tree. + +**C) Missing `api.*` permissions on user account.** Grav 2.0 Admin2 uses a new `api.*` permission namespace (`api.super`, `api.access`, etc.) instead of the old `admin.super`. A user with only `access.admin.super: true` appears as a non-admin to Admin2. + +### Fix + +In `user/config/system.yaml`: +```yaml +accounts: + type: flex +pages: + type: flex +``` + +In `user/accounts/.yaml`: +```yaml +access: + admin: + login: true + super: true # keep for backward compat + api: + super: true # required by Admin2 + access: true # required by Admin2 +``` + +### Notes + +- `api.super: true` causes Admin2 to grant all sub-permissions automatically (`api.pages`, `api.config`, etc.) +- JWT secret in `user/plugins/api/api.yaml` can stay empty — HMAC-SHA256 works with an empty key locally; production generates its own secure secret +- The old `admin` plugin must be disabled (`enabled: false`) to avoid route conflict with `admin2` + +--- + +## BUG-005 — PHP session fails after Grav 2.0 container upgrade + +**Status:** fixed 2026-06-19 + +### Symptom + +After replacing Grav core files inside the container, all pages return a CRITICAL error in `logs/grav.log`: `Failed to start session: session_start(): Failed to read session data: files (path: )`. Site is inaccessible. + +### Root cause + +The `getgrav/grav` Docker image's PHP configuration does not set `session.save_path`. Grav 1.7 worked because the image's default PHP config included it; the updated image layer did not. + +### Fix + +Add to `php/php-local.ini`: +```ini +session.save_path = /tmp +``` + +Restart the container to pick up the change. This file is bind-mounted into the container so no image rebuild is needed. + +--- diff --git a/docs/posting-pipeline.md b/docs/posting-pipeline.md new file mode 100644 index 0000000..3011af5 --- /dev/null +++ b/docs/posting-pipeline.md @@ -0,0 +1,132 @@ +# Daily Entry Posting Pipeline + +Two ways to create a daily entry: the mobile frontend form at `/post`, or directly from the Grav Admin2 panel. Both produce the same page structure under `user/pages/01.trips//01.dailies/`. + +The active trip is set in `user/config/site.yaml` → `active_trip`. The post form's `pageconfig.parent` in `post-form.md` must be kept in sync with this value. + +--- + +## Frontmatter Reference + +Every entry page (`template: entry`) supports these frontmatter fields: + +| Field | Type | Required | Notes | +|---|---|---|---| +| `title` | string | ✅ | Entry headline | +| `date` | datetime | ✅ | Format: `Y-m-d H:i` (e.g. `2026-06-17 10:00`) | +| `template` | string | ✅ | Always `entry` | +| `published` | bool | ✅ | `true` to show in tracker feed | +| `lat` | string | — | Latitude decimal degrees (e.g. `52.3676`) | +| `lng` | string | — | Longitude decimal degrees (e.g. `4.9041`) | +| `location_city` | string | — | City name shown under the title (e.g. `Kyoto`) | +| `location_country` | string | — | Country name shown under the title (e.g. `Japan`) | +| `weather_desc` | string | — | Condition label — must be one of the values below | +| `weather_temp_c` | number | — | Temperature in Celsius (displayed rounded, e.g. `19`) | +| `hero_image` | string | — | Filename of the hero image (e.g. `photo.jpg`). Leave blank to auto-select the first uploaded image. | + +**`weather_desc` allowed values** (matched to emoji icons in `entry.html.twig`): +`Sunny` · `Partly cloudy` · `Cloudy` · `Foggy` · `Drizzle` · `Rain` · `Snow` · `Thunderstorm` + +**Page media (photos):** images are stored as files in the page folder (`user/pages/01.tracker//`). All images in the folder are shown in the gallery. `hero_image` pins one as the full-width header. + +**Example complete frontmatter:** +```yaml +--- +title: 'First Day in Kyoto' +date: '2026-07-20 09:30' +template: entry +published: true +lat: '35.0116' +lng: '135.7681' +location_city: 'Kyoto' +location_country: 'Japan' +weather_desc: 'Sunny' +weather_temp_c: 28 +hero_image: 'temple.jpg' +--- +``` + +--- + +## Flow 1 — Mobile Frontend Form (`/post`) + +This is the primary posting flow, designed for one-handed phone use. + +``` +Browser → /post (post-form.md) + └─ Grav Form plugin validates fields + └─ add-page-by-form plugin (onFormProcessed) + ├─ reads pageconfig.parent (/trips/japan-korea-2026/dailies) and pageconfig.slug_field (date + title) + ├─ reads pagefrontmatter (template: entry, published: true) + ├─ merges form field values into new page frontmatter + ├─ writes user/pages/01.trips//01.dailies//entry.md + └─ moves uploaded photos into the page folder + └─ cache-on-save plugin (onFormProcessed) + └─ calls $grav['cache']->deleteAll() so tracker feed shows the entry immediately + └─ form shows success message, resets fields +``` + +**The form fields and their mapping to frontmatter:** + +| Form field | Frontmatter key | Notes | +|---|---|---| +| `title` | `title` | Required | +| `date` | `date` | Defaults to current datetime | +| `content` | page body (markdown) | Required | +| `photos` | page media files | Uploaded to page folder | +| `lat` | `lat` | Filled via "Get Location" button | +| `lng` | `lng` | Filled via "Get Location" button | +| `location_city` | `location_city` | Manual text entry | +| `location_country` | `location_country` | Manual text entry | +| `weather_temp_c` | `weather_temp_c` | Hidden — set by weather JS widget | +| `weather_desc` | `weather_desc` | Hidden — set by weather JS widget | + +**Slug format:** `.` (controlled by `slug_field: 'date,title'` in `post-form.md`). + +**Security:** the `/post` page requires `access: site.login: true` — anonymous visitors get redirected to login. + +--- + +## Flow 2 — Admin Panel (sit-down workflow) + +Use this for drafts, scheduled posts, or editing existing entries. + +1. Log in at `/admin` +2. Go to **Pages** → **Add Page** +3. Set: + - **Page Title:** your entry title + - **Parent Page:** `/trips/japan-korea-2026/dailies` (adjust to active trip) + - **Page Template:** `entry` +4. Fill in the **Entry** tab fields (city, country, lat/lng, weather) +5. Write content in the **Content** tab +6. Upload photos via the **Media** tab +7. Set `published: true` (or leave `false` for a draft) +8. For scheduling: set `publish_date` in **Options** → **Scheduling** +9. Save + +The Admin form fields are defined by `user/themes/intotheeast/blueprints/entry.yaml`. + +**Drafts:** set `published: false` — the entry won't appear in the tracker feed until you flip it to `true`. Useful for writing ahead of time on the road. + +**Scheduling:** Grav supports `publish_date` and `unpublish_date` in page frontmatter. Set them in the Admin Options tab. Requires `pages.publish_dates: true` in `system.yaml` (already enabled). + +--- + +## Page folder structure + +``` +user/pages/01.trips/ +└─ japan-korea-2026/ ← trip entity (active_trip in site.yaml) + ├─ trip.md ← trip page (title, date_start, date_end, cover_image, album_url) + ├─ *.gpx ← GPX route files (served as media, rendered on map) + ├─ 01.dailies/ + │ └─ 2026-07-20-1430-first-day-in-kyoto.entry/ + │ ├─ entry.md ← frontmatter + markdown body + │ ├─ temple.jpg ← hero image (referenced by hero_image) + │ └─ market.jpg ← additional gallery image + ├─ 02.map/map.md + ├─ 03.stats/stats.md + └─ 04.stories/stories.md +``` + +The entry folder name follows `-.entry`. Grav uses this for ordering and routing. The `.entry` suffix enables the `entry` template.