diff --git a/.gitignore b/.gitignore index 7913508..0748732 100644 --- a/.gitignore +++ b/.gitignore @@ -3,10 +3,12 @@ # Grav CMS /user/ +!/user/ +!/user/plugins/ +!/user/plugins/cache-on-save/ user/accounts/ user/data/ user/cache/ -user/plugins/ # Claude .claude/ diff --git a/CLAUDE.md b/CLAUDE.md index 3d51baf..3f23182 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -72,11 +72,10 @@ Then run `make setup` (starts Docker + installs plugins). ### After make install-plugins: fix cache permissions -If the site returns a 500 error after plugin installation, the cache/logs/tmp directories may have wrong ownership (gpm runs as root inside the container). Fix with: - -```bash -docker exec intotheeast_grav chown -R abc:users /app/www/public/cache /app/www/public/logs /app/www/public/tmp -``` +If the site returns a 500 error after plugin installation or after recreating the container, +run `make fix-perms`. This creates uid 1000 in the container, chowns `/var/www/html` to 1000:1000, +and reloads Apache. Always run `make setup` (not just `make start`) after `docker compose down && up` +to ensure permissions are correct. ### Language URL prefix diff --git a/Makefile b/Makefile index 6954c24..c9c734f 100644 --- a/Makefile +++ b/Makefile @@ -27,23 +27,38 @@ start: stop: docker compose down -setup: start install-plugins +setup: start install-plugins fix-perms + +fix-perms: + docker exec intotheeast_grav bash -c "getent passwd 1000 > /dev/null || useradd -u 1000 -M hostuser" + docker exec intotheeast_grav chown -R 1000:1000 /var/www/html + docker exec intotheeast_grav apachectl graceful install-plugins: - docker exec -w /app/www/public intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y + docker exec -w /var/www/html intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y # ── Demo content ────────────────────────────────────────────────────────────── demo-load: - cp -r user/docs/demo/tracker/. user/pages/01.tracker/ - docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache" + # Load japan-korea-2026 dailies + cp -r user/docs/demo/trips/japan-korea-2026/dailies/. user/pages/01.trips/japan-korea-2026/01.dailies/ + # Load italy-2025 trip (create pages if absent) + mkdir -p user/pages/01.trips/italy-2025/01.dailies user/pages/01.trips/italy-2025/02.map user/pages/01.trips/italy-2025/03.stats user/pages/01.trips/italy-2025/04.stories + cp user/docs/demo/trips/italy-2025/trip.md user/pages/01.trips/italy-2025/trip.md 2>/dev/null || true + cp user/docs/demo/trips/italy-2025/map.md user/pages/01.trips/italy-2025/02.map/map.md 2>/dev/null || true + cp user/docs/demo/trips/italy-2025/stats.md user/pages/01.trips/italy-2025/03.stats/stats.md 2>/dev/null || true + cp user/docs/demo/trips/italy-2025/stories.md user/pages/01.trips/italy-2025/04.stories/stories.md 2>/dev/null || true + cp -r user/docs/demo/trips/italy-2025/dailies/. user/pages/01.trips/italy-2025/01.dailies/ + cp user/docs/demo/trips/italy-2025/*.gpx user/pages/01.trips/italy-2025/ 2>/dev/null || true + docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache" demo-reset: - @for dir in user/docs/demo/tracker/*/; do \ + @for dir in user/docs/demo/trips/japan-korea-2026/dailies/*/; do \ folder=$$(basename "$$dir"); \ - rm -rf "user/pages/01.tracker/$$folder"; \ + rm -rf "user/pages/01.trips/japan-korea-2026/01.dailies/$$folder"; \ done - docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache" + rm -rf user/pages/01.trips/italy-2025 + docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache" # ── Content sync (user repo ↔ Gitea) ────────────────────────────────────────── diff --git a/docker-compose.yml b/docker-compose.yml index 8a3622a..ab2789c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,14 @@ services: grav: - image: lscr.io/linuxserver/grav:latest + image: getgrav/grav container_name: intotheeast_grav environment: - - PUID=1000 - - PGID=1000 + - GRAV_CHANNEL=beta + - APACHE_RUN_USER=#1000 + - APACHE_RUN_GROUP=#1000 ports: - "8081:80" volumes: - - ./user:/config/www/user - - ./php/php-local.ini:/config/php/php-local.ini + - ./user:/var/www/html/user + - ./php/php-local.ini:/usr/local/etc/php/conf.d/php-local.ini restart: unless-stopped diff --git a/docs/superpowers/plans/2026-06-18-grav2-upgrade.md b/docs/superpowers/plans/2026-06-18-grav2-upgrade.md new file mode 100644 index 0000000..c1c6322 --- /dev/null +++ b/docs/superpowers/plans/2026-06-18-grav2-upgrade.md @@ -0,0 +1,508 @@ +# Grav 2.0 Upgrade Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Upgrade the local dev Docker environment from linuxserver/grav 1.7 to getgrav/grav 2.0 RC, validate the full Milestone 1 posting workflow, and update the production install script for a fresh Grav 2.0 deploy. + +**Architecture:** Two tracks in sequence — (1) swap the Docker image and update all dependent config/paths, boot the site with `make setup`, run the existing test suite; (2) update `server-install.sh` so `make remote-install` deploys Grav 2.0 fresh on the production PHP 8.4 server. The `user/` directory (content, config, theme, custom plugins) is already isolated as a git repo and requires only a small compatibility addition to `cache-on-save`. + +**Tech Stack:** Grav CMS 2.0.0-rc.9, PHP 8.4 (production) / Docker `getgrav/grav` with PHP 8.3 (dev), Apache, Twig 3, Symfony 7, Playwright (UI tests). + +## Global Constraints + +- All work on branch `update-to-2.0` (already created) +- Never read `.env` — contains sensitive credentials +- Only modify files in the project root or `user/` subfolders +- `user/config/system.yaml` is tracked in the **user/ git repo** — commit it with `git -C user add config/system.yaml && git -C user commit ...`, NOT from the main repo +- `user/plugins/cache-on-save/` is tracked in the **main repo** (after adding `.gitignore` exception) — commit blueprints.yaml with `git add user/plugins/cache-on-save/blueprints.yaml` from the project root +- Container name stays `intotheeast_grav`; local port stays `8081` +- `make` commands are the only way to interact with the remote server +- Grav 2.0 requires PHP ≥ 8.3 (dev container uses 8.3 default; production uses 8.4 — both compliant) +- Production download URL format: `https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX}` + +--- + +## Files Changed + +| File | Action | Reason | +|---|---|---| +| `docker-compose.yml` | Modify | Switch image, update volume + PHP ini path, add env var | +| `Makefile` | Modify | Three `docker exec` targets hardcode linuxserver's `/app/www/public` path | +| `.gitignore` | Modify | Add `!user/plugins/cache-on-save/` exception to track the custom plugin in the main repo | +| `user/plugins/cache-on-save/blueprints.yaml` | Create | Grav 2.0 compat flag (required by GPM) — committed to main repo | +| `user/config/system.yaml` | Modify | Switch GPM channel from `stable` to `testing` | +| `scripts/server-install.sh` | Modify | Support `GRAV_CHANNEL_SUFFIX` for `?testing` query param on 2.0 RC download | + +--- + +## Task 1: Swap Docker image and fix container paths + +**Files:** +- Modify: `docker-compose.yml` +- Modify: `Makefile` + +**Interfaces:** +- Produces: A running Grav 2.0 container reachable at `http://localhost:8081` with `user/` mounted at `/var/www/html/user` and PHP upload limits applied via `/usr/local/etc/php/conf.d/php-local.ini` + +- [ ] **Step 1: Stop and remove the current container** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +docker compose down +``` + +Expected: container `intotheeast_grav` stops and is removed. + +- [ ] **Step 2: Update `docker-compose.yml`** + +Replace the entire contents of `docker-compose.yml` with: + +```yaml +services: + grav: + image: getgrav/grav + container_name: intotheeast_grav + environment: + - GRAV_CHANNEL=beta + ports: + - "8081:80" + volumes: + - ./user:/var/www/html/user + - ./php/php-local.ini:/usr/local/etc/php/conf.d/php-local.ini + restart: unless-stopped +``` + +Key changes from old file: +- `image`: `lscr.io/linuxserver/grav:latest` → `getgrav/grav` +- `environment`: removed `PUID`/`PGID` (linuxserver-specific), added `GRAV_CHANNEL=beta` +- `volumes[0]`: `/config/www/user` → `/var/www/html/user` +- `volumes[1]`: `/config/php/php-local.ini` → `/usr/local/etc/php/conf.d/php-local.ini` + +- [ ] **Step 3: Update Makefile — three targets use the old container path** + +In `Makefile`, make these three targeted replacements: + +**`install-plugins` target** — change working directory flag: + +Old: +```makefile +install-plugins: + docker exec -w /app/www/public intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y +``` + +New: +```makefile +install-plugins: + docker exec -w /var/www/html intotheeast_grav php bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y +``` + +**`demo-load` target** — change cache clear path: + +Old: +```makefile +demo-load: + cp -r user/docs/demo/tracker/. user/pages/01.tracker/ + docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache" +``` + +New: +```makefile +demo-load: + cp -r user/docs/demo/tracker/. user/pages/01.tracker/ + docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache" +``` + +**`demo-reset` target** — change cache clear path: + +Old: +```makefile +demo-reset: + @for dir in user/docs/demo/tracker/*/; do \ + folder=$$(basename "$$dir"); \ + rm -rf "user/pages/01.tracker/$$folder"; \ + done + docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache" +``` + +New: +```makefile +demo-reset: + @for dir in user/docs/demo/tracker/*/; do \ + folder=$$(basename "$$dir"); \ + rm -rf "user/pages/01.tracker/$$folder"; \ + done + docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache" +``` + +- [ ] **Step 4: Validate docker-compose syntax** + +```bash +docker compose config +``` + +Expected: prints merged compose config with no errors. If you see `Error`, re-check the YAML indentation in `docker-compose.yml`. + +- [ ] **Step 5: Commit** + +```bash +git add docker-compose.yml Makefile +git commit -m "feat: switch to getgrav/grav 2.0 RC docker image + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +## Task 2: Add Grav 2.0 compat flag and switch GPM to testing channel + +**Files:** +- Modify: `.gitignore` (add exception for `user/plugins/cache-on-save/`) +- Create: `user/plugins/cache-on-save/blueprints.yaml` (committed to main repo) +- Modify: `user/config/system.yaml` (committed to user/ git repo, not main repo) + +**Interfaces:** +- Consumes: Running container from Task 1 +- Produces: GPM resolves 2.0-compatible plugin versions on install; `cache-on-save` is recognized as 2.0-compatible by Grav's plugin registry + +- [ ] **Step 1: Create `user/plugins/cache-on-save/blueprints.yaml`** + +Create the file with this exact content: + +```yaml +name: Cache On Save +version: 1.0.0 +description: Clears Grav cache on new-entry form submission +author: + name: Mischa + email: mischa@gorinskat.nl +license: MIT + +dependencies: + - { name: grav, version: '>=1.6.0' } + +grav: + version: ['1.7', '2.0'] +``` + +- [ ] **Step 2: Update GPM channel in `user/config/system.yaml`** + +Find the `gpm:` section (around line 200 in the file) and change `releases: stable` to `releases: testing`: + +Old: +```yaml +gpm: + releases: stable + official_gpm_only: true +``` + +New: +```yaml +gpm: + releases: testing + official_gpm_only: true +``` + +- [ ] **Step 3: Add gitignore exception and commit blueprints.yaml to main repo** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast + +# Add exception so cache-on-save is tracked in the main repo +# Insert after the existing "user/plugins/" line in .gitignore: +# !user/plugins/cache-on-save/ + +# Then commit to the main repo: +git add .gitignore user/plugins/cache-on-save/blueprints.yaml +git commit -m "feat: track cache-on-save plugin in main repo; add Grav 2.0 compat flag + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +- [ ] **Step 4: Commit system.yaml to the user/ git repo** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +git -C user add config/system.yaml +git -C user commit -m "feat: switch GPM to testing channel for Grav 2.0 + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +## Task 3: Boot Grav 2.0 and install plugins + +**Files:** None (runtime only) + +**Interfaces:** +- Consumes: docker-compose.yml from Task 1, GPM config from Task 2 +- Produces: Running Grav 2.0 instance at `http://localhost:8081` with all plugins installed + +- [ ] **Step 1: Run setup** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +make setup +``` + +This starts the container and installs all plugins from `plugins.txt`. First run may take 1-2 minutes as `getgrav/grav` downloads and extracts Grav 2.0 RC. + +Expected output ends with something like: +``` +GPM Packages Installed: admin, email, error, form, login, problems, add-page-by-form, shortcode-gallery-plusplus +``` + +If `make setup` fails on plugin install with a permission error, fix with: +```bash +docker exec intotheeast_grav chown -R www-data:www-data /var/www/html/cache /var/www/html/logs /var/www/html/tmp +make install-plugins +``` + +- [ ] **Step 2: Verify PHP upload limits are applied** + +```bash +docker exec intotheeast_grav php -r "echo ini_get('upload_max_filesize') . ' / ' . ini_get('post_max_size');" +``` + +Expected: `100M / 500M` + +If you see `2M / 8M` (PHP defaults), the ini mount path is wrong. Verify with: +```bash +docker exec intotheeast_grav php -r "echo php_ini_scanned_files();" +``` +It should include `/usr/local/etc/php/conf.d/php-local.ini`. + +- [ ] **Step 3: Verify site loads** + +```bash +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/ +``` + +Expected: `200` + +If you get `500`, check container logs: +```bash +docker logs intotheeast_grav --tail 50 +``` + +- [ ] **Step 4: Verify Admin2 loads** + +```bash +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/admin +``` + +Expected: `200` (Admin2 SPA login page, not the old Twig admin) + +- [ ] **Step 5: Run config and HTTP tests** + +```bash +make test-config +make test-post +``` + +`test-config` validates the form YAML config. `test-post` submits the posting form via HTTP and checks an entry is created. + +Expected: both exit 0. + +If `test-post` fails, check the output of: +```bash +bash scripts/test-post.sh +``` +This is the critical `add-page-by-form` go/no-go test. If it fails with a 500 or the entry isn't created, see the **If add-page-by-form fails** section at the bottom of this plan. + +- [ ] **Step 6: Commit task completion note** + +No new files to commit. Move to Task 4. + +--- + +## Task 4: Run Playwright test suite and fix any Admin2 regressions + +**Files:** +- Modify: `tests/*.spec.js` (only if tests fail due to Admin2 DOM changes) + +**Interfaces:** +- Consumes: Running Grav 2.0 from Task 3 +- Produces: All Playwright tests passing (or updated for Admin2's new DOM) + +- [ ] **Step 1: Run the full UI test suite** + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +make test-ui +``` + +Expected: 25 tests pass. + +- [ ] **Step 2: If any tests fail, classify the failure** + +For each failing test, determine whether it is: + +**A) A genuine regression** (e.g., posting form broken, tracker page missing entries, gallery not rendering) — these are blockers. Stop, investigate the root cause, and fix the underlying Grav/plugin issue before updating the test. + +**B) An Admin2 DOM change** (e.g., selectors targeting old admin HTML structure like `.admin-menu`, `.grav-nav`, admin-specific CSS classes) — these are acceptable test updates. Update the selector in the test file to match Admin2's new HTML. + +To inspect the current Admin2 DOM for a failing selector: +```bash +# Check what the admin page actually renders +curl -s http://localhost:8081/admin | grep -o '<[^>]*class="[^"]*admin[^"]*"[^>]*>' | head -20 +``` + +- [ ] **Step 3: Update any Admin2 selector regressions** + +For each type-(B) failure, open the relevant test file in `tests/` and update the selector. Example pattern for updating an admin navigation selector: + +Old (targeting classic admin): +```js +await page.click('.grav-nav-toggle') +``` + +New (targeting Admin2 SPA — find actual selector from step 2's output): +```js +await page.click('[data-testid="nav-toggle"]') // replace with actual Admin2 selector +``` + +After each fix, re-run just that test: +```bash +npx playwright test tests/.spec.js --headed +``` + +- [ ] **Step 4: Re-run full suite to confirm all pass** + +```bash +make test-ui +``` + +Expected: all tests pass. + +- [ ] **Step 5: Commit any test updates** + +If any test files were modified: +```bash +git add tests/ +git commit -m "test: update Playwright selectors for Admin2 DOM + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +If no test files changed, no commit needed. + +--- + +## Task 5: Update production install script for Grav 2.0 + +**Files:** +- Modify: `scripts/server-install.sh` + +**Interfaces:** +- Consumes: Nothing from prior tasks (independent of Docker) +- Produces: `make remote-install` deploys a fresh Grav 2.0 on the production PHP 8.4 server when `GRAV_VERSION=2.0.0-rc.9` and `GRAV_CHANNEL_SUFFIX=?testing` are set in `.env` + +- [ ] **Step 1: Update the wget download line in `scripts/server-install.sh`** + +The script currently downloads Grav with: +```bash +wget --no-verbose "https://getgrav.org/download/core/grav-admin/$GRAV_VERSION" -O grav-admin.zip +``` + +Grav 2.0 RC requires `?testing` appended to the URL. Add `GRAV_CHANNEL_SUFFIX` support: + +Old (line ~15 in the file): +```bash +echo "==> Downloading Grav $GRAV_VERSION" +cd "$WEBROOT" +wget --no-verbose "https://getgrav.org/download/core/grav-admin/$GRAV_VERSION" -O grav-admin.zip +``` + +New: +```bash +echo "==> Downloading Grav $GRAV_VERSION" +cd "$WEBROOT" +wget --no-verbose "https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX:-}" -O grav-admin.zip +``` + +The `${GRAV_CHANNEL_SUFFIX:-}` expands to empty string if unset, keeping stable releases working without any changes to `.env`. + +- [ ] **Step 2: Add GRAV_CHANNEL_SUFFIX to the env var validation block** + +At the top of the script the required vars are validated. `GRAV_CHANNEL_SUFFIX` is optional, so do NOT add it to the `:?` required list. Instead, add a comment above the download step: + +After the `set -e` and required var block, add a comment before the download line: + +```bash +# GRAV_CHANNEL_SUFFIX: optional, set to '?testing' for RC/beta releases (e.g. 2.0.0-rc.9) +# Leave unset or empty for stable releases. +``` + +- [ ] **Step 3: Verify the script logic looks correct** + +```bash +# Dry-run: simulate what the URL would be with 2.0 RC vars +GRAV_VERSION=2.0.0-rc.9 GRAV_CHANNEL_SUFFIX='?testing' bash -c \ + 'echo "https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX:-}"' +``` + +Expected output: +``` +https://getgrav.org/download/core/grav-admin/2.0.0-rc.9?testing +``` + +```bash +# Dry-run: simulate stable release (no suffix) +GRAV_VERSION=1.7.53 bash -c \ + 'echo "https://getgrav.org/download/core/grav-admin/${GRAV_VERSION}${GRAV_CHANNEL_SUFFIX:-}"' +``` + +Expected output: +``` +https://getgrav.org/download/core/grav-admin/1.7.53 +``` + +- [ ] **Step 4: Commit** + +```bash +git add scripts/server-install.sh +git commit -m "feat: support GRAV_CHANNEL_SUFFIX for Grav 2.0 RC production install + +Co-Authored-By: Claude Sonnet 4.6 " +``` + +--- + +## If `add-page-by-form` fails (contingency) + +If `make test-post` in Task 3 step 5 returns a non-zero exit code or the entry is not created, `add-page-by-form` is incompatible with Grav 2.0. The fallback is to write a custom replacement plugin. + +**Do not proceed to Task 4 if the posting workflow is broken.** Instead: + +1. Check the container logs for the specific error: +```bash +docker logs intotheeast_grav --tail 100 | grep -i "error\|exception\|warning" +``` + +2. Note the error, stop work, and report back. The custom replacement plugin is a separate task requiring design input from the project owner before implementation. + +The custom plugin would: +- Hook `onFormProcessed` (same as `cache-on-save`) +- Read form field values (`title`, `content`, `photo`) +- Build the page path under `user/pages/01.tracker/` +- Write the page file to disk using `Grav\Common\Page\Page` +- Merge `cache-on-save` functionality (call `$this->grav['cache']->deleteAll()`) +- Replace both `add-page-by-form` and `cache-on-save` with a single plugin + +This is ~200 lines of PHP and ~1 day of work. It should be planned separately. + +--- + +## Final smoke test (after all tasks complete) + +Run the full test suite one last time: + +```bash +cd /home/mischa/Nextcloud/Projects/travel-blog-intotheeast +make test +``` + +Expected: all three suites (`test-config`, `test-post`, `test-ui`) exit 0. + +Then verify the go/no-go criteria from the spec are all met before merging to `main` or deploying to production. diff --git a/docs/superpowers/plans/2026-06-19-dark-mode.md b/docs/superpowers/plans/2026-06-19-dark-mode.md new file mode 100644 index 0000000..cf48bea --- /dev/null +++ b/docs/superpowers/plans/2026-06-19-dark-mode.md @@ -0,0 +1,316 @@ +# Dark Mode & Visual Polish Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the warm-paper light theme with a warm-dark "notebook at night" aesthetic — dark-only, no toggle, paper grain texture, dark terrain map tiles, typography polish. + +**Architecture:** Pure CSS token swap in `tokens.css` (all components update automatically), grain overlay via `body::after` SVG data URI in `style.css`, map tile URL swap in two Twig templates. No new dependencies, no JS changes, no structural changes. + +**Tech Stack:** CSS custom properties, inline SVG noise filter, Stadia Maps Alidade Smooth Dark tile CDN, Leaflet.js (already present) + +## Global Constraints + +- All changes in `user/` — commit with `git -C user`, not main-repo git +- Dark-only — no `prefers-color-scheme` media query, no light-mode fallback, no toggle +- Existing token names in `tokens.css` must not change — only values swap +- No new npm/JS dependencies +- `make test-ui` must pass after every task (pre-existing P2 FilePond failure is acceptable) +- Stadia Maps tile URL: `https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png` +- Stadia attribution (exact): `© Stadia Maps © OpenMapTiles © OpenStreetMap contributors` + +--- + +### Task 1: Dark color tokens + +**Files:** +- Modify: `user/themes/intotheeast/css/tokens.css` + +**Interfaces:** +- Produces: CSS custom properties consumed by every component in `style.css` and Twig templates + +- [ ] **Step 1: Read the current tokens file** + +```bash +cat user/themes/intotheeast/css/tokens.css +``` + +Confirm these token names exist before editing: `--color-paper`, `--color-canvas`, `--color-ink`, `--color-ink-2`, `--color-ink-muted`, `--color-border`, `--color-border-soft`, `--color-accent`, `--color-accent-hover`, `--color-accent-light`, `--color-accent-on`. + +- [ ] **Step 2: Replace the color block in tokens.css** + +Replace the entire `:root` color block (from `--color-paper` through `--color-accent-on`) with: + +```css + /* ── Dark palette (warm notebook) ──────────────────────────────────────── */ + --color-paper: #1A1814; /* page background — warm near-black */ + --color-canvas: #22201B; /* card surfaces, form backgrounds */ + --color-ink: #EDE8DF; /* primary text — warm cream */ + --color-ink-2: #B8B0A4; /* body text — muted warm */ + --color-ink-muted: #7A7268; /* labels, timestamps, captions */ + --color-border: #2E2B25; /* standard dividers */ + --color-border-soft: #252219; /* subtle dividers */ + --color-accent: #2A8C73; /* teal — lightened for dark contrast */ + --color-accent-hover: #236655; /* hover/pressed teal */ + --color-accent-light: #1A2E29; /* pale teal tint backgrounds */ + --color-accent-on: #FFFFFF; /* text on accent surfaces */ + --color-surface-raised: #2A2720; /* elevated surfaces: tooltips, hover */ + --color-ink-inverse: #17171A; /* text on accent-coloured buttons */ +``` + +Keep all non-color tokens (`--text-*`, `--leading-*`, `--space-*`, font variables, etc.) unchanged. + +- [ ] **Step 3: Verify no syntax errors** + +```bash +docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache" && curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/dailies +``` + +Expected: `200` + +- [ ] **Step 4: Visual smoke check** + +```bash +curl -s http://localhost:8081/trips/japan-korea-2026/dailies | grep -o 'color: var(--color-paper)' | head -3 +``` + +Not a definitive check — just confirm the page renders. Open a browser and verify the background is dark and text is cream. + +- [ ] **Step 5: Run test suite** + +```bash +make test-ui +``` + +Expected: 24/25 pass (P2 FilePond is pre-existing failure, all others pass). + +- [ ] **Step 6: Commit** + +```bash +git -C user add themes/intotheeast/css/tokens.css +git -C user commit -m "feat: switch to warm-dark color tokens" +``` + +--- + +### Task 2: Paper grain texture + hardcoded color fixes + typography + +**Files:** +- Modify: `user/themes/intotheeast/css/style.css` + +**Interfaces:** +- Consumes: dark color tokens from Task 1 + +- [ ] **Step 1: Find all hardcoded color literals in style.css** + +```bash +grep -n '#[0-9a-fA-F]\{3,6\}\|background: white\|background:#fff\|color: #\|background-color: #' user/themes/intotheeast/css/style.css +``` + +Make note of every hit — each one is a candidate to replace with a token. Exceptions: the CSS SVG data URI you are about to add (the noise filter hex values are part of the graphic, not UI colors). + +- [ ] **Step 2: Add paper grain texture to body** + +Find the `body` rule in `style.css`. It will look something like: + +```css +body { + background-color: var(--color-paper); + color: var(--color-ink); + ... +} +``` + +Add a `body::after` rule immediately after the `body` rule: + +```css +body::after { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 9998; + opacity: 0.035; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23noise)' opacity='1'/%3E%3C/svg%3E"); + background-repeat: repeat; + background-size: 200px 200px; +} +``` + +- [ ] **Step 3: Fix hardcoded login form colors** + +Find this rule (around line 497): + +```css +.login-form .button.secondary { background: #f0f0f0; color: #333; text-decoration: none; line-height: 44px; padding: 0 1rem; } +``` + +Replace with: + +```css +.login-form .button.secondary { background: var(--color-canvas); color: var(--color-ink); text-decoration: none; line-height: 44px; padding: 0 1rem; } +``` + +- [ ] **Step 4: Fix any other hardcoded colors found in Step 1** + +For each hardcoded literal found in Step 1 (excluding the data URI you added): +- `#fff` / `white` → `var(--color-canvas)` (if a surface) or `var(--color-paper)` (if a page background) +- `#333` / dark grays → `var(--color-ink)` or `var(--color-ink-2)` +- `#eee` / light grays → `var(--color-border)` or `var(--color-border-soft)` +- `#f0f0f0` / near-white → `var(--color-canvas)` + +Use judgment: if a hex is inside a gradient or SVG path data, leave it alone. + +- [ ] **Step 5: Typography — increase entry body paragraph spacing** + +Find: + +```css +.entry-body p { margin-bottom: 1.1em; font-size: var(--text-md); line-height: var(--leading-normal); color: var(--color-ink-2); } +``` + +Change `margin-bottom: 1.1em` to `margin-bottom: 1.4em`. + +- [ ] **Step 6: Typography — tighten h1/h2 tracking** + +Find the `h1` and `h2` rules. Any rule that applies `letter-spacing: -0.01em` to an `h1` or `h2` — change it to `-0.02em`. Do not touch h3/h4/h5/h6. + +- [ ] **Step 7: Stats page — tabular numbers** + +Find any CSS rule targeting stats numbers (look for `.stat-value`, `.stats-number`, or similar). Add `font-variant-numeric: tabular-nums` to it. If no such specific rule exists, search the template: + +```bash +grep -n 'stat\|number\|count' user/themes/intotheeast/templates/stats.html.twig | head -20 +``` + +Then add a targeted rule in style.css for whatever class wraps the numeric values. + +- [ ] **Step 8: Verify no syntax errors and visual check** + +```bash +docker exec intotheeast_grav bash -c "cd /var/www/html && php bin/grav clearcache" && curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/dailies +``` + +Expected: `200`. Open browser — grain should be subtly visible on the dark background. + +- [ ] **Step 9: Run test suite** + +```bash +make test-ui +``` + +Expected: 24/25 (P2 pre-existing). + +- [ ] **Step 10: Commit** + +```bash +git -C user add themes/intotheeast/css/style.css +git -C user commit -m "feat: add paper grain texture, fix hardcoded colors, improve typography" +``` + +--- + +### Task 3: Dark terrain map tiles + +**Files:** +- Modify: `user/themes/intotheeast/templates/map.html.twig` +- Modify: `user/themes/intotheeast/templates/dailies.html.twig` + +**Interfaces:** +- Consumes: Leaflet.js already loaded in both templates +- Produces: Stadia Alidade Smooth Dark tiles replacing OpenStreetMap tiles in both map views + +- [ ] **Step 1: Read current tile setup in both templates** + +```bash +grep -n "tileLayer\|openstreetmap\|attribution\|stadia" user/themes/intotheeast/templates/map.html.twig user/themes/intotheeast/templates/dailies.html.twig +``` + +Confirm the current tile URL pattern (`{s}.tile.openstreetmap.org`) in both files. + +- [ ] **Step 2: Replace tile layer in map.html.twig** + +Find: + +```javascript +L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap contributors' +}).addTo(map); +``` + +Replace with: + +```javascript +// TODO: add Stadia API key before launch — free dev use requires no key, production does +L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.png', { + maxZoom: 20, + attribution: '© Stadia Maps © OpenMapTiles © OpenStreetMap contributors' +}).addTo(map); +``` + +- [ ] **Step 3: Replace tile layer in dailies.html.twig (mini-map)** + +Apply the identical tile swap to the mini-map `L.tileLayer` call in `dailies.html.twig`. Find the OpenStreetMap tile URL and replace it with the Stadia dark URL (same as Step 2, same attribution, same TODO comment). + +- [ ] **Step 4: Verify tiles load** + +```bash +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/map +``` + +Expected: `200`. + +Check the tile URL is in the HTML: + +```bash +curl -s http://localhost:8081/trips/japan-korea-2026/map | grep -o 'stadiamaps' +``` + +Expected: `stadiamaps` (appears in the tile URL). + +Open the map in a browser and confirm: +- Dark terrain tiles render (not the default light OSM tiles) +- GPX polyline is visible in teal on the dark background +- Entry pins render correctly on top +- Attribution footer is present + +- [ ] **Step 5: Check mini-map on dailies page** + +```bash +curl -s http://localhost:8081/trips/japan-korea-2026/dailies | grep -o 'stadiamaps' +``` + +Expected: `stadiamaps`. + +- [ ] **Step 6: Run test suite** + +```bash +make test-ui +``` + +Expected: 24/25 (P2 pre-existing). + +- [ ] **Step 7: Commit** + +```bash +git -C user add themes/intotheeast/templates/map.html.twig themes/intotheeast/templates/dailies.html.twig +git -C user commit -m "feat: switch to Stadia Alidade Smooth Dark map tiles" +``` + +--- + +## Final verification + +After all 3 tasks: + +1. `make test-config && make test-post && make test-ui` — all pass +2. Visual check list (browser, not curl): + - `/trips/japan-korea-2026/dailies` — dark warm background, cream text, grain visible, teal accents + - `/trips/japan-korea-2026/map` — dark terrain tiles, teal GPX polyline, entry pins + - `/trips/japan-korea-2026/dailies/` — dark canvas card, no white boxes + - `/post` — form fields readable, no black-on-black inputs + - `/trips/japan-korea-2026/stats` — numbers align (tabular-nums) +3. Final hardcoded-literal check: + ```bash + grep -n '#[0-9a-fA-F]\{3,6\}' user/themes/intotheeast/css/style.css | grep -v 'data:image' + ``` + All remaining hits should be either intentional (e.g. SVG path data) or documented. diff --git a/docs/superpowers/plans/2026-06-19-trip-entity.md b/docs/superpowers/plans/2026-06-19-trip-entity.md new file mode 100644 index 0000000..007e21b --- /dev/null +++ b/docs/superpowers/plans/2026-06-19-trip-entity.md @@ -0,0 +1,538 @@ +# Trip Entity Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` to implement this plan task-by-task. + +**Goal:** Restructure the site around a Trip entity — tracker/map/stats/stories become children of `/trips/japan-korea-2026/`, GPX route files live as media on the trip page, and `site.yaml` holds an `active_trip` slug so the nav can switch trips via config. + +**Architecture:** Trip = a Grav page (`trip.html.twig`) at `/trips//`. Map/stats templates find the tracker via `page.parent().route ~ '/tracker'` instead of the hardcoded `/tracker` path. Leaflet-gpx (CDN) loads all `*.gpx` media files from the trip page. A `trips.html.twig` listing page provides the multi-trip root. Stories is stubbed with a placeholder template. + +**Tech Stack:** Grav CMS 1.7/2.0, Twig, Leaflet.js, leaflet-gpx (CDN, vanilla JS — consistent with existing inline JS pattern) + +## Global Constraints + +- All content/theme edits go in `user/` — commit with `git -C user`, not main-repo git +- Entry URLs change: `/tracker/` → `/trips/japan-korea-2026/tracker/` — acceptable pre-launch +- `make test-post` (6/6) and `make test-ui` (25/25) must pass after every task +- No new JS framework dependencies; leaflet-gpx is 3KB vanilla JS +- `user/config/media.yaml` must whitelist `.gpx` so Grav serves it as a file +- The `02.post/post-form.md` `pageconfig.parent` must stay in sync with the tracker path + +--- + +### Task 1: Restructure pages under `/trips/` + +**Files:** +- Create: `user/pages/01.trips/trips.md` +- Create: `user/pages/01.trips/japan-korea-2026/trip.md` +- Create: `user/pages/01.trips/japan-korea-2026/01.tracker/tracker.md` (copy from `user/pages/01.tracker/tracker.md`, no content change) +- Move: all `*.entry/` folders from `user/pages/01.tracker/` → `user/pages/01.trips/japan-korea-2026/01.tracker/` +- Create: `user/pages/01.trips/japan-korea-2026/02.map/map.md` (copy from `user/pages/03.map/map.md`) +- Create: `user/pages/01.trips/japan-korea-2026/03.stats/stats.md` (copy from `user/pages/04.stats/stats.md`) +- Create: `user/pages/01.trips/japan-korea-2026/04.stories/stories.md` +- Delete: `user/pages/01.tracker/`, `user/pages/03.map/`, `user/pages/04.stats/` +- Modify: `user/config/site.yaml` — add `active_trip: japan-korea-2026` +- Modify (create if absent): `user/config/media.yaml` — whitelist GPX + +- [ ] **Step 1: Verify current structure before touching anything** + +```bash +find user/pages -name "*.md" | sort +``` +Expected: entries under `01.tracker/`, map at `03.map/map.md`, stats at `04.stats/stats.md`. + +- [ ] **Step 2: Create trips hierarchy** + +```bash +mkdir -p user/pages/01.trips/japan-korea-2026/01.tracker +mkdir -p user/pages/01.trips/japan-korea-2026/02.map +mkdir -p user/pages/01.trips/japan-korea-2026/03.stats +mkdir -p user/pages/01.trips/japan-korea-2026/04.stories +``` + +- [ ] **Step 3: Write `trips.md`** + +`user/pages/01.trips/trips.md`: +```yaml +--- +title: Trips +template: trips +content: + items: '@self.children' + order: + by: date + dir: desc +--- +``` + +- [ ] **Step 4: Write `trip.md`** + +`user/pages/01.trips/japan-korea-2026/trip.md`: +```yaml +--- +title: 'Japan & Korea 2026' +template: trip +date: '2026-06-17' +date_start: '2026-06-17' +date_end: '' +cover_image: '' +content: + items: '@self.children' +--- +``` + +- [ ] **Step 5: Copy tracker.md, move entries** + +```bash +cp user/pages/01.tracker/tracker.md user/pages/01.trips/japan-korea-2026/01.tracker/tracker.md +mv user/pages/01.tracker/*.entry user/pages/01.trips/japan-korea-2026/01.tracker/ +``` + +- [ ] **Step 6: Copy map.md and stats.md** + +```bash +cp user/pages/03.map/map.md user/pages/01.trips/japan-korea-2026/02.map/map.md +cp user/pages/04.stats/stats.md user/pages/01.trips/japan-korea-2026/03.stats/stats.md +``` + +- [ ] **Step 7: Write stories stub** + +`user/pages/01.trips/japan-korea-2026/04.stories/stories.md`: +```yaml +--- +title: Stories +template: stories +published: true +--- +``` + +- [ ] **Step 8: Delete old top-level pages** + +```bash +rm -rf user/pages/01.tracker user/pages/03.map user/pages/04.stats +``` + +- [ ] **Step 9: Add `active_trip` to site.yaml** + +Add to `user/config/site.yaml`: +```yaml +active_trip: japan-korea-2026 +``` + +- [ ] **Step 10: Whitelist GPX in media.yaml** + +`user/config/media.yaml` (create if absent): +```yaml +gpx: + type: file + extensions: ['gpx'] + mime: application/gpx+xml +``` + +- [ ] **Step 11: Verify pages load at new URLs** + +```bash +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/tracker +# Expected: 200 +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/map +# Expected: 200 +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/stats +# Expected: 200 +``` + +- [ ] **Step 12: Commit** + +```bash +git -C user add pages/01.trips config/site.yaml config/media.yaml +git -C user rm -r --cached pages/01.tracker pages/03.map pages/04.stats +git -C user commit -m "feat: restructure pages under trips/japan-korea-2026 entity" +``` + +--- + +### Task 2: Update templates for trip-relative paths + new trip/trips/stories templates + +**Files:** +- Modify: `user/themes/intotheeast/templates/map.html.twig` — change hardcoded `/tracker` path +- Modify: `user/themes/intotheeast/templates/stats.html.twig` — same +- Modify: `user/themes/intotheeast/templates/partials/base.html.twig` — nav uses `active_trip` +- Create: `user/themes/intotheeast/templates/trip.html.twig` +- Create: `user/themes/intotheeast/templates/trips.html.twig` +- Create: `user/themes/intotheeast/templates/stories.html.twig` + +**Interfaces:** +- Consumes: `config.site.active_trip` from site.yaml (set in Task 1) +- Produces: map/stats find entries via `page.parent().route ~ '/tracker'` + +- [ ] **Step 1: Fix `map.html.twig` — tracker path** + +Replace: +```twig +{% set tracker_page = grav.pages.find('/tracker') %} +{% set all_entries = tracker_page ? tracker_page.children.published() : [] %} +``` +With: +```twig +{% set tracker_page = grav.pages.find(page.parent().route ~ '/tracker') %} +{% set all_entries = tracker_page ? tracker_page.children.published() : [] %} +``` + +- [ ] **Step 2: Fix `stats.html.twig` — tracker path** + +Same replacement as Step 1 (identical pattern in stats.html.twig). + +- [ ] **Step 3: Update `base.html.twig` nav** + +Replace hardcoded nav href values with `active_trip`-driven paths. The pattern in base.html.twig currently sets hrefs to `/tracker`, `/map`, `/stats`. Replace with: + +```twig +{% set active_trip = config.site.active_trip %} +{% set trip_base = '/trips/' ~ active_trip %} +``` + +Nav links become: +- Journal: `{{ trip_base }}/tracker` +- Map: `{{ trip_base }}/map` +- Stats: `{{ trip_base }}/stats` + +Active state detection: replace `page.url starts with '/tracker'` checks with `page.url starts with trip_base ~ '/tracker'` (and similarly for map/stats). + +- [ ] **Step 4: Create `trip.html.twig`** + +`user/themes/intotheeast/templates/trip.html.twig`: +```twig +{% extends 'partials/base.html.twig' %} + +{% block content %} +{% set tracker_page = grav.pages.find(page.route ~ '/tracker') %} +{% set entries = tracker_page ? tracker_page.children.published() : [] %} + +
+

