The hydration helper alone fixes the flake; restoring CPU-count
workers locally cuts the suite from 55s to 22s. Cover the two
remaining /auth/login navigations the prior commit missed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues caused the same class of flake locally:
1. Default workers were CPU-count, but the journal/planner are served
by Vite dev (not the production build CI uses). Cold-compiling
`/api/auth/register` under N parallel hits produced 30s timeouts
on a quarter of runs. Set workers to 1 in both environments for
parity with CI.
2. Even sequentially, a button is clickable per Playwright's
actionability check before React has hydrated its `onClick`. So
the first click after a navigation could fire native form submit
(or do nothing), which manifested as "URL never changed to /" or
"menuitem Log Out never appeared". Add a `waitForHydration` helper
that polls for React fibers (`__reactProps$<id>`) attached to a
DOM node and call it after each cross-page navigation that ends
in an interactive form or dropdown.
CI is unaffected (production builds hydrate fast and didn't expose
either bug), but the helper is harmless there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two e2e tests intermittently failed with "unmocked external request"
when the Sentry browser SDK emitted an envelope to the real ingest
endpoint despite `enabled: false`. The BrowserTracing integration
captures the page-load transaction before init's enabled flag fully
propagates, so a single envelope leaks on cold start.
Add a SILENT_DROP list that aborts matching requests without recording
them as blocked, so legitimate missing-mock failures still surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stream C from docs/information-architecture.md. The navbar's account
cluster (<username> + Settings + Logout) was three controls for the
same concept; the cluster now collapses behind an avatar dropdown.
On mobile, all primary nav and account controls move into a
hamburger drawer so the top bar stays at logo + bell + hamburger
without wrapping at small viewports.
- New apps/journal/app/components/Avatar.tsx — initials-only avatar
(no image-upload story yet; the component is the single place to
add image fallback when one lands).
- New apps/journal/app/components/AccountDropdown.tsx — click the
avatar trigger to reveal Profile / Settings / Log Out menuitems.
Click-outside + Escape-to-close. role="menu" + role="menuitem" for
a11y.
- New apps/journal/app/components/MobileNavMenu.tsx — hamburger
trigger + slide-out drawer at md and below. Contains all primary
nav (Feed, Explore, Routes, Activities, Notifications) plus the
account cluster (Profile, Settings, Log Out). Auto-closes on
navigation; locks body scroll while open.
- apps/journal/app/root.tsx — refactor NavBar:
* Brand + primary nav links hide at md and below (drawer covers
them).
* Right cluster: bell on every viewport; avatar dropdown on
desktop, hamburger on mobile.
* Loader exposes user.displayName so Avatar can compute initials.
* Drops the now-unused Form import; Form moved into the
dropdown/drawer components.
E2E tests updated:
- e2e/auth.test.ts — logout helper opens the avatar dropdown via
aria-label = displayName||username, then clicks the Log Out
menuitem. Username assertions in nav switch from getByText to
getByRole("button", {name: username}) (the avatar button).
- e2e/settings.test.ts — "settings link visible in nav" opens the
avatar dropdown first, then asserts the Settings menuitem.
i18n: nav.openMenu, nav.closeMenu keys for the hamburger trigger
labels (EN + DE).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stream F implementation. Closes the gap between "I want to follow
someone on this instance" and "I have a username from outside the
app." Anonymous visitors and signed-in users both reach a paginated
directory of local public users; signed-in viewers also see Follow
buttons inline.
- apps/journal/app/lib/explore.server.ts — listDirectory (paginated,
ordered by MAX(public-activity created_at) DESC NULLS LAST,
tiebreaker users.id DESC), listActiveRecently (top 5 in last 30
days), countDirectory, batched countFollowersBatch and
getFollowStateBatch helpers so the page issues two extra queries
regardless of page size. Excludes private profiles and the demo
persona; banned/suspended scaffolding noted for forward-compat.
- apps/journal/app/routes/explore.tsx — loader fans out the four
queries in parallel; component renders an "Active recently" strip
(hidden when empty) above the main directory; FollowButton inlines
per row for signed-in viewers. Bio truncated to 120 chars.
- apps/journal/app/routes.ts — register /explore.
- apps/journal/app/root.tsx — add Explore navbar entry for signed-in
users.
- apps/journal/app/routes/home.tsx — visitor home gains a secondary
"Browse who's here →" link to /explore alongside the Planner
escape hatch.
- packages/i18n/src/locales/{en,de}.ts — explore.*, nav.explore,
home.exploreLink keys.
- apps/journal/app/lib/explore.integration.test.ts — opt-in
(EXPLORE_INTEGRATION=1) integration tests covering inclusion,
exclusion, ordering, NULLS LAST behavior, "Active recently"
30-day filter, count helpers.
- e2e/explore.test.ts — anonymous loads /explore (no auth needed);
private profile is excluded from the directory.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Folds the actionable follow-requests surface into the Notifications page
as a Requests tab (alongside the existing Activity tab), so the navbar
exposes a single bell instead of two adjacent inboxes. The Requests tab
shows a count badge for pending rows regardless of read state, while the
bell badge keeps reflecting the unread-notifications count (which already
covers `follow_request_received` rows). The standalone /follows/requests
URL is preserved as a 301 redirect so prior notification deep-links,
emails, and bookmarks still resolve.
Driven by the IA review captured in docs/information-architecture.md.
Specs (notifications, social-follows, journal-landing) are updated in
the same change to reflect the new structure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The SSE connection to /api/events (added with notifications) keeps the
network in-flight forever, so `networkidle` never resolves. Each save
in the affected helpers timed out at 30s × 3 retries, which both broke
the run and made it suspiciously long.
Switched to explicit waits:
- "Profile saved." text after settings save (notifications + public-content + social)
- "No pending follow requests." after Approve (social.test.ts)
- toBeHidden poll for the Mark all read button (notifications)
- toBeVisible poll for the empty-state copy after Approve (notifications)
These are all the affected `networkidle` call sites in e2e. The
fetcher.Form pattern stays — once the action commits, the page
revalidates and the post-state element appears.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the notifications system end-to-end (4 types, payload-versioned
JSONB, SSE-based live unread badge, /notifications page, mark-read API,
fan-out job for activity_published, daily 90-day retention purge).
Bell icon in the navbar with unread badge.
Side-findings from exercising the change:
- Add 6-digit magic code to registration (mirrors login UX, mobile
paste-friendly), with `[Register Magic Link]` console line in dev so
the code is reachable without a real email transport.
- Manual passkey/magic-link toggle on the register form (login already
had it).
- Restrict ALPN to http/1.1 in HTTPS dev so React Router's
singleFetchAction CSRF check (Origin vs. Host) passes — Node doesn't
synthesize Host from h2's :authority. Plain HTTP dev unaffected.
- Followers/Following routes now use the locked-account rule from the
profile route (owner + accepted followers see the list; others 404).
Profile page renders the count chips as plain spans for viewers who
can't see the lists, so private profiles don't surface dead links.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`getByLabel('Public')` matched both radios because the Private radio's
help-text label contains the substring "public" ("…followers see your
public content."). Switch to targeting the input directly by name+value,
which is unambiguous regardless of help-text wording.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three places assumed the old default-public model:
1. e2e/public-content.test.ts: "profile 404 when user has no public
content" → updated to assert the new locked-account stub renders
200 with "This profile is private" and the private route doesn't
appear. The two follow-on tests (public-route profile, unlisted
route) now flip the owner to public via a setProfileVisibilityPublic
helper before the anon-visit assertions, since new users default to
private.
2. e2e/demo-bot.test.ts: bruno is seeded with profile_visibility =
'public' on insert (timestamped seed) and the existing-bruno path
gets a follow-up UPDATE so the test's anon-visitor assertion
(profile renders, public route is listed) holds regardless of which
default he was first created under.
3. apps/journal/app/lib/demo-bot.server.ts ensureDemoUser: also pins
profile_visibility = 'public' on insert. The demo persona is
discoverable by design — that's the whole point.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the earlier 404-for-private model with Mastodon-style locked
accounts. A private profile now returns 200 with a stub layout and
gates content behind follow approval. Default for new users flips from
'public' to 'private' to align with trails.cool's privacy-first
content defaults.
Schema:
- users.profile_visibility default flipped to 'private'. Existing rows
remain 'public' (backfill on first migration handled them).
Follow API (follow.server.ts):
- followUser now creates Pending (accepted_at = NULL) against private
targets and Accepted against public targets — no more refusal.
- New: countPendingFollowRequests, listPendingFollowRequests,
approveFollowRequest, rejectFollowRequest. Approve/reject are
owner-bound: only the followed user can act on their own incoming
requests.
- countFollowers / countFollowing / listFollowers / listFollowing now
filter to accepted-only relations.
Loader (users.$username.tsx):
- Drops the 404 paths. New canSeeContent flag = isOwn ||
profile_visibility='public' || (followState.following === true).
- When canSeeContent=false, render a stub: header + 🔒 badge + body
copy + Request-to-follow / sign-in CTA. Routes/activities sections
are not rendered.
UI:
- FollowButton gains a "Request to follow" / "Requested" state for
private targets via a new isPrivateTarget prop. Cancel-request reuses
the unfollow endpoint.
- New /follows/requests page lists incoming Pending requests with
Approve / Reject buttons.
- New API routes: POST /api/follows/:id/approve and /reject.
- Navbar shows a count badge linking to /follows/requests when
pending > 0.
Privacy manifest already documents the follows relation; no changes
needed (the locked-account semantics don't add new data — same row,
different lifecycle).
Specs / design (social-feed change):
- public-profiles delta rewritten around the four-mode locked model
(public, private+anon, private+pending, private+accepted) with
scenarios for each.
- social-follows delta gains Pending lifecycle requirements (auto vs.
manual accept, approve/reject endpoints, pending request management,
Pending follows do not contribute to feed).
- design.md decision section reflects the new model and rationale for
default-private; non-goal "locked-local-accounts as a follow-up" is
removed since this change ships it.
Tests:
- follow.integration.test.ts: pending-against-private, approve flips
to accepted, reject deletes, owner-bound enforcement.
- e2e/social.test.ts: full Request → Pending → Approve → full-view
flow, plus stub-for-anonymous and /follows/requests auth gate.
Supersedes PR #309 (closed): the empty-public-profile 200 is now a
side-effect of the new render path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the social-feed change end-to-end. Local-only follows
between users on the same instance, an aggregated /feed of public
activities from people you follow, and an explicit profile_visibility
setting so the question "can someone follow me?" has a deterministic
answer.
Schema (additive, drizzle-kit push --force in cd-apps handles it):
- journal.follows table keyed by `followed_actor_iri TEXT` for
federation forward-compat. Local IRIs look like
`${ORIGIN}/users/${username}`. `accepted_at` is nullable so the
Pending state from social-federation slots in without migration.
- journal.users.profile_visibility ('public' | 'private', default
'public'). Existing users land 'public' via the default; current
effective behavior is unchanged.
Server (apps/journal/app/lib):
- actor-iri.ts: localActorIri(username) helper — single source of
truth for IRI construction.
- follow.server.ts: followUser / unfollowUser / getFollowState /
countFollowers / countFollowing / listFollowers / listFollowing.
Refuses self-follow + private targets. Idempotent.
- activities.server.ts: listSocialFeed(followerId, limit) joining
follows → activities WHERE visibility='public', reverse-chrono.
Routes:
- POST /api/users/:username/follow + /unfollow (session-bound)
- /feed (signed-in only; redirects anon to /auth/login)
- /users/:username/followers + /users/:username/following (paginated)
- /users/:username gates on profile_visibility AND has-public-content
for visitors; owners on private get an amber explainer banner.
- /settings adds a Public/Private radio with explainer text.
UI:
- FollowButton component on profile page (hidden for owner + anon).
- Follower/following counts on profile linking to collection pages.
- "Feed" link in nav (signed-in) + on personal dashboard alongside
"New Activity".
Privacy manifest updated to document the new follows relation and
profile_visibility setting.
Tests: follow.integration.test.ts (FOLLOW_INTEGRATION=1) for the
follow lifecycle; e2e/social.test.ts for /feed redirect, follow
button + count transitions, and the profile_visibility 404 toggle.
Local development: run `pnpm db:push` after pulling to apply the
schema additions. Production migrates automatically via cd-apps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The old home was an h1, subtitle, and two auth buttons — visitors
arriving at trails.cool had no idea what it was, and self-hosted
instances had nothing interesting to land on.
The new home is one layout that serves both audiences:
- Hero: a product-describing h1 ("Federated outdoor journal") +
tagline. The site name already lives in the top banner and nav brand,
so the h1 carries the pitch instead of triplicating "trails.cool".
- Auth CTAs: Register (blue) + Sign In (outlined) get primary weight.
Planner demotes to a small "Or try the Planner without an account →"
link below them — it's a nice escape hatch but not the goal.
- Marketing cards: on the flagship only, a 2x2 grid of emoji-icon cards
for Planner / Journal / Federation / Ownership, matching the Planner
home's visual weight. `IS_FLAGSHIP=true` toggles it. Self-hosters get
a "Powered by trails.cool — about the project" link back to the
flagship instead, so they don't have to write their own marketing.
- Public feed: reverse-chronological list of the 20 most recent public
activities on the instance, with owner + distance + date. Empty state
for fresh installs.
Plumbing:
- `listRecentPublicActivities(limit)` in activities.server.ts joins
users so the feed can render "by <displayName>" in one query.
- `IS_FLAGSHIP` env wired through docker-compose.yml; cd-apps and
cd-infra both write `IS_FLAGSHIP=true` alongside `DOMAIN=trails.cool`,
so self-hosters (who deploy without these workflows) default to off.
Specs:
- `activity-feed`: new "Instance-wide public activity feed" requirement
so the visibility contract is pinned (public in feed, unlisted +
private are not).
- `journal-landing`: new small spec capturing the hero / marketing /
feed layout contract and the `IS_FLAGSHIP` toggle, so future
instance-branding or empty-state changes have a stable anchor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Today both proxies are effectively open to anyone who can set an
Origin header for trails.cool — a third party can use us as a free
BRouter/Overpass relay. Require a live planner session on every call
so abuse traffic costs the scraper a session row (observable,
revocable) before they can issue a single query.
Server:
- New `requireSession(id)` helper — returns the session row or a 401
Response. Reused by both route handlers.
- `/api/route`: `sessionId` in body is now required and verified;
rate-limit key always falls back to the session id.
- `/api/overpass`: new `X-Trails-Session` header, verified. Header
keeps the session out of the request body so the body-keyed cache
is unaffected.
Client plumbing:
- `useRouting(yjs, sessionId)` — sessionId goes into the /api/route
body.
- `usePois(sessionId)` → `queryPois(..., sessionId)` → `X-Trails-Session`
on the proxy call.
- `PlannerMap` + `YjsDebugPanel` gain a `sessionId` prop from
`SessionView`.
Journal server-to-server:
- Demo-bot and `/api/v1/routes/compute` now POST `/api/sessions` to
mint a throwaway planner session, then cite it on the forwarded
`/api/route` call. Planner's `expire-sessions` cron cleans these up
(7d window) so nothing needs explicit teardown.
Tests:
- 5 unit tests for `requireSession` covering missing / empty /
non-string / unknown-session / valid-session cases.
- Two integration E2E tests document the 401 for missing session on
each proxy.
- Pre-existing `/api/route` integration tests updated to mint a
session first.
Caveat: existing browser tabs lose their /api/route ability until
reload (the old JS doesn't know to send sessionId). Acceptable for
an anonymous planner.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Surface net for the class of bug we hit in #282 — a POI test silently
relied on a live Overpass server for months because its `page.route`
mock was targeting a URL the browser no longer hit directly. CI stayed
green on coincidence (real OSM data happened to satisfy the "any
marker renders" assertion) until upstream behaviour drifted.
Add a shared Playwright fixture (e2e/fixtures/test.ts) that installs a
catch-all `page.route("**", ...)` before each test. Requests to
localhost + known tile CDNs pass through via `route.continue()`;
anything else is aborted and accumulated. At test teardown, a non-empty
blocked list throws, failing the test with the offending URLs and a
pointer to the fixture.
All seven e2e *.test.ts files updated to import test/expect (+ types)
from ./fixtures/test.
Full suite passes with this fixture in place (50/50 at --workers=2;
catch-all never fires, meaning no test currently relies on an
unallowlisted external service).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The test was added in 0ff04aa (Apr 11), back when the browser called
overpass-api.de directly. When a4df5a4 (Apr 18) moved POI queries
behind the planner's `/api/overpass` proxy, the test's
`page.route("**/api/interpreter", ...)` mock became dead code —
Playwright can only intercept browser traffic, and the browser now
only sees `/api/overpass`. The real upstream got hit and didn't
return a drinking_water node at exactly 52.52, 13.405, so zero
markers rendered and the test failed.
Mock `/api/overpass` instead. Response body shape is unchanged
(the proxy is transparent).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a background demo user (`bruno`) plus two pg-boss jobs that generate
public trekking routes and activities in inner Berlin and prune them
after 14 days. Disabled by default everywhere; flip `DEMO_BOT_ENABLED`
only in prod.
- Schema: `synthetic` boolean on routes + activities
- `ensureDemoUser()` idempotent insert + badge on /users/bruno
- Generation gate: 07-21 Berlin local, p=0.12, 40-route cap in 14d
- Initial 4-walk backfill on first enable; retention via daily prune
- Prometheus gauges `demo_bot_synthetic_routes_total` / `_activities_total`
- Unit + DB-gated integration + E2E tests
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the public-content-visibility OpenSpec change. Adds the
smallest social surface that lets us demo the product to logged-out
visitors without user signup.
Schema:
- `visibility text NOT NULL DEFAULT 'private'` on routes + activities.
- Shared Visibility type exported from the schema module.
Access:
- New canView(content, viewer, { asDirectLink }) helper in auth.server.ts
centralises the rule: public → anyone; unlisted → anyone on direct
link; private → owner only.
- routes.$id and activities.$id loaders return 404 (not 403) when
canView rejects, so existence of private content isn't leaked.
- Detail pages emit Open Graph + Twitter Card meta on public/unlisted
content only.
Editing:
- Visibility <select> on routes/:id/edit with owner-only access.
- Activity detail page gets a small visibility form + set-visibility
action intent (no separate activity-edit page needed).
- EN + DE i18n under routes.visibility.* and activities.visibility.*.
Listings:
- Listing helpers listPublicRoutesForOwner / listPublicActivitiesForOwner
for cross-user queries. Existing owner-scoped listRoutes/listActivities
stay — owners see their own content regardless of visibility.
Public profile:
- /users/:username is now truly public. Renders the user's public
routes + activities, 404s when no public content exists AND viewer
isn't the owner (prevents account enumeration).
- Owner sees a short "this is your profile" note linking to settings.
- Open Graph meta (og:type=profile) for shareable preview.
Privacy manifest:
- Added a bullet noting public content is world-visible on profile
and indexable by search engines.
- Bumped PRIVACY_LAST_UPDATED to 2026-04-20 + rendered legal-archive
snapshot.
Tests:
- 13 unit tests for canView covering the full matrix.
- 6 E2E tests in e2e/public-content.test.ts covering:
- Private route → 404 for logged-out visitor
- Public route → reachable + OG tags present (og:title, og:type,
og:site_name)
- Owner still sees own private content
- Profile 404 when no public content
- Profile renders when at least one public route exists
- Unlisted route reachable via direct URL but hidden from profile
- Test file runs serially (describe.configure mode=serial) to avoid
WebAuthn virtual-authenticator races under Playwright's default
parallel workers.
- New public-content Playwright project added to config.
Rollout safety: every existing row in prod keeps visibility='private'
by default — nothing becomes visible to outsiders until an owner
explicitly opts in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- sentry.client.ts: add stopSentryClient() that awaits Sentry.close() so
the SDK tears down when a user logs out. Hub methods become no-ops
until initSentryClient() is called again on next login.
- root.tsx: call stopSentryClient() from the user-effect when user === null.
- Add sentry-config to journal + planner Dockerfiles (was missing after
#237 introduced the new workspace package, breaking the Dockerfile
package check).
- Footer: replace inner <nav> with <div> — nested nav landmarks broke
the journal "nav bar shows on all pages" e2e test (two navigation
roles on the page).
- e2e auth: tick the required ToS checkbox in the shared registerUser
helper so passkey registration tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Waypoints (3)" assertion timed out at 5s in CI. The waypoints
load from URL params but the sidebar needs BRouter mock response +
Yjs sync before updating. Increase to 15s to match other assertions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
'cycleway' now matches both the highway legend and the Cycleway
dropdown option, causing a strict mode violation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The dummyUsage trick needs a separate assign per tag:
assign dummyUsage2 = maxspeed=
(not appended to the existing smoothness= line)
- Dockerfile: Patch all BRouter profiles to include maxspeed in
WayTags output via separate assign dummyUsage2
- brouter.ts: Extract maxspeed with direction handling
(maxspeed:forward/backward + reversedirection awareness)
- ColoredRoute: maxspeed color mode (green ≤30, yellow ≤50,
orange ≤70, red ≤100, dark red 100+)
- ElevationChart: maxspeed chart rendering, legend, hover "X km/h"
- PlannerMap: maxspeeds state + Yjs read + prop passing
- i18n: EN "Speed Limit" / DE "Tempolimit"
- Mock fixtures: maxspeed data for E2E tests
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
E2E fixes:
- Color mode select locator uses option[value='highway'] filter
instead of fragile page.locator("select").last()
Removed maxspeed color mode:
- BRouter does not expose maxspeed in WayTags output despite the tag
existing in its lookup table. The dummyUsage profile trick only
affects cost calculation, not tiledesc output.
- Can revisit if BRouter adds maxspeed to WayTags in a future version
or if we fork/patch the BRouter profiles more deeply.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
E2E fixes:
- Color mode select locator now uses option[value='highway'] filter
instead of fragile page.locator("select").last()
Speed limit color mode:
- Extract maxspeed tags from BRouter tiledesc (with forward/backward
direction handling following brouter-web's pattern)
- Color route and chart by speed limit: green ≤30, yellow ≤50,
orange ≤70, red ≤100, dark red 100+ km/h
- Legend with speed thresholds, hover shows "X km/h"
- i18n: EN "Speed Limit" / DE "Tempolimit"
- Mock fixtures include maxspeed data
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract highway=* tags from BRouter tiledesc messages alongside surface
data and add a new "Road Type" color mode that colors the route polyline
and elevation chart by OSM highway classification (cycleway, residential,
path, etc.). Includes color palette, legend, hover labels, i18n (EN+DE),
unit tests for tag extraction, and E2E tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The color mode selector moved from the header to the elevation chart,
which only renders when a route exists. Updated test to use mocked
BRouter with waypoints.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CodeMirror renders placeholders as .cm-placeholder elements, not
HTML placeholder attributes. Updated the sidebar tabs test to look
for the CodeMirror content area instead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The test dropped GPX with far-apart waypoints (Berlin/Dessau/Erfurt)
which timed out waiting for BRouter on CI. Switched to nearby Berlin
waypoints and added mockBRouter for instant deterministic routing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The app is primarily for cycling. Default profile is now "Cycling (fast)"
instead of "Hiking". Profile order: fastbike, safety, shortest, car,
trekking.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Hillshading: mock tile endpoint, enable via DOM evaluate, verify
tile requests fired
- POI markers: mock Overpass API response, zoom to threshold, enable
category, verify marker rendered in marker pane
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- brouter-mock.ts: Canned route responses for 2 and 3 waypoint sessions,
plus latLngToPixel helper for pixel-precise map interactions
- planner.test.ts: Route split, map zoom, and overnight tests now use
mockBRouter() for instant deterministic responses instead of real
BRouter — fixes all 3 flaky tests
- Used .first() for sidebar km locator to avoid strict mode violations
when both header summary and stats footer match
All 38 E2E tests pass in 9.7s (was 34s+ with real BRouter).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The test used waypoints 300km apart causing BRouter timeouts. Switched
to nearby Berlin waypoints for fast route computation. Scoped all
locators to the sidebar to avoid strict mode violations from map
elements matching the same text.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- "km" locator matched both header summary and stats footer — narrowed
to match "km ·" pattern unique to the header
- "Day 1" locator matched both map label pill and sidebar header —
scoped to sidebar with page.locator("aside")
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- planner.test.ts: E2E test for overnight toggle → day breakdown in sidebar,
and GPX import with overnight metadata
- integration.test.ts: E2E test for isDayBreak preservation through GPX import
- RouteMapThumbnail: Per-day route coloring with alternating colors
- overnight.test.ts: Y.Doc-aware tests for isOvernight/setOvernight
- daybreaks-extraction.test.ts: GPX → dayBreaks index extraction tests
All 28 tasks complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- /settings with profile, security, and account sections
- Profile: edit display name and bio
- Security: list passkeys with device labels, add/delete with
last-passkey warning
- Account: email change with magic link verification, account
deletion with username confirmation
- Settings link in nav for authenticated users
- E2E tests: auth guard, profile editing, passkey add/delete,
last-passkey warning, account deletion, nav visibility
- i18n: full en + de translations for all settings UI
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Uses Playwright's virtual WebAuthn authenticator (CDP) to test:
- Register with passkey + sign in (full round-trip)
- Login fails with no credential (friendly error message)
- Duplicate email rejection
- Duplicate username rejection
Would have caught the credential encoding bug.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Enrich BRouter response with per-point 3D coordinates and segment boundary
tracking (EnrichedRoute interface)
- ColoredRoute component: plain, elevation gradient (green→yellow→red), and
surface color modes with invisible wide polyline for click targeting
- Click-to-split: click on route polyline inserts waypoint at nearest point,
mapped to correct segment via boundary indices
- MidpointHandles: draggable CircleMarkers at route segment midpoints for
reshaping, hidden below zoom 12, opaque on hover
- Color mode toggle (select) synced via Yjs routeData
- i18n keys for color mode labels (en + de)
- Unit tests for segment boundary tracking (13 tests)
- E2E tests for enriched route response and color mode toggle
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Transactional emails:
- Add nodemailer SMTP email module with dev-mode console logging
- Magic link template and welcome template with HTML + plain text
- Wire sendMagicLink into login flow, sendWelcome into registration
- Update privacy page and deploy docs for SMTP configuration
Planner features:
- No-go areas: draw polygons on map (leaflet-geoman), synced via Yjs,
passed to BRouter as nogos parameter, route recomputes on change
- Session notes: collaborative Y.Text textarea in sidebar tab
- Crash recovery: periodic localStorage save of Yjs state, restore on reconnect
- Rate limit session creation (10/IP/hour) in /new route
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Journal gets a nav bar in root.tsx with conditional links: Routes and
Activities for authenticated users, Sign In and Register for guests.
Active route is highlighted via useLocation(). User menu shows username
linking to profile and a logout button.
Planner SessionView header title becomes a home link back to /.
Adds i18n keys (en + de) for all nav labels. Updates E2E tests to use
scoped nav selectors now that links appear in both nav bar and page body.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace blank home page with a landing page explaining collaborative
route planning. Hero section with "Start Planning" CTA, 5 feature cards
(collaboration, routing, elevation, GPX, no account), secondary CTA
linking to Journal, and footer with privacy/source/attribution links.
All strings in en + de via i18n.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
"Register" appears 3 times on the page (heading, button text, link).
Use getByRole("heading") to target the h1 specifically.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The i18n change renamed "Create Account" to "Register" — update the
E2E test to match.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CI now runs the full stack for E2E tests:
- PostgreSQL as a service container + schema push via drizzle
- BRouter JAR + Berlin segment (E10_N50) with caching
- Both cached across runs (BRouter JAR + segment)
Removed all skip/workaround logic from tests — all 20 E2E tests
now run in CI with real services.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Integration test DB check now hits Planner API (not Journal)
so it correctly skips when no PostgreSQL in CI
- Playwright uploads HTML report artifact on all runs (not just failures)
- Install Chromium with --with-deps for CI compatibility
- Report retained for 30 days
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Planner session tests and integration tests now check for DB
availability before running. Skip gracefully in CI where no
PostgreSQL or BRouter is available.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>