Commit graph

153 commits

Author SHA1 Message Date
Ullrich Schäfer
0fdcbf77d6
Merge pull request #36 from trails-cool/fix-cd-trigger
Fix CD: trigger after automerge + manual dispatch
2026-03-25 01:18:56 +01:00
Ullrich Schäfer
46c880973d
Fix CD: trigger on automerge + manual dispatch
GITHUB_TOKEN merges (from automerge action) don't trigger push
events on other workflows. Added:
- workflow_run trigger after Automerge completes
- workflow_dispatch for manual gh workflow run cd.yml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 01:18:12 +01:00
github-actions[bot]
14656b6527
Merge pull request #35 from trails-cool/fix-docker-builds 2026-03-25 00:12:43 +00:00
Ullrich Schäfer
0a09df27f4
Fix Docker builds: missing deps + bump actions
Dockerfiles:
- Add db package.json to deps stage
- Copy full node_modules (including app-level) to runtime
- Copy packages/ dir for workspace imports at runtime

CD workflow:
- docker/login-action v3 → v4
- docker/build-push-action v6 → v7

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 01:10:01 +01:00
Ullrich Schäfer
21eb943393
Merge pull request #34 from trails-cool/deploy-to-hetzner
Enable CD deployment to Hetzner
2026-03-25 01:02:45 +01:00
Ullrich Schäfer
8e2f183288
Enable CD deployment to Hetzner (task 11.8)
CD workflow:
- Uncommented deploy step
- Copy docker-compose.yml + Caddyfile to server via SCP
- Login to ghcr.io with DEPLOY_GHCR_TOKEN (classic PAT, read:packages)
- Download Germany BRouter segments on first deploy (~750MB)
- Pull images, restart services, verify

Infrastructure:
- BRouter segments as bind mount (./segments) not Docker volume
- Added ORIGIN, PLANNER_URL, SESSION_SECRET, NODE_ENV to Journal
- Added NODE_ENV to Planner
- Disabled Garage for now (no media storage yet)

Required secrets: DEPLOY_HOST, DEPLOY_SSH_KEY, DEPLOY_GHCR_TOKEN

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 01:00:00 +01:00
github-actions[bot]
970f591a60
Merge pull request #33 from trails-cool/fix-playwright-summary 2026-03-24 23:48:55 +00:00
Ullrich Schäfer
6d8e570afc
Fix Playwright job summary: handle nested suite structure
Playwright JSON has file → describe → specs nesting.
Use stats.expected/unexpected for counts, iterate nested suites.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 00:46:21 +01:00
github-actions[bot]
a09f41e03d
Merge pull request #32 from trails-cool/playwright-job-summary 2026-03-24 23:41:02 +00:00
Ullrich Schäfer
074bcff4e5
Add Playwright job summary to CI
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>
2026-03-25 00:38:38 +01:00
Ullrich Schäfer
19ce2d1f1e
Merge pull request #31 from trails-cool/fix-automerge-workflow-run
Fix automerge: trigger after CI completes
2026-03-25 00:35:24 +01:00
Ullrich Schäfer
052ff6ac20
Fix automerge: add workflow_run trigger after CI completes
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>
2026-03-25 00:32:22 +01:00
Ullrich Schäfer
e4d4c790b3
Merge pull request #30 from trails-cool/ci-e2e-services
Add PostgreSQL + BRouter to CI E2E tests
2026-03-25 00:29:26 +01:00
Ullrich Schäfer
70d9d71a5c
Fix CI: wait for PostGIS extension before schema push
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>
2026-03-25 00:21:25 +01:00
Ullrich Schäfer
1d45c57ae7
Cache PostGIS Docker image in CI
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>
2026-03-25 00:17:30 +01:00
Ullrich Schäfer
f2f5c6a83e
Cache Playwright browsers in CI
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>
2026-03-25 00:15:26 +01:00
Ullrich Schäfer
f0afa8a4a7
Fix withDb: detect DataWithResponseInit from data() throws
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>
2026-03-25 00:11:48 +01:00
Ullrich Schäfer
dee6f2806f
Add root ErrorBoundary to both apps, simplify withDb
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>
2026-03-25 00:06:10 +01:00
Ullrich Schäfer
83009c2b9b
Fix withDb: re-throw React Router ErrorResponseImpl
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>
2026-03-25 00:03:15 +01:00
Ullrich Schäfer
b71117e4ca
Add PostgreSQL + BRouter to CI E2E tests
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>
2026-03-24 23:58:01 +01:00
Ullrich Schäfer
cd6f6a6f18
Merge pull request #29 from trails-cool/planner-journal-handoff
Groups 10-11: Activity feed and E2E tests
2026-03-24 23:51:13 +01:00
Ullrich Schäfer
7d273b73c0
Fix CI E2E: check Planner DB, add Playwright HTML report
- 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>
2026-03-24 23:47:43 +01:00
Ullrich Schäfer
2d0a4ac2c7
Fix withDb: catch all non-Response errors as DB failures
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>
2026-03-24 23:43:10 +01:00
Ullrich Schäfer
8fb7771223
Add withDb() helper for graceful DB error handling
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>
2026-03-24 23:39:33 +01:00
Ullrich Schäfer
cadcf753a7
Gracefully handle missing DB in Planner routes
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>
2026-03-24 23:37:05 +01:00
Ullrich Schäfer
e86ff5a8a3
Fix E2E tests in CI: skip DB-dependent tests when no PostgreSQL
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>
2026-03-24 23:33:03 +01:00
Ullrich Schäfer
79e6ae6ea2
Add E2E and integration tests (Group 11)
Journal tests (6):
- Home page, auth pages render correctly
- No password fields on register/login
- Protected routes redirect to login