{{ page.title }}

+ {% if page.header.date_start %} +

+ {{ page.header.date_start|date('d M Y') }} + {% if page.header.date_end %} — {{ page.header.date_end|date('d M Y') }}{% endif %} +

+ {% endif %} +
+ + + +{% if entries|length > 0 %} +
+

Recent entries

+ {% for entry in entries|slice(0, 3) %} + + {{ entry.date|date('d M Y') }} + {{ entry.title }} + {% if entry.header.location_city %} · {{ entry.header.location_city }}{% endif %} + + {% endfor %} +
+{% endif %} +{% endblock %} +``` + +- [ ] **Step 5: Create `trips.html.twig`** + +`user/themes/intotheeast/templates/trips.html.twig`: +```twig +{% extends 'partials/base.html.twig' %} + +{% block content %} +

{{ page.title }}

+{% set trips = page.children.published() %} +{% if trips|length == 0 %} +

No trips yet.

+{% else %} + +{% endif %} +{% endblock %} +``` + +- [ ] **Step 6: Create `stories.html.twig` stub** + +`user/themes/intotheeast/templates/stories.html.twig`: +```twig +{% extends 'partials/base.html.twig' %} + +{% block content %} +

{{ page.title }}

+

Stories coming soon.

+{% endblock %} +``` + +- [ ] **Step 7: Verify templates render** + +```bash +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026 +# Expected: 200 +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips +# Expected: 200 +curl -s -o /dev/null -w "%{http_code}" http://localhost:8081/trips/japan-korea-2026/stories +# Expected: 200 +``` + +Check nav links resolve correctly on tracker/map/stats pages. + +- [ ] **Step 8: Commit** + +```bash +git -C user add themes/intotheeast/templates/ +git -C user commit -m "feat: add trip/trips/stories templates, update nav and map/stats to use trip-relative paths" +``` + +--- + +### Task 3: Add GPX route support to map template + +**Files:** +- Modify: `user/themes/intotheeast/templates/map.html.twig` + +**Interfaces:** +- Consumes: `*.gpx` files uploaded as media to the trip page (`page.parent()`) +- Produces: GPX tracks rendered as colored polylines on the Leaflet map, underneath entry pins + +- [ ] **Step 1: Add leaflet-gpx script tag** + +In `map.html.twig`, after the existing Leaflet script tag, add: +```html + +``` + +- [ ] **Step 2: Collect GPX URLs from trip media** + +After the `{% set trip_page = page.parent() %}` line (add this at the top of the template, alongside the tracker_page lookup), add: + +```twig +{% set gpx_urls = [] %} +{% for name, media in trip_page.media.all %} + {% if name|split('.')|last == 'gpx' %} + {% set gpx_urls = gpx_urls|merge([media.url]) %} + {% endif %} +{% endfor %} +``` + +- [ ] **Step 3: Pass GPX URLs to JavaScript** + +In the `