From 94bfc53b9048929119c77b2674201bef5f10adbf Mon Sep 17 00:00:00 2001 From: Mischa Date: Thu, 9 Jul 2026 17:59:05 +0200 Subject: [PATCH] docs(working): overnight pre-trip readiness audit + product ideation report Audit: posting pipeline / auth / API surface review with prioritized P1-P3 findings and a morning checklist; P1-1 (prod 2M upload limit) marked resolved 2026-07-09 via the CGI->FPM switch in Webmin. Ideation: 7 ranked, repo-grounded ideas (top pick: OG meta + RSS follow-along stack). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0195b3cDdMeize2Mm1FgC2aU --- .../2026-07-08-travel-blog-ideation.md | 142 +++++++++++++++ .../qa/2026-07-08-pre-trip-readiness-audit.md | 170 ++++++++++++++++++ 2 files changed, 312 insertions(+) create mode 100644 docs/working/2026-07-08-travel-blog-ideation.md create mode 100644 docs/working/qa/2026-07-08-pre-trip-readiness-audit.md diff --git a/docs/working/2026-07-08-travel-blog-ideation.md b/docs/working/2026-07-08-travel-blog-ideation.md new file mode 100644 index 0000000..1d6373e --- /dev/null +++ b/docs/working/2026-07-08-travel-blog-ideation.md @@ -0,0 +1,142 @@ +--- +date: 2026-07-08 +topic: travel-blog-reader-experience-and-road-workflow +focus: reader experience, story mode, on-the-road posting workflow — ahead of Denmark 2026 (departing ~mid-July) +mode: repo-grounded +--- + +# Ideation: Reader Experience, Story Mode & the Road Workflow + +## Grounding Context + +**Codebase context.** Grav 2.0.7 blog structured around Trips → Entries/Stories (CONCEPTS.md). Posting pipeline is mature and hardened as of the 2026-07-08 journal-post-form ship: `/post` create+edit, FilePond photos with client HEIC→JPEG, live photo editor, draft persistence, owner-scoped `entry-actions` API (delete / reorder / trip publish). Trip page renders inline map + filter-bar feed + stats. `main.js` already has a lightbox. Verified gaps: **no Open Graph / twitter:card meta anywhere in `user/themes/intotheeast/templates/partials/base.html.twig`**, **no RSS/feed plugin installed**, `transport_mode` is serialized into the map JSON (`trip.html.twig:66-69`) but **no JS or partial consumes it**, `entry.html.twig` detail view is a 12-line stub already slated for retirement (`docs/working/backlog.md`). + +**Past learnings & open threads.** Curated-home brainstorm PAUSED mid-layout (hero+stats / map / latest entry / latest story / CTA; marker→popup preview). Per-photo captions deferred (`data-alt` uses filename placeholder). Transport-mode visualization deferred. Story-blocks authoring deferred until real stories are written. `travel-memories` Immich→Grav pipeline complete. Backlog: Komoot GPX pull, GPX-manager polish, full-res photo re-import. + +**External context.** Polarsteps' most-loved follow feature: family views a shared trip link **without an account or app** ([Polarsteps vs FindPenguins](https://voluntouring.org/2025/07/04/polarsteps-vs-findpenguins/), [Polarsteps review](https://www.overlandsite.com/tools/polarsteps-review/)); both apps monetise post-trip printed travel books. RSS-to-email digests (Buttondown, MailerLite, Mailchimp RSS campaigns) are the standard low-friction "family inbox" channel ([RSS-to-email guide](https://www.wprssaggregator.com/rss-to-email/), [service comparison 2026](https://www.readless.app/blog/rss-to-email-services-2026)). + +**Run notes.** Autonomous overnight run: no blocking questions asked; ideation frames applied inline by one agent instead of the parallel fleet (budget-lean), orchestrator-only basis verification. `direct:` bases were verified by grep/read against the working tree this night. + +--- + +## Topic Axes + +- Following along — how family & friends learn there's a new entry +- Reading the feed — arrival/dwell experience on the trip page +- Story mode — curated set pieces +- On-the-road posting — the owner's daily workflow +- After the trip — compounding, archive, keepsakes + +--- + +## Ranked Ideas + +1. [The follow-along stack](#1-the-follow-along-stack-og--share--rss--digest) +2. [The thirty-second post](#2-the-thirty-second-post-quick-log--auto-location--auto-weather) +3. [Transport-mode visualization](#3-transport-mode-visualization) +4. [The "Today" view](#4-the-today-view-resume-the-curated-home) +5. [Per-photo captions](#5-per-photo-captions) +6. [Trip Wrapped recap page](#6-trip-wrapped-recap-page) +7. [Komoot route pull](#7-komoot-route-pull-in-gpx-manager) + +### 1. The follow-along stack (OG → share → RSS → digest) + +**Description:** Make following the trip effortless for people who will never bookmark a blog. Four stages, each independently shippable, each building on the last: + +```mermaid +flowchart TB + A[Stage 1: Open Graph + twitter:card meta\nper entry/trip/story] --> B[Stage 2: Share button on the\npost-success panel - Web Share API] + B --> C[Stage 3: RSS/Atom feed\nof the active trip] + C --> D[Stage 4: RSS-to-email digest\nfor family inboxes] +``` + +Stage 1 alone changes every link pasted into WhatsApp/Signal from a bare URL into a photo + title + location card. Stage 2 turns the existing post-success panel ("View your journal / Post another") into "…/ Share this entry" — one tap after every post, while the moment is fresh. Stage 3 gives the RSS-literate a subscription and is the substrate for Stage 4, where a Buttondown/MailerLite RSS campaign mails new entries to subscribed family on a daily/weekly cadence. + +**Axis:** Following along +**Basis:** direct: grep confirms zero `og:` / `twitter:` meta tags in `partials/base.html.twig` and no feed plugin in `plugins.txt` or `user/plugins/`; the success panel exists in `post-form.js` (`initSuccessState`). external: Polarsteps' account-free share link is its most-cited family feature; RSS-to-email is a commodity integration. +**Rationale:** The site's readers are family and friends on phones in messaging apps — not blog visitors. Every entry already produces a perfect preview image (cover = first photo, by design). This is the highest leverage-to-effort ratio in the whole candidate set, and Stage 1 could ship before departure. +**Downsides:** Stage 4 introduces an external service and subscriber management; OG images should respect draft/unpublished state (don't leak draft covers to crawlers); feed must exclude unpublished entries. +**Confidence:** 90% (Stage 1–2), 75% (Stage 3–4) +**Complexity:** Low (Stage 1–2), Medium (Stage 3–4) + +### 2. The thirty-second post (quick log + auto-location + auto-weather) + +**Description:** A "quick log" posting mode for hard days: one photo + one sentence, no title required (derive it from date/location), plus removal of the two manual taps that remain in the flow — read GPS from the first photo's EXIF server-side to fill `lat`/`lng` when the fields are empty, and fetch weather server-side at submit time from coords + entry date (Open-Meteo archive API for backdated entries). The full form stays for real writing days; quick log keeps the streak alive on the days that produce none. + +**Axis:** On-the-road posting +**Basis:** direct: `post-form.md` requires photos + title + content + date; location and weather are manual button taps in `post-form.js` (`initGeo`). reasoned: on a solo trip the binding constraint on journal completeness is end-of-day energy, not tooling; every removed field measurably raises the posting rate — the same logic that already removed the hero-image field and auto-collapsed the photo section. +**Rationale:** The blog's value compounds with consistency. Denmark is a cycling trip — many days will end tired. A 30-second floor means zero-entry days become one-photo entries instead of gaps. +**Downsides:** EXIF GPS may not survive the client-side HEIC→JPEG conversion (canvas-based converters typically strip metadata) — verify with a real iPhone photo first; if stripped, read EXIF client-side before conversion and post coords explicitly. Title-less entries need a rendering decision in the feed partials. +**Confidence:** 70% +**Complexity:** Medium + +### 3. Transport-mode visualization + +**Description:** Consume the already-serialized `transport_mode` field: style the map connector line per mode (e.g. dashed for train/bus/plane, solid for walking/cycling) and show the mode emoji/icon on entry cards and map popups. The data is being shipped to the client on every trip page load and rendered nowhere. + +**Axis:** Reading the feed +**Basis:** direct: `trip.html.twig:68` serializes `transport_mode` into the map entries JSON; grep finds zero consumers in `maplibre-utils.js`, `main.js`, or any partial. The form select (walking/bicycle/bus/train/car/plane) shipped in the current post form. +**Rationale:** For a cycling-centric trip, *how you moved* is half the story the map tells. This closes a loop that was deliberately half-built: the capture side shipped, the display side was deferred. All data will exist from day one of Denmark — the earlier this ships, the more of the trip benefits. +**Downsides:** Connector styling interacts with the GPX-vs-connector suppression logic (`force_connect`, same-file proximity checks) — needs care in `MapUtils.initEntryMap`; `js/map.js` rebuild via `make build-assets`. +**Confidence:** 85% +**Complexity:** Low–Medium + +### 4. The "Today" view (resume the curated home) + +**Description:** Resume the paused curated-home brainstorm with a sharper frame: the active-trip home is the page family checks daily, so lead with *now* — a pulsing last-position marker, "Day 12 · Aarhus · 340 km so far", the latest entry, the latest story, then the full feed/map below. Marker→popup preview (already sketched in the paused brainstorm) makes the map the navigation surface. + +**Axis:** Following along / Reading the feed +**Basis:** direct: the curated-home brainstorm exists and is paused at the layout question (hero+stats / map / latest entry / latest story / CTA). external: Polarsteps' follow screen is exactly this — current position + day counter first, log second. +**Rationale:** The home page is the URL family will have. Today it renders the same feed chrome as the trip page; a "where is he *now*" lead answers the question every visitor actually arrives with, in one glance, and gives repeat visits a reason. +**Downsides:** It's a design decision as much as a build — the brainstorm needs finishing first; risks scope creep against the shared `trip-feed-col` partial (keep the partial single-purpose, add a curated lead above it rather than forking it). +**Confidence:** 65% +**Complexity:** Medium + +### 5. Per-photo captions + +**Description:** Give photos one-line captions: store per-image captions in Grav media metadata (`.meta.yaml`), add a caption field to the edit-mode photo editor grid (tap a thumbnail → caption input, persisted via the media API), render as museum-style wall text in the feed and lightbox, and use it as real `alt` text (replacing the filename placeholder in `data-alt`). + +**Axis:** Reading the feed +**Basis:** direct: `data-alt` currently carries the filename as a placeholder; per-image captions were explicitly deferred "pending Mischa's decision". reasoned: photos carry most of the feed's content weight; a single line of context ("the ferry that almost left without me") is the cheapest possible narrative upgrade and doubles as accessibility. +**Rationale:** Between a bare photo grid and a written story there is nothing today; captions are the missing middle register — and they make the eventual printed book/recap dramatically better. +**Downsides:** Captioning is one more thing to do on the road (keep it optional and editable later); `.meta.yaml` sidecars must survive the `photo-NN` renumber pipeline (`PhotoRenumberer` currently renames files — sidecars need to move with them, and `deleteUnlistedImages` already deletes them). +**Confidence:** 70% +**Complexity:** Medium + +### 6. Trip Wrapped recap page + +**Description:** An auto-generated end-of-trip recap: days on the road, total km (GPX-exact where available), entries written, photos taken, countries/towns visited, transport-mode split, biggest climbing day — rendered as a shareable, designed page per trip (`/trips//recap` or an inline trip-page section that unlocks when the trip ends). Extension later: print-CSS → the Polarsteps-style trip book. + +**Axis:** After the trip +**Basis:** external: Spotify Wrapped / Strava Year in Sport demonstrate the format's shareability; Polarsteps' printed travel book is its flagship post-trip product. direct: the stats machinery (per-file GPX aggregation, cycling stats, haversine fallback) already exists in `initTripStats`. +**Rationale:** The site already computes most of these numbers live; a recap reuses them as a keepsake and gives every finished trip a satisfying capstone that the trip page (an infinite feed) doesn't provide. Slovenia/Italy/US archives get retroactive value. +**Downsides:** Needs the full-res photo re-import (backlog) before a *printed* extension is worthwhile; design effort is the real cost — a half-designed recap undercuts the point. +**Confidence:** 65% +**Complexity:** Medium + +### 7. Komoot route pull in gpx-manager + +**Description:** Paste a Komoot tour URL into `/gpx-manager` and the server fetches the GPX (`api.komoot.de` returns GPX per tour ID) and saves it to the trip page — replacing the export→download→upload dance after each riding day. + +**Axis:** On-the-road posting +**Basis:** direct: `docs/working/backlog.md` names this with the API endpoint; the gpx-manager UI, slugification, and media API plumbing all exist. +**Rationale:** On a cycling trip the GPX step is *daily* friction; this collapses it to a paste. Server-side fetch also sidesteps mobile-browser download/upload juggling. +**Downsides:** Auth requirements for non-public tours are unresearched (backlog says the same); Komoot's API is unofficial — could break mid-trip, so the manual upload path must remain first-class. +**Confidence:** 60% +**Complexity:** Medium + +--- + +## Rejection Summary + +| # | Idea | Reason Rejected | +|---|------|-----------------| +| 1 | Offline-first `/post` (service-worker queue) | Cost ≫ value: queued multipart uploads vs sessions/nonces is genuinely hard, Denmark coverage is good, and localStorage drafts already protect the text — too risky days before departure | +| 2 | Retire entry permalink + add `#anchor` deep links | Already a tracked backlog item; cleanup, not a product direction | +| 3 | Auto-story scaffold from a date range | Premature — story authoring tooling is deliberately deferred until real stories have been written; revisit with material in hand | +| 4 | No-account emoji reactions on entries | Adds the site's first anonymous public **write** endpoint (abuse/rate-limit/storage surface) right before departure; worth revisiting post-trip as the only "return channel" idea | +| 5 | Printed trip book (standalone) | Folded into idea 6 as its extension — the recap is the shippable first step and the book depends on the full-res re-import | +| 6 | Full-res pixelfed re-import + srcset | Enabler already tracked in the backlog, not an idea in itself; sequence it before any print/keepsake work | +| 7 | Distribution foundation (RSS+OG+sitemap bundle) | Duplicate of idea 1, which stages the same work | +| 8 | travel-memories on-trip cadence | Workflow practice with the existing app; nothing to build | +| — | axis: story mode | No survivors — deliberate gap: story tooling stays deferred until the first real stories exist (only candidate was rejection #3) | diff --git a/docs/working/qa/2026-07-08-pre-trip-readiness-audit.md b/docs/working/qa/2026-07-08-pre-trip-readiness-audit.md new file mode 100644 index 0000000..22aa42b --- /dev/null +++ b/docs/working/qa/2026-07-08-pre-trip-readiness-audit.md @@ -0,0 +1,170 @@ +# Pre-Trip Readiness Audit — 2026-07-08 (overnight) + +**Scope:** everything the trip depends on from the road — the posting pipeline +(/post → cache-on-save → add-page-by-form), photo handling, edit mode, auth & +sessions, GPX manager, the custom API surface, and prod's anonymous exposure. +**Method:** read-only code audit of the current `main` + anonymous HTTP probes +against production. **No code was changed.** Findings are prioritized; a +10-minute morning checklist is at the bottom. + +--- + +## What was verified and looks solid ✅ + +- **Prod anonymous surface holds.** Probed 2026-07-08 (UTC night): `GET + /api/v1/pages` → 401 with a clean JSON error; `/post` and `/gpx-manager` + render the login form; no `X-Powered-By` leak. API CORS is same-origin + (`origins: {}`), rate limiting on (120 req/60s), session auth enabled. +- **The custom API routes are properly hardened.** `entry-actions` + (DELETE entry / reorder photos / trip publish) all require the authenticated + **owner** (`site.owner_username`, not just any login), enforce + `api.pages.write`, validate slugs as safe single segments, and resolve + targets through the page tree via the shared `EntryScopeGuard` — no raw path + concatenation anywhere. The publish route handles the APCu/in-place-write + cache gotcha explicitly and never turns a cache-invalidation failure into a + fake 500. Audit logging on all three. +- **Text can't be lost while composing.** `post-form.js` mirrors every text + field to localStorage on each keystroke and clears the draft **only** on a + server-confirmed success notice. Any failure path (validation error, expired + session, network drop, closed tab) re-offers the text on the next visit. +- **HEIC handling fails closed.** Sniffed from bytes (not filename), converted + client-side, submit is gated while a conversion is in flight, and a failed + conversion skips the file with a visible message instead of uploading a + broken HEIC. +- **Photo reconcile is fail-safe.** Runs exactly once per submit (latched), + an empty/missing `photo_order` touches nothing, only image extensions are + ever deleted, and edit-mode targets resolve through the same scope guard. +- **Edit-mode photo editor has honest error paths.** Failed reorder → revert + to last-known-good; failed refresh after a successful save → keeps the saved + order; failed batch-add → rollback with an explicit warning when rollback + itself was incomplete; 404 on delete treated as convergent success. +- **Cache invalidation on post/edit is correct even under prod caching.** + cache-on-save does `deleteAll()` + `Cache::invalidateCache()` (config + checksum bump → new page-tree index key), so in-place edits appear without + needing APCu-specific clearing on that path. + +--- + +## Findings — do before departure (P1) + +### P1-1 · Prod PHP upload limits are unverified — could block photo posting entirely +`php/php-local.ini` (100M upload / 500M post) is **mounted only into the local +Docker container** (`docker-compose.yml`); nothing in `scripts/` or `deploy/` +ships PHP limits to the prod Apache server. If prod runs distro defaults +(`upload_max_filesize=2M` is common), a single modern phone photo (3–8 MB) +fails to upload — the exact core use case of the trip. +**Action:** `make remote-diag` (or a one-off phpinfo check) to read prod's +`upload_max_filesize` / `post_max_size` / `max_file_uploads`. If low, add a +`.user.ini` (FPM) or `.htaccess` `php_value` (mod_php) via a new make target. +The real proof is P1-2's live post with photos. + +> **Resolved 2026-07-09.** Confirmed prod was at the 2M default. Fixed by +> Mischa via Webmin: PHP execution switched from CGI to **PHP-FPM** (package +> was already installed) and the upload limits raised in the FPM +> configuration. Because the setting lives in the server-side FPM config — +> not in the webroot — it survives fresh Grav installs, so no +> `deploy/`-versioned `.user.ini` / make target is needed. Side benefit: APCu +> now persists in shared memory, matching the assumptions in the +> entry-actions publish endpoint's cache invalidation. +> Config location for future reference: Webmin → PHP-FPM Configuration. +> Still owed: P1-2's live phone post is the end-to-end proof. + +### P1-2 · One real end-to-end post from the actual phone, on prod, over cellular +The runbook's pre-launch smoke (handover step 7) calls for one `/post` submit +on prod. After the 2026-07-08 deploy, confirm this happened **from the phone +you'll travel with, on cellular, with 2+ HEIC photos** — that exercises HEIC +conversion, FilePond upload, prod PHP limits, cache-on-save under +`twig.cache:true`, and the feed render in one shot. Then edit that entry +(reorder + delete a photo), then delete it — the edit/delete paths shipped +today and deserve one prod rep. + +### P1-3 · Session expiry mid-compose: test the 30-minute window once +`system.yaml` has `session.timeout: 1800` (30 min) and `form.yaml` has +`refresh_nonce: false`. A slow entry written on a train can easily outlive the +session; rememberme (enabled, 7-day cookie) should transparently re-auth the +next request, but the form **nonce** and the FilePond **flash uploads** were +created under the old session. The localStorage draft guarantees the text +survives whatever happens — but you should see the actual failure mode once +now, not first in a hostel. +**Test:** open `/post`, add a photo, wait 35+ minutes, submit. +**If it's ugly:** consider raising `session.timeout` in the prod env override +(`deploy/env/prod/system.yaml`, e.g. 4–12 h) — single-owner site, low risk, +big comfort. (Per-env override, not the committed dev `system.yaml`.) + +### P1-4 · Make sure you can log back in from the road +The rememberme cookie lasts **7 days** — on a multi-week trip you *will* be +re-typing the password, possibly on hotel wifi after a cookie wipe. Login +throttling is 5 attempts / 10 min (easy to hit with phone typos). +**Action:** confirm the password is in the phone's password manager and test a +fresh login on the phone once. Know that after 5 typos you wait 10 minutes — +don't panic-retry. + +--- + +## Findings — worth doing before departure (P2) + +### P2-1 · Duplicate home page: `user/pages/home/` shadows `00.home/` +Both `user/pages/home/home.md` (old, committed in `a440583`, carries +`routes: default: /`) and `user/pages/00.home/home.md` (the real one) exist +with the same slug and near-identical content — which is exactly why a silent +mix-up would go unnoticed. Which page wins `/home` (the `home.alias` target) +depends on page-index ordering luck. +**Action:** delete `user/pages/home/` (verify `/` and `/home` still render the +context-aware home from `00.home` afterwards, incl. the pre-departure branch). + +### P2-2 · Docs drift in CLAUDE.md +- `active_trip: japan-korea-2026` example — that trip doesn't exist; the real + upcoming trip is **`/trips/denmark-2026`** (local `site.yaml`, uncommitted). +- The `entry-actions` plugin (three owner-only API routes, shipped with the + journal-post-form feature) isn't mentioned in CLAUDE.md's plugin list or + architecture sections, and the "post form uses filepond via cache-on-save" + description predates the edit-mode photo editor. + +### P2-3 · No HSTS header on prod +Apache serves without `Strict-Transport-Security`. One-line header addition; +the login form and session cookie deserve it (`secure_https: true` is already +set for the cookie). + +### P2-4 · Shrink the unused API auth surface +`api.yaml` enables **api_keys + JWT + session** auth. The site only uses +session auth (gpx-manager, post-form edit, entry-actions). If no API keys are +in use (`user/config/plugins/api-private.php` is untracked/local — not +audited), disabling `api_keys_enabled`/`jwt_enabled` in config removes two +whole credential classes from the attack surface. Not urgent — the endpoints +behind them still enforce owner checks. + +### P2-5 · Confirm the backup path is live +Every road post only exists on the prod disk until git-sync commits it to +Gitea. **Action:** `make remote-content-status` — confirm git-sync is enabled +on prod and the working tree is clean/pushed. (Photos live under `pages/`, so +they ride along in the content repo — the backup covers them too.) + +--- + +## Known limitations — accepted, no action (P3) + +- **Photos are not draft-persisted** (File/Blob can't go to localStorage); the + restore hint says so explicitly. Re-selecting photos after a failure is the + designed trade-off. +- **Location/weather helpers depend on free third-party APIs** (BigDataCloud + reverse-geocode, Open-Meteo). Both are best-effort with manual fallbacks — + fine. +- **No offline mode.** `/post` needs connectivity to load; composing offline + means the phone's notes app. (Logged as an ideation candidate, not a bug.) +- **Rate limit 120 req/60s** is generous for a single owner; a 6-photo edit + batch stays far below it. + +--- + +## Morning checklist (~10 minutes + one coffee) + +1. `make remote-diag` → check `upload_max_filesize` / `post_max_size` on prod + (P1-1). Fix limits first if they're at defaults. +2. From the phone, on cellular, on prod: log in fresh → post a test entry with + 2 HEIC photos → verify it's in the feed immediately → edit it (reorder + + remove a photo) → delete it (P1-2, P1-4). +3. `make remote-content-status` → git-sync clean and pushing (P2-5). +4. Optional but cheap: start the 35-minute `/post` session-expiry test in a + background tab while doing the above (P1-3). +5. Queue the P2 cleanups (duplicate home folder, CLAUDE.md drift, HSTS) for a + normal dev session — none block departure.