`submodule deinit` is needed before `worktree remove` (a populated user/
blocks it), but worktrees share .git/config — so deiniting inside the
worktree stripped submodule.user.url globally. After any `make worktree-rm`
the main checkout's `git submodule status` reported `-` (not initialised)
while user/ sat there fully intact, and a later `submodule update` would
have had no URL to work from. Re-register with an idempotent `submodule
init` after the removal.
Found by tearing down the post-location-override worktree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merged state recorded (user/ dd19995, outer 4450bd6, pin bumped).
The "owner_username cluster" was wrong: on merged main only DEL4 fails, with
identical site.yaml and content, so auth was never the cause. The worktree's
extra five failures came from its incomplete git-ignored user/plugins/ set.
DEL4 itself is real and stays open — deleting an entry removes it from the
DOM and from disk, but a fresh trip-page load makes the server re-emit the
card, the same invalidation bug the spec's header says was fixed once before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Post-form location override (U1-U6) plus the code-review hardening, the
maplibre-CSS lazy <link>, and the test-entry leak fix. Bumps the user pin to
dd19995, the corresponding user/ merge-to-main commit.
CLAUDE.md conflicted because both sides changed it deliberately: main cut it
to rules-only (839a4d0, ed6e43a) while this branch added the map-doctrine
carve-out (829325c). Resolved to main's rules-only structure with the
carve-out ported into it — without it CLAUDE.md would forbid the second map
engine this feature deliberately ships. The descriptive detail stays in
docs/reference/architecture.md, per main's content-tiering convention.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
~/Projects is a symlink to ~/Nextcloud/Projects — one directory, not two
clones. The differing user/main refs came from the worktree having its own
submodule git dir (.git/worktrees/<name>/modules/user), which is worth
knowing: submodule commits made from the main checkout stay invisible in a
worktree until fetched, and a local fetch moves them without a push.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The warnings added in 6398542 were wrong. UG1, UG2 and LD1 were failing
because this branch predated e17a5dc, not because the behaviour they assert
was missing: merging user/main brought the FilePond upload gate and the
oriented-derivative slide dims, and all three pass with no product change.
Headers now point at e17a5dc for both mechanisms. Also corrects the plan's
.env note — the env layering is intentional (.env global, .env.<ENV> per
environment via the generated remote-*-<env> targets); the actual fault is
just that `-include .env` additionally requires makefile-valid syntax and
line 6 is not, which breaks make in both non-worktree clones.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Also corrects the green-run line: "test-post 6/6" is the scripts/test-post.sh
shell suite, not the Playwright specs under tests/ui/post/ — conflating the
two made the Playwright post specs look covered when they were never run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lightbox-dims.spec.js hardcoded ../../../user, so a run against a checkout
detached from the served tree planted its fixture in a different user/ than
Grav renders and LD1 failed as an opaque "card never appeared" timeout.
Take USER_DIR from helpers instead, which honours GRAV_USER_DIR.
The three specs in this folder that fail do so for real, pre-existing
reasons, and both files' headers implied otherwise:
- UG1/UG2 specify a submit gate that is not implemented. post-form.js's
only create-form guard is `converting > 0` (pre-FilePond HEIC
conversion); it never inspects FilePond item state at submit time, and
.photo-convert-status is created lazily by photoStatusEl() only from the
HEIC paths — so for a plain JPEG the element never exists and both
expectations fail as "element(s) not found". UG2 is the one that matters:
a failed upload keeping its thumbnail is unguarded silent data loss.
- LD1's header described its root cause in the past tense, reading as
fixed. entry-journal.html.twig:48-49 still emits {{ img.width }} /
{{ img.height }}, so EXIF-rotated photos still declare pre-rotation dims
and PhotoSwipe still squeezes them.
Left failing rather than skipped, per retries:0 — a red test here is a real
defect, and hiding these would lose both.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The image declares /var/www/html as a VOLUME, so tmp/forms/<session>/ —
where Grav parks FilePond uploads until the submit moves them into the page
folder — lived in an anonymous volume that any container-recreating
`docker compose up` discards, dropping the photos of a post that was filled
in but not yet submitted. grav_tmp gives the staging area its own lifecycle.
Also pins makefile.configureOnOpen off in the workspace, so the VS Code
Makefile extension stops trying to configure a Makefile whose `-include
.env` cannot be parsed as makefile syntax.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A ui-test entry had survived into the active trip's dailies. Three independent
failures had to line up for that, and all three were real:
1. cleanupEntry() used host-side fs.rmSync. Grav's Apache workers run as root,
so every entry the form creates is root-owned and recursive removal needs
write permission on that directory — which the host user lacks. Cleanup had
never worked for form-created entries; it just threw inside a path nothing
checked. It now falls back to `docker exec … rm -rf` in the container that
actually serves USER_DIR.
2. globalTeardown's dailies sweep keyed off a `parent:` in post-form.md — a key
deliberately removed (the write target comes from site.yaml active_trip, and
CLAUDE.md forbids re-adding a static parent). The regex could never match, so
dailiesDir was always null and the sweep silently did nothing. It now reuses
helpers' own resolution instead of keeping a divergent copy.
3. Nothing pinned the suite to this checkout's server. playwright.config.js
defaults to :8081, so a worktree run hit the MAIN checkout — entries created
in one content tree while the specs asserted and cleaned up in another.
test-ui now passes GRAV_BASE_URL from GRAV_PORT, and globalSetup hard-fails
when the server's bind mount disagrees with the tree the specs read.
Also fixed, found on the way to a green run:
- test-account interpolated the password into an `sh -c` string, so a password
containing a shell metacharacter was re-parsed by the container's shell
(`sh: 2: <fragment>: not found`, no account, every UI run dead). It now
travels via `docker exec -e`, making the recipe indifferent to its contents.
- `make start` in a worktree always failed: travel-memories declares
`env_file: .env` and worktree-new creates none. It degrades to start-grav
there — a worktree with no server is what sent runs to :8081 in the first
place.
- test-form-config asserted a hero_image field that 8cf1145 deliberately
removed; it had been failing ever since.
Verified: config 22/22, post 6/6, location-override 20/20, and a full UI run
now leaves zero ui-test entries behind. The remaining UI failures are
pre-existing on main — site.yaml pins owner_username to a real account while
the suite logs in as testrunner, so owner-only controls never render for it.
Only trip-publish.spec.js patches that; delete-flow, edit-mode and anon-view
do not. Left for a separate branch.
Four rounds of CLAUDE.md reduction (255 -> 305 -> 179 -> 74 lines) turned
up one consistent finding: every stale fact was a *description* of code or
config, never a rule. Two had been written by Claude days earlier.
Documents the operational test ("does this line change what Claude does on
a task where it wouldn't otherwise open the relevant file?"), the tiering
table, why gotchas are the one category that cannot move to a read-on-demand
docs/exceptions/, invariants-over-enumerations, and how to tell when a
reduction pass has hit the floor.
Also surfaces the docs/solutions frontmatter fields in CLAUDE.md's
entry-point table so the store is greppable by module, not just browsable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Enumerating the bundles meant adding a fifth one silently falsified
CLAUDE.md. "Everything in js/ is generated except js/src/,
maplibre-utils.js and nav.js" is exactly true today and stays true.
The source->output table lives in docs/reference/architecture.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
R8 and R13 both described behaviour that changed in review, and R13 rested on a
server-side cleanCoordinate() that had never been committed. Both now describe
what actually ships, with the revision called out inline rather than silently
rewritten. The plan's Status keeps ✅ Complete but now records what the review
changed and the two things still open before merge.
Bumps the `user` gitlink to e873a9c (the review fixes). The submodule is
deliberately NOT pushed: git-sync would propagate it to production. So this pin
still references a commit that exists only locally — push `user/` and re-point
before this branch merges.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md keeps the status convention as a one-line rule (it has to be
loaded to be followed). This is the same convention written out for a
human reader, with the meanings the trim dropped, plus what each
subfolder of docs/working/ is for and a grep one-liner for "what's open".
Notes the two distinctions that matter in practice: Deferred is not
Abandoned, and a trailing note after "Complete" is normal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The U5 guard asserted only `.notices` toHaveCount(0) and toHaveURL(/\/post/).
Both pass instantly, and both also hold for a *successful* submit — the form
posts to /post and only renders its notice after the round trip, and the click
is dispatched via evaluate(el => el.click()), which skips Playwright's
navigation-aware waiting. So the one test standing between a bad coordinate
and the server could not fail. It now proves the negative on disk via
findEntry() after a settling interval, registers the tag for cleanup before
the click, and asserts the flag and value survived.
CLAUDE.md's "one map path" section stated flatly that a single map code path
exists, which location-map.js now contradicts. Recorded it as the one
sanctioned exception (an editor, not a display map; lazy-imported; shares only
MAP_STYLE) rather than leaving the doctrine wrong.
The `user` gitlink is deliberately NOT bumped here: its pin already points at
an unpushed submodule commit, which must be pushed and re-pointed before this
branch merges.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CLAUDE.md now carries only what must be known *before* opening a file:
hard rules, gotchas, and an entry-point table. Everything descriptive
moved to the doc that lives next to the code.
Moved out:
- stack versions, plugin roles, asset pipeline, nav shape, user/ repo
tracking rules → docs/reference/architecture.md
- Playwright layout, config facts, auth-setup project, test account
→ docs/reference/testing.md (new)
- folder map, full make command list (build/test/demo/worktree targets
that only existed in CLAUDE.md) → README.md
- dev/prod Twig settings table → already in docs/guides/deploy-cycle.md
Fixed while verifying, all of them descriptions that had drifted:
- demo fixtures were listed as italy-2026-demo + no-photos-demo; the
actual folders are italy-2025 + italy-2026-demo
- the map engine was cited at js/src/maplibre-utils.js; it is
js/maplibre-utils.js, a hand-authored file beside the bundles
- the build-output list omitted fonts/ and the generated
templates/partials/weather-icons.html.twig, and did not flag that
js/maplibre-utils.js and js/nav.js are sources living in js/
- README called user/ a "standalone git repo" (it is a submodule)
- docs/README.md linked to a non-existent working/production-todo.md
- git-sync-notes.md pointed at "CLAUDE.md §1", a section number that
no longer exists
Net: ~17.1k → ~8.5k chars of always-loaded context.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CLAUDE.md is loaded into context on every request, so every line has a
recurring cost. Applies one rule to decide what earns its place: keep what
changes behaviour (rules and gotchas Claude cannot discover before it acts);
extract what merely describes code (Claude reads the code anyway, and prose
about code silently drifts).
The four stale facts fixed in the previous commit were all in the
"describes code" class -- active_trip, the Admin2 version, demo-load's
scope, the gitignore list. None were rules. That is the argument for moving
this material next to what it documents.
Extracted (kept as pointers):
- entry-map + trip-feed-col parameter contracts (56 lines) -> reference/
architecture.md "Shared partial contracts". CLAUDE.md keeps only the
invariants: single map path, must assign window.tripMap/homeMap, keep
trip-feed-col single-purpose, initTripStats depends on MapUtils.
- Prod override runbook (49 -> 9 lines) -> guides/deploy-cycle.md "The env
override tree", incl. the Twig dev/prod table and WEB_HOST. CLAUDE.md
keeps the two behavioural rules: never commit prod values, and Admin on
the server writes to the env tree (so check both config paths, env wins).
- GPX API routes, session auth and the Blob/FormData upload gotcha ->
guides/gpx-manager.md "How the manager is wired".
- Trip-switch procedure -> guides/trip-switching.md. CLAUDE.md keeps the
one rule that matters: never re-add pageconfig.parent to post-form.md.
- Also trimmed the dev-command table and custom-plugin table added in the
previous commit; both largely restated the Makefile and blueprints.
Fixed the guides being pointed into, so the pointers lead to truth:
- trip-switching.md instructed editing a pageconfig.parent that no longer
exists -- its whole "two files must be updated together" premise was
obsolete and would have reintroduced the desync it warned about.
- architecture.md: Grav 2.0.4->2.0.7, Admin2 2.0.10->2.0.12, corrected the
posting pipeline to show cache-on-save injecting parent before the write,
added entry-actions to the custom-plugin list.
- japan-korea-2026 -> denmark-2026 across guides/reference (docs/solutions
keeps its historical references intact -- those are incident records).
Verified: every markdown link resolves, every referenced section heading
exists, and each extracted item was confirmed present in its new home.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Audit of the root CLAUDE.md (scored 76/100) found the architecture and
remote-ops coverage strong but the test workflow entirely undocumented and
several facts drifted from the tree.
Corrections (verified against the checkout):
- active_trip was japan-korea-2026; committed value is /trips/denmark-2026
and no japan-korea trip folder exists. Also note the value is a route.
- Admin2 2.0.10 -> 2.0.12 (installed version).
- demo-load/demo-reset described as italy-only; the Makefile loops over every
fixture trip under user/docs/demo/trips/.
- user/ gitignore claim omitted the three un-ignored site-owned plugins and
the secret/env exclusions.
Additions:
- Section 3 "Testing": make test/test-config/test-post/test-ui, the
auto-created testrunner account, Playwright layout, the auth.setup.js
storageState dependency, and GRAV_BASE_URL for worktree servers.
- Local dev command table, plus which theme assets are build outputs
(js/src -> bundles) versus hand-authored (css/style.css, css/tokens.css).
- Custom plugins: story-blocks and entry-actions alongside cache-on-save.
- Local plugin patches: install-plugins overwrites git-ignored third-party
plugins; deploy/patches/ + apply-plugin-patches is the tracked fix path.
- travel-memories service on 8082; make pixelfed-import.
Every make target and file path referenced was verified to resolve.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds Playwright coverage for the four cross-reviewer-confirmed bugs
fixed in the user/ submodule (map-load race on rapid reopen, mismatch
flag clearing on blank, and submit blocked on unresolved mismatch),
and bumps the user/ pointer to the commit with those fixes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mocks the Open-Meteo geocoding endpoint via page.route() so the suite is
hermetic. Covers the panel's closed-by-default state, search happy path,
Paris/Texas disambiguation ranking, no-match/network-failure/in-flight
states, XSS-safe rendering, map canvas singleton behavior, drag sync, the
mismatch flag, the maplibre-gl lazy-load boundary, and a full submit
round-tripping lat/lng into the entry's frontmatter.
Adds the implementation plan for the location-override feature and folds in
ce-doc-review findings: a panel-open sync gap (pin didn't render on reopen
with pre-existing coordinates), keyboard/ARIA accessibility gaps in the
search-results list and mismatch flag, a shared MAP_STYLE module to remove
duplication drift risk, and a corrected Open-Meteo risk/mitigation split.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Addresses the actual root cause behind the Denmark 2026 corrupted-coordinate
bug: there was no supported way to set an entry's location to somewhere other
than the current GPS position, forcing hand-typed/pasted raw coordinates
through Admin2's fragile text field. Backend sanitization (cache-on-save)
already guards against silent corruption; this spec adds a frontend way to
avoid needing that path at all.
Regression specs for the two 2026-07-09 prod bugs (fixed in user/ e17a5dc):
- upload-gate.spec.js — UG1/UG2: create submit is blocked with a visible
message while a photo upload is in flight or after it FAILED; nothing may
land on disk. The form plugin's own guard misses LOADING and
PROCESSING_ERROR, which silently dropped a photo on a fast save.
- lightbox-dims.spec.js — LD1: a slide's data-pswp-* must equal the
browser-rendered natural size of the linked image. Fixture is an 800x600
JPEG with EXIF Orientation=6 (renders 600x800 portrait), planted on disk
in the demo trip (the active trip may be an unpublished draft that 404s).
New fixture: tests/fixtures/test-photo-exif-portrait.jpg.
Note: the suite currently needs GRAV_TEST_USER/GRAV_TEST_PASS overrides —
the .env GRAV_TEST_PASS contains shell-special chars that break `make
test-account` (see the Makefile comment requiring a plain password).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195b3cDdMeize2Mm1FgC2aU
Close the remaining root-owned bind-mount vector: build-assets (a docker
run, missed by the docker-exec fix in 209b804) now runs as the host
uid/gid with HOME=/tmp for npm's cache. Verified: build completes clean,
zero root-owned files under user/themes, bundles byte-identical.
Solution doc updated from "still open" to fixed; CLAUDE.md stack section
now matches the Dockerfile's Grav 2.0.7.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195b3cDdMeize2Mm1FgC2aU
Refresh audit of all 13 docs/solutions learnings against the current
codebase. Core guidance verified accurate everywhere; three docs had
reference drift:
- dual-repo-submodule-workflow: point worktree setup/teardown at the
make worktree-new/worktree-rm targets (manual procedure misses
.worktree-env isolation)
- docker-exec-root-owned-bind-mount-files: tracked-plugin list now
includes entry-actions; fix-perms description matches actual target
- grav-plugin-config-without-code-wont-enable: 3-category model's
custom-in-repo list now includes entry-actions
CONCEPTS.md: add Container, Content repo, Outer repo, Pin, Env tree,
Remote-only plugin; refresh Active Trip (switching is one setting now).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195b3cDdMeize2Mm1FgC2aU
Both shipped with feat/journal-post-form (merged + deployed to prod) and
passed owner UI/touch-drag QA on 2026-07-08. Corrected the stale
"not merged / not deployed" language and fixed a duplicate Status marker.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The install-plugins fix (209b804) only covered docker exec. build-assets
runs `docker run node:20-alpine` without --user, so it still writes
root-owned node_modules + esbuild bundles into user/themes/ — which is what
blocked `git worktree remove` at teardown. Broaden the doc and prevention
rule to cover docker run, with the --user fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
Land the owner trip publish/unpublish toggle to local main: Playwright specs
(TP1-TP8), the ce-compound solution doc + CONCEPTS.md Published/Draft concept,
and the plan/spec docs. Bumps the user submodule pin to 543e8e3 (the merged
user/ main containing the feature + denmark-2026 cover content).
Local landing only — nothing pushed.
Document the trip publish-toggle cache-invalidation finding: an in-place
trip.md `published` edit under cache.check.method: folder + APCu driver stays
stale because the folder checksum is unchanged AND the web APCu store is
unreachable by a CLI clearcache — fixed with apcu_clear_cache() from the web
request. Cross-link the sibling grav-deleteall doc (the create/delete case) as
necessary-but-not-sufficient here, and add the Published/Draft trip status
concept to CONCEPTS.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
Add the three cases the code review flagged as uncovered:
- TP7 (R15): a failed POST reverts the switch and surfaces the visible toast.
- TP8 (R13): the in-flight lock suppresses a concurrent second submit (exactly
one POST fires while the switch is aria-busy/disabled).
- TP5 leg: a MISSING published key -> 400 (the array_key_exists branch, distinct
from the is_bool branch already covered).
All 10 trip-publish specs green (serial, worktree container).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
TP1/TP1b/TP2–TP6 cover the owner gate, coverless drafts, cache-correct
hide/restore, the active-trip confirm, backend authz (401/403/400), and
the home fallback. The suite pins site.owner_username to the authenticated
test user (restore on teardown) and runs serially — it mutates global
config and clears the shared cache, so it collides with parallel readers.
Bumps the user/ pin to the finished trip-publish-toggle content (064f0f0)
and marks the plan Complete.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mpdu3Dt1iVoozHwAMyjrbn
A `reset --hard` content deploy leaves Grav's compiled-Twig/page cache
stale, so the first visitor pays the recompile. `remote-warmup` clears
the cache then crawls the public site (homepage + trips listing + every
trip page linked from it) to pre-render pages. Grav has no native warmup
command, so it's an HTTP crawl — which also doubles as a smoke test
(non-2xx pages flagged). Wired into REMOTE_TARGETS (-test/-prod variants)
and added as the final step in both deploy-cycle.md phases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
New learning: docker exec defaults to root, so make targets writing into
the ./user bind mount (esp. install-plugins -> gpm) created root-owned
files (11,624 accumulated), breaking worktree-rm. Fix: HOST_UID/HOST_GID +
`-u` on file-writing execs while the grav container still boots as root.
Cross-linked reciprocally with the sibling docker-dev-env upgrade doc.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
`docker exec` defaults to root, so `make install-plugins` wrote plugins into
the ./user bind mount as root — un-removable on the host without a root
container (exactly what blocked the 2.0.4 worktree cleanup). The grav service
can't simply run `user: 1000` because the base image entrypoint needs root to
bind :80 and set up cron, so drop only the file-CREATING CLI to the host user:
- HOST_UID/HOST_GID from id -u / id -g
- install-plugins makes cache/tmp writable (container-internal, never touches
the host) then runs gpm as the host user, so plugins land owned by you — no
post-hoc chown, no root files, no root rm needed at teardown
Verified: gpm reinstall as uid 1000 leaves 0 root-owned files under ./user
(was 11624), site healthy (/ and /admin 200), plugin patches reapplied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
Encode the dual-repo worktree SOP as make targets so no step is skipped:
worktree-new creates the outer worktree off main, inits its own user/
submodule, branches both repos, and starts an isolated Grav dev server on an
auto-picked free port (8090+) whose identity is persisted in a git-ignored
.worktree-env; worktree-rm tears it all down including the submodule deinit
that, when skipped by hand, leaves orphaned .worktrees/ dirs.
docker-compose.yml container_name + ports are parametrized as ${VAR:-default}
so the main checkout is byte-identical, and the 11 hardcoded intotheeast_grav
refs in local targets now use $(GRAV_CONTAINER). CLAUDE.md points at the
commands instead of the manual steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
New ui-bugs learning: cropResize fits-inside (returns a source-aspect
sliver), cropZoom crops-to-fill. Using cropResize for a cover/banner
strip hands the browser a portrait sliver that object-fit:cover then
upscales into a blur. Documents the fix, the empirical op comparison,
and prevention (verify Medium op dimensions; guard retina upscaling;
regression-test composition, not just the URL).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K
- trip-header/trips-list specs now assert the cropZoom wide-strip aspect
and the all-or-nothing retina rule (1x-only for sub-2w sources), and a
new regression on us-canada-mex-2024 covers the reported portrait-blur
- remove the orphaned AE4 test (no-photos-demo fixture was deleted)
- backlog: full-res pixelfed reimport (luxury quality item)
- plan: record the cover-quality follow-up + AE4 fixture removal
- bump user pin -> 536ca20 (trip-description-hero cover fix)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RDS6t8wcpbwKvvrxykVQ5K