Commit graph

1138 commits

Author SHA1 Message Date
Ullrich Schäfer
6aac8bd885
Add map previews to journal route and activity pages
- 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>
2026-04-04 09:42:07 +01:00
Ullrich Schäfer
3a4af698ca
Merge pull request #159 from trails-cool/feat/gpx-import-planner
Add GPX file import to planner
2026-04-03 17:34:15 +01:00
Ullrich Schäfer
a9f8ee61f0
Add GPX file import to planner + archive change
Two import entry points:
- Home page: "Import GPX" button next to "Start Planning"
- In-session: drag-and-drop GPX onto the map (with confirmation)

Parses GPX client-side, extracts waypoints (Douglas-Peucker) and
no-go areas from extensions. Non-GPX files show error toast.

Also:
- Fix spec drift: non-GPX drop now shows error toast (was silent)
- Add E2E tests for import button, invalid GPX, and session creation
- Archive gpx-import-planner change, sync specs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 17:31:44 +01:00
Ullrich Schäfer
516b86aa3d
Merge pull request #158 from trails-cool/feat/export-plan-with-nogos
Add split export: Export Route vs Export Plan
2026-04-03 12:55:39 +01:00
Ullrich Schäfer
7ef694c366
Update no-go areas spec with GPX persistence and export options
Documents:
- Right-click to delete
- Save to Journal preserves no-go areas in GPX extensions
- Export Plan includes full planning state
- Export Route is clean track only
- GPX extensions format with trails:planning namespace

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:53:17 +01:00
Ullrich Schäfer
180f1fc4bd
Add unit tests for no-go area round-trip and waypoint extraction
15 tests covering:
- No-go area generation (namespace, structure, multiple areas)
- No-go area parsing (namespaced, non-namespaced, min 3 points, empty)
- Full round-trip: generate → parse → verify no-go areas match
- Complete planning state round-trip (waypoints + tracks + no-go areas)
- extractWaypoints: explicit waypoints, multi-segment, Douglas-Peucker,
  empty tracks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:50:57 +01:00
Ullrich Schäfer
b6179fbdd2
Initialize no-go areas client-side via URL params
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>
2026-04-03 12:47:40 +01:00
Ullrich Schäfer
fdfacd2997
Save full planning state to journal (waypoints + no-go areas)
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>
2026-04-03 12:41:35 +01:00
Ullrich Schäfer
45d1360be5
Include no-go areas when saving to journal
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>
2026-04-03 12:32:05 +01:00
Ullrich Schäfer
6ccf7af7b1
Fix no-go area parsing for namespaced XML elements
querySelectorAll('nogo') doesn't match <trails:nogo>. Now matches
both unprefixed and prefixed element names.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 12:26:54 +01:00
Ullrich Schäfer
e12479293d
Add descriptions to export dropdown, fix z-index and alignment
- 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>
2026-04-03 11:06:58 +01:00
Ullrich Schäfer
87251ea366
Fix dropdown race: use mousedown for outside-click handler
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>
2026-04-03 10:56:22 +01:00
Ullrich Schäfer
721ec9e5e5
Fix export chevron: wider padding + stop click propagation
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>
2026-04-03 10:53:18 +01:00
Ullrich Schäfer
4b711abaec
Add split export: Export Route vs Export Plan
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>
2026-04-03 10:52:39 +01:00
Ullrich Schäfer
237660db5e
Merge pull request #156 from trails-cool/fix/nogo-button-click-through
Fix no-go area button click creating a waypoint
2026-04-03 10:40:05 +01:00
Ullrich Schäfer
16568ffa45
Fix no-go button click creating a waypoint
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>
2026-04-03 10:37:43 +01:00
Ullrich Schäfer
596d96d8cb
Merge pull request #155 from trails-cool/fix/nogo-polygons-native
Pass no-go area polygons natively to BRouter
2026-04-03 10:34:08 +01:00
Ullrich Schäfer
3abea9d5a8
Pass no-go area polygons natively to BRouter
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>
2026-04-03 10:31:43 +01:00
Ullrich Schäfer
d2ff488f37
Merge pull request #154 from trails-cool/chore/prometheus-5s-scrape
Reduce Prometheus scrape interval to 5s
2026-04-03 10:16:38 +01:00
Ullrich Schäfer
55076ef440
Reduce Prometheus scrape interval from 15s to 5s
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 10:15:08 +01:00
Ullrich Schäfer
5a5fe87905
Merge pull request #149 from trails-cool/fix/planner-metrics-gauges
Fix planner metrics gauges always reporting zero
2026-04-03 10:04:40 +01:00
Ullrich Schäfer
d97992e967
Guard all metric registrations against re-evaluation
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>
2026-04-03 10:02:21 +01:00
Ullrich Schäfer
a3ad073d96
Guard collectDefaultMetrics against duplicate registration
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>
2026-04-03 09:16:31 +01:00
Ullrich Schäfer
e083ee134d
Wire planner metrics gauges to actual session/client counts
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>
2026-04-03 09:16:31 +01:00
Ullrich Schäfer
ca057eb9d4
Merge pull request #153 from trails-cool/chore/update-drifted-specs
Update specs to match implementation
2026-04-03 09:15:31 +01:00
Ullrich Schäfer
02939ca828
Update specs to match implementation
Addresses spec drift items #3, 5, 6, 7, 8, 9, 10, 11, 12 from #147:

- transactional-emails: Resend → Nodemailer + SMTP
- infrastructure: CX21 → cx23
- secret-management: single secrets.env → split app/infra files
- brouter-integration: 5s failover delay → instant via clientID election
- brouter-integration: 2 profiles → 5 (trekking, fastbike, safety, shortest, car)
- observability: add version field to health response
- observability: add brouter_request_duration_seconds metric
- planner-session: remove 30-day max ceiling (not enforced)
- shared-packages: clarify map package scope vs planner-specific features

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-03 09:13:07 +01:00
Ullrich Schäfer
9e823317b9
Merge pull request #152 from trails-cool/fix/remove-sync-parsegpx
Fix GPX distance + remove sync parseGpx
2026-04-03 09:11:36 +01:00
Ullrich Schäfer
2b0b89add8
Use epsilon 0.005 for Douglas-Peucker waypoint extraction
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:40:01 +01:00
Ullrich Schäfer
0e27d6eaef
Use Douglas-Peucker simplification for single-segment GPX
For GPX files with one long track segment, extracting only start/end
gives just 2 waypoints. Now uses Douglas-Peucker line simplification
(epsilon=0.05° ≈ 5km) to find significant turning points.

Result: berlin-dresden-radweg (249km, 5660 points) → 10 waypoints
that capture the route's key direction changes.

