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
9.4 KiB
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;/postand/gpx-managerrender the login form; noX-Powered-Byleak. 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), enforceapi.pages.write, validate slugs as safe single segments, and resolve targets through the page tree via the sharedEntryScopeGuard— 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.jsmirrors 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_ordertouches 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-2026example — that trip doesn't exist; the real upcoming trip is/trips/denmark-2026(localsite.yaml, uncommitted).- The
entry-actionsplugin (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.
/postneeds 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)
make remote-diag→ checkupload_max_filesize/post_max_sizeon prod (P1-1). Fix limits first if they're at defaults.- 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).
make remote-content-status→ git-sync clean and pushing (P2-5).- Optional but cheap: start the 35-minute
/postsession-expiry test in a background tab while doing the above (P1-3). - Queue the P2 cleanups (duplicate home folder, CLAUDE.md drift, HSTS) for a normal dev session — none block departure.