Planner tests (9):
- Session creation via API
- Map loads in session
- Yjs WebSocket connects (Connected status)
- Profile selector, export button, waypoint sidebar
- Session with initial GPX waypoints
- Expired session returns 404

Integration tests (5):
- GPX import returns parsed waypoints
- BRouter computes Berlin routes
- Routes pass through all waypoints (segment by segment)
- Rate limit headers present
- Rejects < 2 waypoints

Total: 32 unit tests + 20 E2E tests, all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:20:04 +01:00
Ullrich Schäfer
a9c99844c6
Implement Journal activity feed (Group 10)
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>
2026-03-24 23:20:04 +01:00
Ullrich Schäfer
a7c4a0759b
Merge pull request #27 from trails-cool/update-claude-md-automerge-warning
Update claude md automerge warning
2026-03-24 23:14:19 +01:00
Ullrich Schäfer
21e0d3633a
Merge pull request #26 from trails-cool/planner-journal-handoff
Implement Planner-Journal handoff (Group 9)
2026-03-24 21:25:20 +01:00
Ullrich Schäfer
545469eae0 Fix SSR hydration mismatch for dates
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>
2026-03-24 20:22:58 +00:00
Ullrich Schäfer
14b179bb0c Fix waypoint loading in Edit in Planner flow
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>
2026-03-24 20:22:58 +00:00
Ullrich Schäfer
2e2dbe05c1 Fix GPX export: use API route with direct download link
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>
2026-03-24 20:22:58 +00:00
Ullrich Schäfer
155d36e94c Fix Edit in Planner: create session via API, avoid URL size limit
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>
2026-03-24 20:22:58 +00:00
Ullrich Schäfer
8559b89be6 Fix Edit in Planner: use client-side redirect for cross-origin
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>
2026-03-24 20:22:58 +00:00
Ullrich Schäfer
3567dac259 Allow repeated saves to Journal from Planner
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>
2026-03-24 20:22:58 +00:00
Ullrich Schäfer
40e541fcce Implement Planner-Journal handoff (Group 9)
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>
2026-03-24 20:22:58 +00:00
Ullrich Schäfer
f47eca565c
Merge pull request #25 from trails-cool/journal-routes
Implement Journal route management (Group 8)
2026-03-24 21:22:44 +01:00
Ullrich Schäfer
edd714fbb1
Implement Journal route management (Group 8)
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>
2026-03-23 22:25:10 +01:00
Ullrich Schäfer
9ce90eb550
Merge pull request #24 from trails-cool/journal-auth
Implement Journal auth: passkeys + magic links
2026-03-23 22:20:56 +01:00
Ullrich Schäfer
cc308bc17f
Complete Journal auth: add passkey prompt, dev magic link, fix validation
- 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>
2026-03-23 22:19:17 +01:00
Ullrich Schäfer
6950fd7de5
Fix username validation: strip invalid chars instead of pattern error
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>
2026-03-23 21:51:34 +01:00
Ullrich Schäfer
2215099501
Fix auth: separate API routes, verify registration with real DB
- 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>
2026-03-23 17:55:49 +01:00
Ullrich Schäfer
b65b61cbc5
Implement Journal auth: passkeys + magic links, no passwords
Auth flows:
- Passkey registration: email + username → WebAuthn create → account + session
- Passkey login: WebAuthn get → session (instant, no form)
- Magic link fallback: email → token → verify link → session
- Logout via POST /auth/logout

Infrastructure:
- auth.server.ts: WebAuthn (SimpleWebAuthn), magic tokens, cookie sessions
- DB schema: credentials (WebAuthn), magic_tokens tables, no password_hash
- Session middleware via getSessionUser()

Pages:
- /auth/register — email + username + passkey creation
- /auth/login — passkey button + magic link fallback
- /auth/verify — magic link token verification
- /users/:username — public profile page
- Home page shows auth state (register/signin or welcome)

Dynamic imports for @simplewebauthn/browser to avoid SSR issues.
Magic links logged to console in dev (email integration later).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 17:38:46 +01:00
Ullrich Schäfer
3604a2d066
Update auth spec: passkeys + magic links, no passwords
- 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>
2026-03-23 17:24:27 +01:00
Ullrich Schäfer
afb413b026
Merge pull request #23 from trails-cool/planner-ui-fixes
Fix waypoint icons, add elevation chart scrubbing
2026-03-23 12:03:54 +01:00
Ullrich Schäfer
288e9c07f2
Fix cross-window Yjs sync: broadcast doc updates to all connections
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>
2026-03-23 08:25:25 +01:00
Ullrich Schäfer
55806fc84a
Fix waypoint drag, host election, segment routing, debug panel
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>
2026-03-23 08:52:52 +01:00
Ullrich Schäfer
2e5c31d117
Add animated progress bar during route computation
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>
2026-03-23 08:34:34 +01:00
Ullrich Schäfer
6ee07f0570
Fix waypoint icons and add elevation chart scrubbing
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>
2026-03-23 07:04:04 +01:00