Commit graph

92 commits

Author SHA1 Message Date
Ullrich Schäfer
1433997d5b Fix journal health check and improve log collection
Switch journal Docker health check from `node -e "fetch(...)"` to `curl -sf`,
matching the planner. The Node.js process spawn was heavy and prone to timeout
under memory pressure, causing false health check failures and 502s.

Upgrade Promtail from static file scraping to Docker service discovery so logs
get `service` and `container` labels. Parse Pino JSON logs to extract the
`level` label, enabling Loki queries filtered by log level.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 15:00:00 +02:00
Ullrich Schäfer
04c8995d7c
Update Sentry auth token with org:ci scope
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 02:27:53 +02:00
Ullrich Schäfer
7994ea79f4
Fix overview error rate panel, add request metrics to journal
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>
2026-04-05 16:20:18 +02:00
Ullrich Schäfer
3adf6fbfc4
Fix empty Caddy and pg_stat_statements panels in service-health dashboard
- Caddy Response Status Codes: caddy_http_responses_total doesn't exist,
  use caddy_http_response_duration_seconds_count which has the code label
- pg_stat_statements: enable extension in init script, add custom queries
  config for postgres-exporter, remove invalid datname filter from query
- Add postgres/queries.yml to cd-infra SCP sources

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 16:02:23 +02:00
Ullrich Schäfer
5a19217b33
Add Promtail to ship Docker container logs to Loki
Loki was running but had no log shipper — no labels or logs were
visible in Grafana. Adds Promtail to scrape all Docker container
logs and push them to Loki.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 15:23:41 +02:00
Ullrich Schäfer
ef39379a12
Fix missing Wahoo OAuth env vars in journal container
WAHOO_CLIENT_ID, WAHOO_CLIENT_SECRET, and WAHOO_WEBHOOK_TOKEN were in
SOPS secrets but never passed through to the journal service in
docker-compose.yml, causing the OAuth authorize URL to have an empty
client_id.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:36:43 +01:00
Ullrich Schäfer
b6711d23d6
Add Wahoo activity sync with provider-agnostic framework
Provider-agnostic sync framework + Wahoo as first implementation:

Framework (apps/journal/app/lib/sync/):
- SyncProvider interface for OAuth2, webhooks, import, conversion
- Provider registry for settings UI iteration
- Generic sync_connections + sync_imports tables
- Token storage with auto-refresh

Wahoo provider:
- OAuth2 with workouts_read, user_read, offline_data scopes
- Webhook-based auto-import (workout_summary events)
- Manual import page with pagination
- FIT→GPX conversion via fit-file-parser
- Webhook token verification

Routes:
- /api/sync/connect/:provider — OAuth redirect
- /api/sync/callback/:provider — OAuth callback
- /api/sync/disconnect/:provider — remove connection
- /api/sync/webhook/:provider — webhook receiver
- /sync/import/:provider — manual import page

Settings: Connected Services section with per-provider connect/disconnect

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 11:22:02 +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
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
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
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
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
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
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
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
2b0aba205f
Fix error rate alert: avoid NaN when no traffic
The error rate formula (errors/total*100) produces NaN when there's
zero traffic (0/0). Grafana treats NaN as a firing condition.
Use clamp_min on the denominator to return 0% instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 07:54:49 +01:00
Ullrich Schäfer
f344c27478
Allow Planner to connect to Journal in CSP
The Planner's save-to-journal callback makes a cross-origin request to
trails.cool from planner.trails.cool. Add the Journal's origin to the
Planner's connect-src CSP directive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 21:34:41 +01:00
Ullrich Schäfer
296f9e57dd
Add SMTP configuration to Grafana for alert emails
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:27:45 +01:00
Ullrich Schäfer
3914da7a57
Fix Grafana alert rules: proper threshold expressions
The threshold expressions were missing the 'expression' field referencing
the query refId, causing 'no variable specified for refId C' errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:23:46 +01:00
Ullrich Schäfer
e866a1ad40
Add Business Metrics Grafana dashboard
Panels:
- Stat cards: total users, routes, activities, active planner sessions
- Time series: cumulative user growth, route growth
- Bar charts: signups/day, routes/day, planner sessions/day
- Table: top 20 routes by distance

