Android emulator has its own network stack — localhost refers to the
emulator, not the host. Use 10.0.2.2 (host loopback alias) on Android.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- prebuild: generate native ios/ and android/ projects
- prebuild:clean: regenerate from scratch
- run:ios / run:android: build and run locally with Xcode/Gradle
- Add ios/ and android/ to .gitignore (CNG-generated)
Allows building locally without EAS credits:
pnpm --filter @trails-cool/mobile prebuild
pnpm --filter @trails-cool/mobile run:ios
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace custom View-based bottom sheet with @gorhom/bottom-sheet:
- Native gesture-driven drag, snap points, pan-down-to-close
- Proper safe area handling built in
- Wrap app with GestureHandlerRootView
- Add react-native-reanimated plugin to Expo config
Adds react-native-reanimated and react-native-gesture-handler as
native dependencies (requires new EAS build).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Close button was overlapping the home indicator. Now uses
useSafeAreaInsets() for dynamic bottom padding.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The route polyline wasn't visible because segments were initialized
empty. Now extractSegmentsFromGpx() parses trkpt coordinates from
the GPX string via regex on init, so the route shows immediately
in both view and edit modes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The journal doesn't have an app/jobs directory yet — that will arrive
with the Komoot import PR. The COPY line was added prematurely and
breaks the Docker build.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a script that verifies every packages/*/package.json is listed in
all app Dockerfiles. Catches the recurring issue where adding a new
workspace package breaks production because the Dockerfile deps stage
doesn't know about it.
Also fixes two pre-existing missing packages in the planner Dockerfile
(api, map-core) caught by the new check.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Both Dockerfiles were missing packages/jobs/package.json in the deps stage,
causing pnpm install to skip pg-boss. Also copy app/jobs directories into
the runtime stage so job handlers are available at runtime.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add @trails-cool/jobs package wrapping pg-boss for durable background job
execution using the existing PostgreSQL database. Wire up planner session
expiry as the first scheduled job (hourly, 7-day TTL) — this was previously
dead code that never ran. Add placeholder worker to journal for future
Komoot import and federation jobs.
Infrastructure: grant grafana_reader access to pgboss schema, add job queue
health panels to Service Health dashboard, add failed job alert.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fix blind spot where Caddy 502 errors were invisible: the dashboards and
alerts queried caddy_http_response_duration_seconds (upstream responses only),
missing 502s that Caddy generates itself. Switch to
caddy_http_request_duration_seconds (server-level, all responses).
Add Journal Grafana dashboard with: 502 rate, response codes, request rate
by route, latency percentiles, container restarts/memory/CPU, Node.js event
loop lag and heap, and Loki log panels for errors and Caddy 5xx entries.
Add color coding to Caddy status code panel (green=2xx, blue=3xx, yellow=4xx,
red=5xx). Add log-based error rate panel to the overview dashboard.
New alerts: container restart loop, PostgreSQL connections > 80, application
crash log detection (Loki), and Caddy 502 rate.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
oauth.server.ts used a TypeScript parameter property (`public code: string`
in constructor) which is not supported by Node.js v25's strip-only TypeScript
mode. This caused the journal to crash-loop on startup after seeding OAuth
clients.
Also enable `erasableSyntaxOnly` in tsconfig.base.json so `pnpm typecheck`
catches any future use of non-erasable TypeScript syntax.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The smoke test failed in CI with version mismatch between react
(19.2.5 from catalog) and react-test-renderer (19.2.0 transitive).
Adding it explicitly ensures they stay in sync.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The journal now depends on @trails-cool/api (added in PR #217) which
depends on zod. The Dockerfile was missing the package.json for api
and map-core, causing pnpm install to skip them and the build to fail
with "Failed to resolve import zod".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove SENTRY_DISABLE_AUTO_UPLOAD from all build profiles
- Add @sentry/cli as explicit dependency to fix pnpm module
resolution in EAS cloud builds
- SENTRY_AUTH_TOKEN configured as EAS environment secret
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Initialize @sentry/react-native with dedicated mobile project DSN
- Wrap root layout with Sentry.wrap() for unhandled error capture
- Configure Sentry plugin with org (trails-qq) and project (mobile)
for source map uploads during EAS builds
- Disabled in dev, full tracing in production
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add eas.json with development, preview, and production profiles
- Link project to EAS (projectId: 93c75cae-fecf-4ce5-8cd8-c823760b12e2)
- Add expo-dev-client, expo-constants, expo-linking dependencies
- Remove eas-cli from project deps (use globally)
- Add ITSAppUsesNonExemptEncryption to iOS infoPlist
- Use catalog react with expo.install.exclude to skip version check
- Fix slug to match EAS project registration
- Run pnpm dedupe to resolve duplicate react across workspaces
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Route detail at /routes/:id shows:
- Header with back navigation and route name
- Map placeholder (MapLibre requires EAS dev build)
- Stats row: distance, elevation gain/loss, day count
- Description text
- Version count
- "Edit in Planner" opens Journal web UI
- "Download Offline" button (handler pending Phase 5)
Safe area insets applied to header and scroll content.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Routes tab (Phase 3.1):
- Paginated FlatList fetching from Journal REST API
- Route cards with name, distance (km), elevation gain, day count
- Pull-to-refresh, loading spinner, error state with retry, empty state
- Safe area insets for notch and tab bar
API client:
- Import types from @trails-cool/api instead of hand-written interfaces
- Parse list/detail responses through Zod schemas at runtime
- Add zod as mobile app dependency
i18n:
- Add mobile namespace (en + de) for routes, activities, profile, login
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests the full handler flow with mocked DB: auth guard (401), Zod
validation (400), successful responses, and 404s for missing resources.
Also adds ~ alias resolution to Journal vitest config.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests Zod schema validation for all API endpoints (routes, activities,
compute, uploads), error codes, and cursor pagination logic. 21 new
tests covering the validation layer without requiring a running DB.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add all REST API endpoints for mobile app consumption:
Routes:
- GET /api/v1/routes — paginated list with cursor, Zod validation
- GET /api/v1/routes/:id — full detail with GPX, versions
- POST /api/v1/routes — create with Zod-validated body
- PUT /api/v1/routes/:id — update, creates new version
- DELETE /api/v1/routes/:id — returns 204
Activities:
- GET /api/v1/activities — paginated list with cursor
- GET /api/v1/activities/:id — full detail
- POST /api/v1/activities — create with GPX stat extraction
- DELETE /api/v1/activities/:id — returns 204
Supporting:
- POST /api/v1/routes/compute — BRouter proxy
- POST /api/v1/uploads — presigned upload URL generation
Device management:
- GET /api/v1/auth/devices — list connected devices with isCurrent
- DELETE /api/v1/auth/devices/:id — revoke device token
- Store device_name on token exchange
Infrastructure:
- requireApiUser() guard — returns 401 with structured error
- apiError() helper for consistent error responses
- All endpoints use Zod schemas from @trails-cool/api for validation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Journal server (Phase 1.4 + 1.5):
- Add oauth_clients, oauth_codes, oauth_tokens tables to journal schema
- Implement GET /oauth/authorize with PKCE flow and login redirect
- Implement POST /oauth/token (authorization_code + refresh_token grants)
- Add validateBearerToken() + getAuthenticatedUser() middleware
- Seed trails-cool-mobile as trusted OAuth client on server startup
- Add GET /.well-known/trails-cool discovery endpoint
- Add returnTo support to login page and magic link verify
- Add @trails-cool/api workspace dependency to journal
Mobile app (Phase 1.5 + 1.6):
- Login screen with server URL input and discovery validation
- OAuth2 PKCE login via expo-web-browser with expo-crypto for Hermes
- Token storage in expo-secure-store with auto-refresh on 401
- API client with bearer token injection and typed errors
- Server URL persistence with localhost default in dev mode
- API version compatibility check on app foreground
- Log out + switch server on Profile tab
- iOS ATS exception for local networking
Tests:
- PKCE crypto verification, OAuthError, token generation
- Discovery endpoint response shape
- API version semver compatibility
- API client error types
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously only 2-3 workspaces participated in each turbo task. This
expands test, lint, and typecheck to cover all 11 workspaces with
parallel execution and caching.
Test pipeline:
- Move from single root vitest to per-workspace test scripts via turbo
- Shared vitest config (vitest.shared.ts) with passWithNoTests
- Per-workspace configs re-export the shared base
- Mobile uses Jest (jest-expo + React Native Testing Library)
- Add node-environment GPX tests verifying linkedom fallback
- Add i18n mobile init tests
- Exclude apps/mobile from root vitest (uses Jest separately)
Lint pipeline:
- Add eslint lint script to all 9 previously unlinted workspaces
- Standardize all scripts to "eslint ." with shared root config
- Add .expo/ to global ESLint ignores
- Fix lint errors: unused imports in api/types, export parseGpx
Typecheck pipeline:
- Add "typecheck": "tsc" to all 8 packages
- Add @types/node (catalog) to gpx and db packages
- Fix mobile app.config.ts: remove deprecated experiments.monorepo
and newArchEnabled (both default in Expo SDK 55)
- Add allowImportingTsExtensions to mobile tsconfig
Shared package compatibility (mobile-app Phase 1.3):
- Add linkedom as explicit dependency to @trails-cool/gpx
- Add initI18nMobile() export to @trails-cool/i18n
- Confirm @trails-cool/types is pure interfaces (no DOM deps)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Switches from expo-router Tabs to NativeTabs (unstable API) which
renders native tab bars with liquid glass on iOS 26 and Material
bottom navigation on Android. Adds SF Symbols and Material icons.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New renderer-agnostic package with zero dependencies:
- Tile configs (base layers, overlay layers)
- Color palettes (surface, highway, smoothness, tracktype, cycleway,
bikeroute, elevation, maxspeed) — 8 color maps + 3 color functions
- POI category definitions (9 categories with Overpass queries, icons,
colors, profile mappings)
- Z-index layering constants
- Snap distance constant
Updated 15 consuming files to import from @trails-cool/map-core.
Deleted poi-categories.ts and z-index.ts from the Planner (fully moved).
packages/map re-exports tile configs from map-core for backwards compat.
All 117 tests pass, no user-facing changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Participant list: hidden on mobile (sm:block)
- Undo/redo buttons: hidden on mobile (sm:flex)
- Connection status: hidden on mobile (sm:inline)
- Header: no-wrap, tighter padding on mobile
- Username input: text-base on mobile to prevent iOS auto-zoom
Keeps profile selector and export button visible on all sizes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
maximum-scale=1 + user-scalable=no prevents the browser UI from
zooming when pinching. Map zoom (Leaflet) still works since it
handles touch events independently.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- viewport-fit=cover enables safe area insets in Safari
- Header: pt-[max(0.5rem,env(safe-area-inset-top))] clears the notch
- Elevation chart: pb-[max(0.5rem,env(safe-area-inset-bottom))] clears
the home indicator bar
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Single touch + drag: scrub the highlight point along the chart,
map dot follows in real-time
- Tap: pan map to that point (10px threshold for movement)
- Two-finger touch: range select — area between fingers highlighted,
on release zooms map to fit that route section
- touch-none CSS + preventDefault prevents page scrolling on chart
Spec updated with mobile touch interaction scenarios.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three new interactions:
1. Route hover → chart: Invisible interactive polyline (weight 20,
opacity 0) detects mouse hover on the route, computes cumulative
distance, and highlights the corresponding position on the chart.
2. Chart click → map pan: Clicking the elevation chart pans the map
to center on that point along the route. Uses 5px threshold to
distinguish from drag.
3. Chart drag-select → map zoom: Dragging a range on the chart shows
a blue selection overlay, then zooms the map to fit the route
coordinates in that range. "Reset zoom" button appears to restore
the full route view.
State flow avoids feedback loops: external highlights (from map hover)
set the chart crosshair without re-emitting onHover back to the map.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clicking the chart title (e.g. "Road Type Profile") opens the
corresponding OSM wiki page. Plain, elevation, and grade modes
remain as plain text since they're not OSM tag-based.
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>
Full notes lifecycle:
- GPX: description field in GpxData, <metadata><desc> in generate/parse
- Export: Plan GPX and Save to Journal include notes as description
- Journal: updateRoute extracts description from GPX, stores on route
- Reimport: Edit in Planner passes notes via URL params → Yjs Y.Text
- Drop import: GPX with <desc> restores notes in session
Spec updated: session-notes gains GPX export, Journal sync, and
reimport requirements.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Legend shows actual surface types present in the route (up to 6)
with colored swatches instead of generic text
- Hover label shows surface type name (e.g. "34m · 12.3km · asphalt")
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The chart was falling through to plain blue when surface mode was
selected. Now reads surface data from Yjs routeData and draws
segments colored by road surface type, matching the route colors
on the map.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>