Files
intotheeast-com/docs/working/qa/2026-07-08-pre-trip-readiness-audit.md
m038andClaude Fable 5 94bfc53b90 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195b3cDdMeize2Mm1FgC2aU
2026-07-09 17:59:05 +02:00

9.4 KiB
Raw Permalink Blame History

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 (38 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. 412 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.