All queries use the grafana_reader PostgreSQL datasource.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:16:54 +01:00
Ullrich Schäfer
424eecd274
Add PostgreSQL datasource to Grafana with read-only user
- Create grafana_reader role with SELECT-only access on all schemas
- Init script runs on postgres first boot (docker-entrypoint-initdb.d)
- Grafana PostgreSQL datasource provisioned with read-only credentials
- Enables SQL queries in dashboards (user count, routes, etc.)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 20:07:27 +01:00
Ullrich Schäfer
424e692ee0
Add service health monitoring: postgres, node, cAdvisor exporters + dashboard
Exporters:
- postgres_exporter: DB connections, transactions, cache hit ratio, query stats
- node_exporter: host CPU, memory, disk, network
- cAdvisor: per-container CPU and memory usage

PostgreSQL:
- Enable pg_stat_statements for query-level performance tracking
- Track index scans vs sequential scans, cache hit ratio

Dashboard (service-health.json):
- DB: connections, size, transactions/s, slow queries, cache hit ratio, index usage
- Host: disk gauge, CPU, memory, network I/O, disk I/O
- BRouter: request latency p50/p95/p99, container CPU + memory
- All containers: CPU and memory comparison

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 18:50:42 +01:00
Ullrich Schäfer
57094323d2
SOPS+age secrets, split CD workflows, GitHub OAuth for Grafana
Secrets:
- Add .sops.yaml with age encryption config
- Add encrypted secrets.app.env (app secrets) and secrets.infra.env (Grafana OAuth)
- CD decrypts at deploy time with AGE_SECRET_KEY — all other secrets
  move out of GitHub Actions into version-controlled encrypted files

Split CD:
- cd-apps.yml: triggered by apps/packages changes, builds Docker images, deploys apps
- cd-infra.yml: triggered by infrastructure/ changes, copies configs, restarts services
- Remove monolithic cd.yml

Grafana auth:
- GitHub OAuth (trails-cool org), disable login form
- Remove Caddy basic_auth block and all GRAFANA_* env vars/secrets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 17:28:04 +01:00
Ullrich Schäfer
65f30b952a
Merge pull request #94 from trails-cool/fix-caddy-basicauth
Fix deprecated basicauth directive in Caddyfile
2026-03-27 14:35:03 +01:00
Ullrich Schäfer
73edb1bfed
Use basic_auth instead of deprecated basicauth in Caddyfile
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:32:57 +01:00
Ullrich Schäfer
a8ec0a9509
Merge pull request #93 from trails-cool/fix-grafana-datasource-uids
Add explicit UIDs to Grafana datasources
2026-03-27 13:10:35 +00:00
Ullrich Schäfer
83243cea31
Add explicit UIDs to Grafana datasources
Alert rules reference datasourceUid: prometheus/loki which requires
explicit UIDs in the provisioning config.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:08:21 +01:00
Ullrich Schäfer
c47426213b
Fix Grafana alert provisioning: hardcode email address
Grafana provisioning YAML doesn't support env var substitution.
Replace ${ALERT_EMAIL} with hardcoded admin@trails.cool.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:03:48 +01:00
Ullrich Schäfer
2ecfe81c08
Pass Grafana env vars to Caddy container, fix Loki compactor config
- Add DOMAIN, GRAFANA_USER, GRAFANA_PASSWORD_HASH as environment vars
  on the Caddy container so Caddyfile can reference them
- Fix Loki compactor: add delete_request_store required when retention is enabled

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 13:53:56 +01:00
Ullrich Schäfer
559f53d8fc
Use *.internal.trails.cool wildcard for internal services
Add wildcard DNS record for *.internal.trails.cool (A + AAAA) so
internal services don't leak hostnames in individual DNS entries.
Move Grafana from grafana.trails.cool to grafana.internal.trails.cool.

Future internal services just need a Caddy block — no DNS changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 13:19:47 +01:00
Ullrich Schäfer
49aadd04a9
Add observability: health endpoints, structured logging, metrics, Grafana stack
Health endpoints:
- /api/health (Journal) and /health (Planner) with DB connectivity check
- Docker healthchecks updated to use app health endpoints

Structured logging:
- Pino with JSON output in production, pretty-print in dev
- Request logging middleware in Planner (method, path, status, duration)
- Replaced console.log/error with structured logger in email and auth flows

Prometheus metrics:
- prom-client with default Node.js metrics + custom histograms/gauges
- /metrics endpoints on both apps
- http_request_duration, planner_active_sessions, brouter_request_duration

Monitoring stack:
- Prometheus, Loki, Grafana containers in docker-compose
- Grafana provisioned with datasources, dashboards, and alert rules
- Caddy access logging (JSON to stdout for Loki)
- grafana.trails.cool with basic auth via Caddy