Multi-segment GPX files still use segment endpoints as before.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:36:13 +01:00
Ullrich Schäfer
cc90c32a0e
Extract shared extractWaypoints into @trails-cool/gpx
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>
2026-04-02 20:47:17 +01:00
Ullrich Schäfer
e0f7c5b806
Apply track-segment waypoint extraction to sessions API
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>
2026-04-02 20:45:05 +01:00
Ullrich Schäfer
79942bb99e
Extract start/end from track when GPX has no waypoints
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>
2026-04-02 20:41:29 +01:00
Ullrich Schäfer
5f3d4ae846
Remove sync parseGpx, use parseGpxAsync everywhere
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>
2026-04-02 20:32:48 +01:00
Ullrich Schäfer
d5bcecb345
Fix distance calculation for GPX files without elevation data
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>
2026-04-02 20:32:47 +01:00
Ullrich Schäfer
ed2d04c248
Merge pull request #151 from trails-cool/fix/postgis-geom-async-parse
Fix PostGIS geom: use parseGpxAsync + raw SQL
2026-04-02 17:34:43 +01:00
Ullrich Schäfer
6ed828ef9c
Fix PostGIS geom population: use parseGpxAsync + raw SQL
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>
2026-04-02 17:32:07 +01:00
Ullrich Schäfer
feeca58064
Merge pull request #150 from trails-cool/fix/populate-postgis-geom
Populate PostGIS geom column from GPX track data
2026-04-02 17:07:26 +01:00
Ullrich Schäfer
b4c3f97296
Populate PostGIS geom column from GPX track data
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>
2026-04-02 17:05:02 +01:00
Ullrich Schäfer
47baa11e69
Merge pull request #148 from trails-cool/fix/grafana-oauth-user-lookup
Fix Grafana OAuth creating duplicate users
2026-04-02 16:41:54 +01:00
Ullrich Schäfer
acccfcc53b
Enable Grafana OAuth email lookup to reuse existing users
Without this, GitHub OAuth creates a new Grafana user on every login
instead of linking to the existing account by email.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 16:39:35 +01:00
Ullrich Schäfer
d1faf21a83
Merge pull request #146 from trails-cool/feat/europe-brouter-segments
Expand BRouter routing to all of Europe
2026-03-29 22:49:16 +02:00
Ullrich Schäfer
836e0a2b6a
Expand BRouter routing coverage to all of Europe
Add ~67 segment tiles covering W10-E40, N35-N70 (was 4 Germany-only
tiles). Total disk usage ~2.9 GB. Tiles that don't exist on the
BRouter CDN are silently skipped.

Already downloaded to the server and BRouter restarted.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:48:11 +02:00
Ullrich Schäfer
56cd6b1ccc
Merge pull request #145 from trails-cool/chore/archive-account-settings
Archive account-settings change
2026-03-29 22:44:20 +02:00
Ullrich Schäfer
7e253caf64
Archive account-settings change, sync specs
All 37 tasks complete. Synced delta specs to main:
- New: openspec/specs/account-settings/
- Updated: openspec/specs/journal-auth/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:41:51 +02:00
Ullrich Schäfer
0150c40631
Merge pull request #144 from trails-cool/chore/harden-sshd
Harden SSH config
2026-03-29 22:33:57 +02:00
Ullrich Schäfer
8eb3481831
Harden SSH: disable passwords, forwarding, add keepalive
Add SSH hardening to user_data for future server rebuilds:
- PasswordAuthentication no (key-only access)
- X11Forwarding no (headless server)
- AllowTcpForwarding no (no SSH tunnels needed)
- AllowAgentForwarding no (no agent forwarding needed)
- MaxAuthTries 3 (reduced from 6)
- ClientAliveInterval 300 + ClientAliveCountMax 2 (clean up dead sessions)

Already applied manually to the existing server via
/etc/ssh/sshd_config.d/99-hardening.conf.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:32:30 +02:00
Ullrich Schäfer
7d63e2b754
Merge pull request #143 from trails-cool/fix/unwrap-dashboard-json
Fix dashboard JSON: unwrap for Grafana provisioning
2026-03-29 22:21:26 +02:00
Ullrich Schäfer
3b6aede461
Fix error-rate-high alert firing on no data
The alert was firing with DatasourceNoData because zero 5xx responses
means the numerator returns empty (not 0). Fixed by:
- Adding `or vector(0)` so zero 5xx returns 0% instead of empty
- Setting `noDataState: OK` as a safety net

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:20:14 +02:00
Ullrich Schäfer
d45d6e3eaf
Unwrap dashboard JSON for Grafana provisioning
Grafana file-based provisioning expects flat dashboard JSON, not the
API format with a "dashboard" wrapper. The 4 wrapped dashboards were
silently failing to load — only business.json (already flat) worked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 22:18:06 +02:00
Ullrich Schäfer
1103deb7e8
Merge pull request #142 from trails-cool/fix/grafana-annotation-filter-type
Fix Grafana annotation filter: remove type:dashboard
2026-03-29 22:00:07 +02:00