Dashboard: switch error rate panel from app-level http_request_duration
(never observed) to Caddy's caddy_http_response_duration_seconds_count.
Journal: add custom server.ts (matching planner pattern) that observes
http_request_duration_seconds on every request. Replaces react-router-serve
with a custom Node HTTP server that handles /api/health, /api/metrics,
static assets, and request timing. Removes redundant React Router routes
for health and metrics.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Some Wahoo workouts (indoor, manual entries) don't have a FIT file URL.
Both the manual import action and webhook handler called downloadFile
unconditionally, throwing "No file URL for workout". Now skips the
download and creates the activity without GPX when no file is available.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Yjs UndoManager tracks waypoints, no-go areas, and notes. Only
local mutations (origin "local") are undoable — remote changes
from other users are not.
- Keyboard shortcuts: Ctrl/Cmd+Z (undo), Ctrl/Cmd+Shift+Z/Y (redo)
- Shortcuts suppressed in text inputs (browser-native undo there)
- Undo/redo buttons in header with disabled state
- stopCapturing on drag to isolate drag as one undo step
- All mutation sites wrapped with "local" origin
- 5 unit tests for UndoManager behavior
- Archived undo-redo change
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Route/activity list pages: map thumbnails with route drawn on OSM tiles
- Route/activity detail pages: interactive Leaflet map with zoom controls
- Server: expose GeoJSON from PostGIS via ST_AsGeoJSON (simplified for lists)
- RouteMapThumbnail component: shared, supports thumbnail and interactive modes
- Placeholder shown for routes/activities without geometry
- Archive journal-route-previews change, sync specs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In dev mode, the sessions API and the Yjs WebSocket server are
separate processes (Vite plugin vs React Router action), so
server-side Yjs doc initialization doesn't reach the client.
Now no-go areas flow the same way as waypoints:
API response → URL params → client-side Yjs initialization.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Save to Journal now includes waypoints and no-go areas alongside
the track — the full planning state needed for round-tripping.
Export Route remains the clean track-only option for other apps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Save to Journal now includes no-go areas in GPX extensions so they
round-trip correctly: Planner → Journal → Edit in Planner preserves
the no-go polygons.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add description text below each export option explaining the difference
- Fix z-index (z-[1001]) so dropdown renders above the map
- Align dropdown left instead of right to avoid overflow
- Widen dropdown (w-56) to fit descriptions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The document click handler registered on open could catch the same
click event that opened the dropdown, immediately closing it.
Using mousedown avoids the race since it fires before click.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The chevron was too narrow (px-1.5 → px-2.5) and clicks were
caught by the outside-click handler before toggling the dropdown.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split button with default "Export GPX" (track only) and dropdown:
- Export Route: clean track for use in any app
- Export Plan: track + waypoints + no-go areas in GPX extensions
(trails:planning namespace) for reimporting into the planner
Also:
- Add no-go area serialization to generateGpx (GPX extensions)
- Parse no-go areas from GPX extensions on import
- Initialize no-go areas in Yjs session from imported GPX
- Save to Journal now exports track only (consistent with Export Route)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The button was rendered as a React component, not a Leaflet control,
so clicks propagated to the map. Use L.DomEvent.disableClickPropagation
on the container — same approach Leaflet's built-in controls use.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BRouter supports a `polygons` parameter with full vertex coordinates.
We were approximating polygons as circles (centroid + max radius),
which poorly represented elongated or concave shapes.
Now passes polygon vertices directly — no approximation, exact
no-go boundaries.
Also removes unused haversineMeters helper and updates the spec.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Vite's dev server can re-evaluate modules, causing prom-client
"already registered" errors for all metrics, not just default ones.
Use getOrCreate pattern to reuse existing metrics from the registry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Importing metrics.server.ts from yjs-server.ts caused
collectDefaultMetrics() to re-register during HMR, crashing
the planner dev server. Check if metrics already exist first.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
plannerActiveSessions and plannerConnectedClients were registered
but never updated — they always reported 0. Now incremented on
WebSocket connect and decremented on close.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both new.tsx and api.sessions.ts had duplicated track-segment
waypoint extraction logic. Moved to packages/gpx as
extractWaypoints(gpxData) — uses <wpt> elements when present,
falls back to start of each track segment + end of last.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Same fix as new.tsx — the journal→planner handoff also only used
<wpt> elements from GPX. Now both code paths extract waypoints
from track segments when no explicit waypoints exist.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The planner's GPX import only used <wpt> elements, which many GPX
files don't have. Now falls back to the first and last track point,
giving the planner two endpoints to route between.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
parseGpx (sync) needs browser DOMParser which doesn't exist on the
server — it silently failed in every server-side caller. Removed the
export and migrated all callers to parseGpxAsync. Updated tests.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Distance was only stored in the elevation profile array, which was
empty when track points had no <ele> elements. Now computed via
haversine independently and exposed as gpxData.distance.
Tested: berlin-dresden-radweg-2021.gpx (5660 points, no elevation)
now correctly reports 248.8 km.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The initial approach (#150) didn't work because:
1. routes.server.ts used parseGpx (sync, needs browser DOMParser)
instead of parseGpxAsync (uses linkedom on Node.js) — GPX parsing
silently failed, so stats AND geom were never populated
2. Drizzle's customType doesn't pass SQL expressions through toDriver
Fixes:
- Switch to parseGpxAsync for all server-side GPX parsing
- Use db.execute() with raw ST_GeomFromGeoJSON SQL after insert
- Remove unused lineStringFromCoords helper from schema
- Share setGeomFromGpx between routes and activities
- Add unit tests for GPX→GeoJSON coordinate extraction
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract coordinates from parsed GPX tracks and store as PostGIS
LineString geometry via ST_GeomFromGeoJSON. Applied to:
- createRoute / updateRoute (routes.server.ts)
- createActivity / createRouteFromActivity (activities.server.ts)
Adds lineStringFromCoords() helper to the journal schema that
builds the SQL expression from [lon, lat] coordinate pairs.
Unblocks spatial queries (e.g. route discovery via ST_Intersects).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
invalidateSize() + requestAnimationFrame ensures Leaflet knows
the map's actual dimensions after the elevation chart renders.
Also removes unnecessary filter on already-typed coordinates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Show error toasts when route calculation fails, with distinct messages:
- "No route found" for BRouter 4xx (unroutable waypoints, missing tiles)
- "Route calculation failed" for server errors / network failures
- "Too many requests" for rate limiting
- Rollback all waypoints to last known good state on route failure
- Guard against restore→recompute loop via restoringRef flag
- Extract reusable useToasts hook from awareness toast logic
- Add BRouterError class to distinguish client vs server errors
- API returns 422 for unroutable requests instead of blanket 502
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The ghost marker (route split indicator) was interfering with waypoint
dragging because it sat above waypoints (zIndexOffset 1000) and stayed
active during drag operations.
Adopt brouter-web's suspension pattern:
- Suspend route interaction on waypoint mouseover (not just dragstart),
unsuspend on mouseout — ghost marker hides before any click/drag
- Lower ghost marker zIndexOffset to -100 so waypoints always receive
clicks first
- Keep drag guards as a safety net
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Now that both server and client use the same locale from context,
the post-hydration useEffect that re-formatted with the browser's
OS locale was causing a visible flicker. Just render with the
context locale directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add detectLocale() that extracts the full locale tag (e.g. "de-DE")
from the request, not just the language. The journal root loader passes
it via LocaleContext so ClientDate renders with the correct locale on
both server and client — no more hardcoded "en-US" fallback.
Also fixes settings page hydration mismatch by using ClientDate for
passkey creation dates instead of inline toLocaleDateString().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split initI18n() into separate server/client initialization paths
inspired by remix-i18next's architecture:
- initI18nServer(lng): no LanguageDetector, per-request language from
Accept-Language header
- initI18nClient(): LanguageDetector reads <html lang> first to match
server, then localStorage and navigator
- detectLanguage(request): parses Accept-Language for best match
Fixes rehydration errors caused by v26 removing initImmediate option
(replaced with initAsync) and LanguageDetector running on the server
where browser APIs don't exist.
Also makes <html lang> dynamic instead of hardcoded "en".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The prompt now only appears when coming from signup/login with
?add-passkey=1 AND the user has zero passkeys. Passkey count display
removed from home — that info lives in /settings now.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add `purpose` column (default: 'login') to distinguish login tokens
from email-change tokens
- verifyMagicToken only matches purpose='login'
- verifyEmailChange only matches purpose='email-change'
- Re-check email availability at verification time, not just initiation
— prevents race where someone registers with the new email between
request and verification
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>
- Login: hide passkey button when browser lacks WebAuthn, default to
magic link mode, hide "back to passkey" link
- Register: detect passkey support and offer magic link registration as
fallback. New server-side registerWithMagicLink() creates account
without credential and sends verification email.
- Home: show passkey count for logged-in users, show amber unsupported
message instead of hiding the add-passkey prompt entirely
- i18n: add passkeyNotSupported, passkeyNotSupportedRegister,
registerWithMagicLink, passkeyStatus keys (en + de)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bug: @simplewebauthn v13 changed credential.id from Uint8Array to
base64url string. Buffer.from(string) without encoding stored the
ASCII text instead of decoded bytes. Authentication then failed to
match because it correctly decoded with "base64url".
Fix: Buffer.from(credential.id, "base64url") in both registration
and add-passkey flows.
Also: catch WebAuthn "not allowed" errors and show a friendly message
instead of the raw browser error.
Existing passkeys must be re-registered after deploy.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@trails-cool/db doesn't export a db instance — only createDb and withDb.
The health check was importing a non-existent export, always failing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Removes replayIntegration and replay sample rates from both apps.
Quota was already exhausted.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Returns SENTRY_RELEASE (set at build time) as the version field,
making it easy to verify which commit is deployed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The route files existed but weren't registered in the explicit routes.ts,
so React Router never compiled them into the build.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
node:25-slim doesn't include CA certificates, causing sentry-cli
SSL errors: "unable to get local issuer certificate". Also removes
debug logging from previous troubleshooting.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>