Dashboards and alerting:
- Overview: request rate, error rate, latency p50/p95/p99
- Planner: active sessions, connected clients, BRouter latency
- Infrastructure: memory, CPU, event loop lag
- Alerts: disk >80%, app down 2min, error rate >5%

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 22:59:44 +01:00
Ullrich Schäfer
0a8dd0b766
Add transactional emails (SMTP) and planner features (no-go areas, notes, crash recovery)
Transactional emails:
- Add nodemailer SMTP email module with dev-mode console logging
- Magic link template and welcome template with HTML + plain text
- Wire sendMagicLink into login flow, sendWelcome into registration
- Update privacy page and deploy docs for SMTP configuration

Planner features:
- No-go areas: draw polygons on map (leaflet-geoman), synced via Yjs,
  passed to BRouter as nogos parameter, route recomputes on change
- Session notes: collaborative Y.Text textarea in sidebar tab
- Crash recovery: periodic localStorage save of Yjs state, restore on reconnect
- Rate limit session creation (10/IP/hour) in /new route

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 01:00:42 +01:00
Ullrich Schäfer
5d542a9391
Fix CSP: allow blob: for Yjs web worker
y-websocket creates a Web Worker from a blob URL. The CSP script-src
needs blob: and an explicit worker-src directive to allow this.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 22:40:05 +01:00
Ullrich Schäfer
0e98efe1ab Add Fastmail DNS records to Terraform
MX, SPF, DMARC, DKIM (4 keys), mail A record, and SRV records for
autodiscover, IMAP, JMAP, CalDAV, CardDAV, SMTP submission.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:11:42 +00:00
Ullrich Schäfer
be3e69c10b
Security hardening: headers, scanning, Docker, firewall
- Caddy: HSTS, CSP, X-Frame-Options, nosniff, Referrer-Policy,
  Permissions-Policy on all responses
- Caddy: Block scanner paths (.env, .git, wp-config, etc.) with 403
- CI: Gitleaks secret scanning + pnpm audit for vulnerabilities
- Dependabot: Weekly npm + GitHub Actions + monthly Docker updates
- Docker: Non-root user in journal, planner, and brouter containers
- Server: UFW firewall (22/80/443 only) + fail2ban (8 IPs already banned)
- SECURITY.md: Vulnerability disclosure policy
- Privacy page: Security practices section added

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 09:58:12 +01:00
Ullrich Schäfer
091fc0a4bd
Add Sentry source maps and release tracking
- @sentry/vite-plugin uploads source maps during Docker build
- Release tagged with git SHA (SENTRY_RELEASE) on both client and server
- Environment set to production/development for filtering
- CD passes SENTRY_AUTH_TOKEN + SENTRY_RELEASE as Docker build args
- docker-compose passes SENTRY_RELEASE to runtime containers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 02:43:28 +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
Ullrich Schäfer
75c9f6901b
Add reverse DNS (PTR) records to Terraform config (#14) 2026-03-22 23:17:33 +00:00
Ullrich Schäfer
a9a6b27af4
Add IPv6 (AAAA) DNS records and www redirect (#8) 2026-03-22 22:06:49 +00:00
Ullrich Schäfer
9160301ab3
Add DNS records via Terraform, bump hcloud to v1.60 (#4)
- trails.cool and planner.trails.cool A records pointing to server
- Using hcloud_zone_rrset (native hcloud provider, no separate DNS provider)
- Bumped hcloud provider ~> 1.45 to ~> 1.60
- Server type updated to cx23

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 22:54:07 +01:00
Ullrich Schäfer
7908a518b1
Add infrastructure: Terraform, Docker Compose, BRouter, CD pipeline (#2)
Terraform:
- Hetzner CX22 server with Docker, firewall (80/443/22)
- SSH key and output for server IP

Docker Compose:
- Caddy reverse proxy (auto HTTPS)
- Journal and Planner app containers
- BRouter routing engine container
- PostgreSQL + PostGIS
- Garage S3 storage

BRouter:
- Dockerfile based on eclipse-temurin:11-jre
- Segment download script for Germany (4 tiles, ~750MB)

CD Pipeline:
- Build & push Docker images to ghcr.io on main push
- Deploy step commented out (enable when server provisioned)

Scripts:
- PostgreSQL daily backup with 14-day retention
- Dockerfiles for both apps (multi-stage builds)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 13:32:01 +01:00