Commit graph

773 commits

Author SHA1 Message Date
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
Ullrich Schäfer
9223eacb91
Remove type:dashboard from annotation config
The type:dashboard field was causing Grafana to filter annotations
by dashboard scope. Our deploy annotations are global (no dashboardUID),
so the target.type:tags is the only filter needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:57:39 +02:00
Ullrich Schäfer
16fd07bd97
Merge pull request #141 from trails-cool/fix/grafana-annotation-target
Fix Grafana deploy annotation query
2026-03-29 21:53:57 +02:00
Ullrich Schäfer
cafedfe6fc
Fix Grafana annotation query: add target with type tags
The annotation config was missing the target block that tells
Grafana to filter by tags. Without it, the annotation query
was silently ignored.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 21:53:33 +02:00
Ullrich Schäfer
af12837772
Merge pull request #140 from trails-cool/fix/deploy-annotations-curl-dashboards
Fix deploy annotations: wget→curl + dashboard queries
2026-03-29 20:51:06 +02:00
Ullrich Schäfer
fcfea209b8
Add deploy annotation query to all Grafana dashboards
Provisioned dashboards now display deploy markers (cyan vertical
lines) by querying built-in Grafana annotations tagged "deploy".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:49:40 +02:00
Ullrich Schäfer
87be96db67
Switch deploy annotations from BusyBox wget to curl
BusyBox wget in the Grafana Alpine container doesn't support
--post-data/--header. Grafana's image includes curl.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 20:49:40 +02:00
Ullrich Schäfer
7d0ad7fe31
Merge pull request #139 from trails-cool/fix/deploy-annotation-env-file
Fix deploy annotation reading token from wrong env file
2026-03-29 20:00:07 +02:00
Ullrich Schäfer
c2b9032686
Fix deploy annotation reading token from wrong env file
GRAFANA_SERVICE_TOKEN is in secrets.infra.env, which the infra
workflow merges into .env on the server. The apps workflow was
reading from app.env (which only has app secrets).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:59:17 +02:00
Ullrich Schäfer
8440c2de8f
Merge pull request #138 from trails-cool/chore/grafana-service-token
Add GRAFANA_SERVICE_TOKEN to encrypted infra secrets
2026-03-29 19:51:45 +02:00
Ullrich Schäfer
8ab174d5bb
Add GRAFANA_SERVICE_TOKEN to encrypted infra secrets
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:49:29 +02:00
Ullrich Schäfer
4d8b9a6bb4
Merge pull request #137 from trails-cool/feat/grafana-deploy-annotations
Add Grafana deploy annotations to CD workflows
2026-03-29 19:47:53 +02:00
Ullrich Schäfer
6c482238e8
Add Grafana deploy annotations to all CD workflows
Posts an annotation to Grafana after each successful deploy, tagged
with "deploy" and the service name. Annotations appear as vertical
markers on dashboards, making it easy to correlate deploys with
metric changes.

Uses GRAFANA_SERVICE_TOKEN from SOPS secrets — must be added via
sops infrastructure/secrets.infra.env. Gracefully skips if the
token is not configured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 19:44:15 +02:00
Ullrich Schäfer
f6b89a2b7e
Merge pull request #135 from trails-cool/copilot/change-map-zoom-for-route
Fit map to route bounds on open
2026-03-29 19:38:05 +02:00
Ullrich Schäfer
156bb7cb36
Merge branch 'main' into copilot/change-map-zoom-for-route 2026-03-29 19:35:27 +02:00
Ullrich Schäfer
99a4d7af97
Fix fitBounds running before elevation chart resizes map
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>
2026-03-29 15:20:06 +02:00