Generates a markdown summary table in GitHub Actions showing
passed/failed tests with timing per test. Uses the JSON reporter
to output results, then a Node script to format as markdown
and append to $GITHUB_STEP_SUMMARY.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
check_suite and status events don't reliably trigger for workflows
on the same commit (GitHub prevents infinite loops). Replaced with
workflow_run on CI completion, which fires after all CI jobs finish.
Added comments explaining why each trigger group exists.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
pg_isready reports true before PostGIS extension is initialized.
Added a second wait loop that queries PostGIS_Version() to ensure
the extension is fully loaded before running drizzle-kit push.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace service container with manual docker run + cached image tar.
Service containers pull the image before steps run (uncacheable).
Manual approach: cache the image as a tar, load on cache hit.
PostGIS image is ~400MB — first run pulls and saves, subsequent
runs load from cache in ~2s instead of ~19s.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Playwright browser download (25s) is now cached by pnpm-lock hash.
On cache hit, only install system deps (fast). On miss, full install.
Current E2E timings (first run):
- Postgres container: 19s
- BRouter JAR: cached
- Berlin segment: 6s (will be cached next run)
- Playwright install: 25s → cached after first run
- BRouter startup: 9s
- Tests: 27s
Expected after caching: ~60s total (down from ~107s).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
React Router's data() throw creates a DataWithResponseInit object
(type + data + init), not a Response or ErrorResponseImpl.
Check for type === "DataWithResponseInit" to re-throw correctly.
Verified: nonexistent session → 404, DB down → 503.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both apps now have proper ErrorBoundary exports in root.tsx:
- 404: "Page not found"
- 503: "Service temporarily unavailable"
- Other: shows error message
Simplified withDb re-throw check: anything with a "status" property
is treated as a React Router response (covers Response and
ErrorResponseImpl). DB errors throw a plain Response(503) that
the error boundary renders nicely.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
React Router's data() throws ErrorResponseImpl, not Response.
Check for objects with status + data properties to catch both
Response and ErrorResponseImpl, preventing withDb from
swallowing 404s as 503s.
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>
The previous version checked for specific error messages that
didn't match Drizzle's actual error text ("Failed query").
Now any error from a withDb-wrapped handler returns 503.
React Router Response objects are still re-thrown.
Verified: DB down → 503 JSON, DB up → 201, home page always 200.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Shared withDb() wrapper in @trails-cool/db catches database
connection errors and throws a 503 Response instead of crashing
the server. Re-throws React Router responses (redirects, data()).
Applied to Planner session routes — replaces manual try/catch.
Any route can use withDb(() => ...) for automatic 503 on DB failure.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wrap session API and loader in try/catch to return 503 instead
of crashing the server when PostgreSQL is unavailable. This
prevents E2E test failures in CI where no DB is running.
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>
Activity CRUD:
- Activity list page (/activities) with date, distance, elevation
- Create activity with GPX upload, description, optional route link
- Activity detail with stats and date
- "Link to Route" action (select from existing routes)
- "Create Route from Activity" action (creates route from GPX)
Server logic in activities.server.ts with async GPX parsing.
Uses ClientDate component for SSR-safe date rendering.
All 6 Group 10 tasks complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Created ClientDate component that renders dates client-only via
useEffect to prevent server/client locale mismatches. Server
renders with de-DE as default, client updates to user's locale.
Applied to route detail (version dates) and route list (updated dates).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three issues fixed:
1. GPX parser used browser DOMParser which doesn't exist in Node/Vite SSR.
Added async parseGpxAsync() using linkedom for server-side parsing.
2. Server-side session initialization stored waypoints in a Yjs doc
instance separate from the Vite WebSocket plugin's doc store.
Moved waypoint initialization to client-side: API returns parsed
waypoints, client adds them to Yjs after sync.
3. GPX was encoded in URL params causing HTTP 431. Now the Journal
creates a Planner session via API (POST body), and only passes
compact waypoint coordinates in URL params.
Verified: Journal route → Edit in Planner → 4 waypoints loaded,
route computed (79.3km), elevation profile, Save to Journal ready.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
React Router page actions wrap responses, preventing raw file
downloads. Moved GPX export to GET /api/routes/:id/gpx and
changed the button to a plain <a download> link.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GPX data was being encoded in URL params, causing HTTP 431 (Request
Header Fields Too Large). Now the Journal creates a Planner session
via POST to /api/sessions with GPX in the request body, then
redirects to the session URL (154 chars vs 100KB+).
Also moved edit-in-planner logic to dedicated API route for proper
JSON response handling.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
React Router's redirect() can't navigate to a different origin
(localhost:3001). Return the Planner URL as data and redirect
via window.location.href on the client.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Save button stays visible after saving — users can keep editing
and save again. "Saved!" shows as inline confirmation, "Return to
Journal" link appears alongside the save button.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
JWT-based handoff between Journal and Planner:
Journal side:
- JWT token generation scoped to route_id with 7-day expiry (jose)
- "Edit in Planner" button on route detail page — generates JWT,
redirects to Planner /new with callback URL, token, and GPX
- Callback endpoint (POST /api/routes/:id/callback) validates JWT
and creates new route version from received GPX
Planner side:
- /new route accepts callback, token, returnUrl, gpx params
- Creates session with callback metadata, initializes with GPX
- "Save to Journal" button POSTs GPX with Bearer token to callback
- "Return to Journal" link shown after successful save
All 6 Group 9 tasks complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Route CRUD:
- Create route with name, description, optional GPX upload
- Route detail page with stats (distance, elevation), version history
- Edit route (name, description, upload new GPX → creates new version)
- Delete route with confirmation dialog
- Route list page sorted by last updated
GPX handling:
- Import: parse GPX, compute distance/elevation stats on save
- Export: download route GPX as file
- Version history: new version created on each GPX update
Server logic in routes.server.ts with full Drizzle ORM queries.
All 10 Group 8 tasks complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add passkey prompt after magic link login (/?add-passkey=1)
- addPasskeyStart/addPasskeyFinish for existing users
- Dev mode: magic link auto-redirects (returns devLink in response)
- Fix username validation: strip invalid chars instead of HTML pattern
- Separate API routes from page routes for JSON fetch compatibility
All 10 Group 7 tasks complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Browser's native pattern validation fired before preventDefault,
showing "The string did not match the expected pattern" error.
Replace HTML pattern with onChange that strips invalid characters.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move auth logic from page actions to dedicated API routes
(/api/auth/register, /api/auth/login) — React Router page
actions don't handle JSON fetch() properly
- Remove server imports from page components
- Verify registration start works end-to-end with PostgreSQL:
WebAuthn challenge generated, user ID created, RP set to localhost
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Passkey (WebAuthn) as primary auth for registration and login
- Magic link email as fallback for new devices
- Removed password_hash from users table
- Added credentials (WebAuthn) and magic_tokens tables
- Updated tasks (7.1-7.10)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The Vite Yjs plugin was only sending sync responses back to the
sender, not broadcasting updates to other WebSocket connections
in the same room. This caused cross-window sync to only work via
BroadcastChannel (same browser context), not across separate
browser windows/incognito.
Fix: Add doc.on('update') listener that broadcasts updates to all
connections in the room except the origin sender.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Waypoint drag:
- Use observeDeep instead of observe to detect Y.Map property changes
Host election:
- Extracted to host-election.ts with 8 unit tests
- Simple rule: lowest real client ID is always host, deterministic
- Always sets role (host or participant), no missing roles
- Filters server docs (empty awareness state) from election
BRouter routing:
- Route segment-by-segment like bikerouter.de (one request per
waypoint pair), guarantees route passes through every waypoint
- Merge GeoJSON segments with accumulated stats
- 8 unit tests for segment merging and waypoint splitting
Awareness cleanup:
- Track client IDs per WebSocket connection
- Immediately remove awareness on WebSocket close
Debug panel:
- Visibility + expanded state persisted in localStorage
- Hotkey (Shift+Cmd+Option+T) toggles visibility
- Labels server docs as (server)
- All logic self-contained in YjsDebugPanel.tsx
User identity:
- Color and name persisted in localStorage across reloads
Tests: 32 total (16 existing + 8 host election + 8 routing)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Blue sliding progress bar appears at the top of the map while
BRouter is computing a route. Overlays on top of Leaflet map
with z-index 1000.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Waypoint icons:
- Replace broken Leaflet default icons with numbered blue circles
- Numbers match sidebar order (1, 2, 3...)
Elevation chart:
- Mouse hover shows crosshair, elevation, and distance at cursor
- Red dot appears on the map at the corresponding route position
- Dot disappears when mouse leaves the chart
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use the full set of triggers from pascalgn/automerge-action docs:
pull_request (labeled, synchronize, opened, etc), pull_request_review,
check_suite, and status events.
Replaces workflow_run trigger which wasn't firing reliably after
the label was added before CI completed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use the full set of triggers from pascalgn/automerge-action docs:
pull_request (labeled, synchronize, opened, etc), pull_request_review,
check_suite, and status events.
Removes workflow_run trigger which wasn't firing reliably.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add Vite plugin implementing y-protocols sync server for dev mode
- Fix useYjs hook: move provider creation into useEffect to survive
React StrictMode remounts
- Fix WebSocket URL: use /sync as base (y-websocket appends room name)
- Verified in browser: Connected status, Host badge, awareness working
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Added Y.Map observer on routeData that triggers route recomputation
when the 'profile' key changes. Previously profile changes synced
via Yjs but didn't trigger a new BRouter request.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>