From 02939ca828fe249568848ef8cab53e552af41523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Fri, 3 Apr 2026 09:13:07 +0100 Subject: [PATCH] Update specs to match implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- openspec/specs/brouter-integration/spec.md | 4 ++-- openspec/specs/infrastructure/spec.md | 2 +- openspec/specs/observability/spec.md | 8 ++++++-- openspec/specs/planner-session/spec.md | 2 +- openspec/specs/secret-management/spec.md | 6 +++--- openspec/specs/shared-packages/spec.md | 2 +- openspec/specs/transactional-emails/spec.md | 2 +- 7 files changed, 15 insertions(+), 11 deletions(-) diff --git a/openspec/specs/brouter-integration/spec.md b/openspec/specs/brouter-integration/spec.md index b2e8053..6757712 100644 --- a/openspec/specs/brouter-integration/spec.md +++ b/openspec/specs/brouter-integration/spec.md @@ -32,7 +32,7 @@ The Planner SHALL elect one participant per session as the "routing host" who is #### Scenario: Host failover - **WHEN** the current routing host disconnects -- **THEN** the longest-connected remaining participant becomes the new host within 5 seconds +- **THEN** failover is immediate via deterministic Yjs clientID election: the client with the lowest remaining ID becomes host instantly ### Requirement: Route broadcast The routing host SHALL store computed route results in the Yjs document so that all participants receive route updates automatically. @@ -45,7 +45,7 @@ The routing host SHALL store computed route results in the Yjs document so that The Planner SHALL support selecting a routing profile that determines how BRouter computes the route. #### Scenario: Switch routing profile -- **WHEN** a user changes the routing profile from "trekking" to "shortest" +- **WHEN** a user changes the routing profile (available profiles: `trekking`, `fastbike`, `safety`, `shortest`, `car`) - **THEN** the profile change syncs via Yjs and the routing host recomputes the route ### Requirement: BRouter API proxy diff --git a/openspec/specs/infrastructure/spec.md b/openspec/specs/infrastructure/spec.md index 61fb6c5..a1bb45b 100644 --- a/openspec/specs/infrastructure/spec.md +++ b/openspec/specs/infrastructure/spec.md @@ -5,7 +5,7 @@ Infrastructure SHALL be provisioned on Hetzner Cloud using Terraform with the He #### Scenario: Provision server - **WHEN** `terraform apply` is run -- **THEN** a Hetzner CX21 server (2 vCPU, 4 GB RAM, 40 GB SSD) is created with Docker installed +- **THEN** a Hetzner cx23 server (2 vCPU, 4 GB RAM, 40 GB SSD) is created with Docker installed ### Requirement: Docker Compose deployment All services SHALL be deployed via Docker Compose on the Hetzner server. diff --git a/openspec/specs/observability/spec.md b/openspec/specs/observability/spec.md index 850e803..88ae7ca 100644 --- a/openspec/specs/observability/spec.md +++ b/openspec/specs/observability/spec.md @@ -5,11 +5,11 @@ Both apps SHALL expose a `/health` endpoint returning service and database statu #### Scenario: Healthy service - **WHEN** the app is running and the database is reachable -- **THEN** `GET /health` returns `{ "status": "ok", "db": "connected" }` with HTTP 200 +- **THEN** `GET /health` returns `{ "status": "ok", "db": "connected", "version": "" }` with HTTP 200 (version from `SENTRY_RELEASE` env var, defaults to `"dev"`) #### Scenario: Degraded service - **WHEN** the app is running but the database is unreachable -- **THEN** `GET /health` returns `{ "status": "degraded", "db": "unreachable" }` with HTTP 503 +- **THEN** `GET /health` returns `{ "status": "degraded", "db": "unreachable", "version": "" }` with HTTP 503 ### Requirement: Prometheus metrics Both apps SHALL expose a `/metrics` endpoint with Prometheus-formatted metrics. @@ -26,6 +26,10 @@ Both apps SHALL expose a `/metrics` endpoint with Prometheus-formatted metrics. - **WHEN** the Planner is running - **THEN** `planner_active_sessions` and `planner_connected_clients` gauges reflect current state +#### Scenario: BRouter latency metrics +- **WHEN** the Planner proxies a routing request to BRouter +- **THEN** `brouter_request_duration_seconds` histogram is updated (buckets: 0.1s to 10s) + ### Requirement: Structured logging Both apps SHALL output structured JSON logs in production. diff --git a/openspec/specs/planner-session/spec.md b/openspec/specs/planner-session/spec.md index accceb9..414aa90 100644 --- a/openspec/specs/planner-session/spec.md +++ b/openspec/specs/planner-session/spec.md @@ -45,7 +45,7 @@ The Planner SHALL persist Yjs session state to PostgreSQL so that sessions survi - **THEN** reconnecting clients recover the full session state from PostgreSQL ### Requirement: Session expiry -The Planner SHALL automatically expire sessions after a configurable period of inactivity (default: 7 days, max: 30 days). +The Planner SHALL automatically expire sessions after a configurable period of inactivity (default: 7 days, no hard ceiling enforced). #### Scenario: Session expires - **WHEN** no edits are made to a session for 7 days diff --git a/openspec/specs/secret-management/spec.md b/openspec/specs/secret-management/spec.md index abaa0ae..3d64bb1 100644 --- a/openspec/specs/secret-management/spec.md +++ b/openspec/specs/secret-management/spec.md @@ -1,15 +1,15 @@ ## Requirements ### Requirement: Encrypted secrets in repository -Production secrets SHALL be stored as a SOPS-encrypted file in the repository, decryptable only with a single age private key. +Production secrets SHALL be stored as SOPS-encrypted files in the repository, decryptable only with a single age private key. Secrets are split into two files: `secrets.app.env` (application secrets) and `secrets.infra.env` (infrastructure secrets). #### Scenario: Edit secrets -- **WHEN** a developer runs `sops infrastructure/secrets.env` +- **WHEN** a developer runs `sops infrastructure/secrets.app.env` or `sops infrastructure/secrets.infra.env` - **THEN** the file is decrypted in a temporary editor, and re-encrypted on save #### Scenario: CD decryption - **WHEN** the CD workflow runs -- **THEN** the encrypted secrets file is decrypted using the AGE_SECRET_KEY GitHub secret and provided to docker-compose as an env file +- **THEN** both encrypted secrets files are decrypted using the AGE_SECRET_KEY GitHub secret and merged at deploy time as env files for docker-compose #### Scenario: Secret audit trail - **WHEN** a secret is changed diff --git a/openspec/specs/shared-packages/spec.md b/openspec/specs/shared-packages/spec.md index e40a8e1..ec7aa45 100644 --- a/openspec/specs/shared-packages/spec.md +++ b/openspec/specs/shared-packages/spec.md @@ -27,7 +27,7 @@ The `@trails-cool/gpx` package SHALL parse GPX XML into structured data (waypoin - **THEN** it returns elevation gain, loss, and a profile array of distance/elevation pairs ### Requirement: Map rendering package -The `@trails-cool/map` package SHALL provide React components for rendering Leaflet maps with configurable base layers and route overlays. +The `@trails-cool/map` package SHALL provide core React components (MapView, RouteLayer) for rendering Leaflet maps with configurable base layers and route overlays. Interactive features (route drag-reshape, ghost markers, no-go area drawing, elevation chart, cursor tracking, colored routes) are implemented directly in the Planner app since they are Planner-specific. #### Scenario: Render map component - **WHEN** the map package's MapView component is rendered with a center and zoom diff --git a/openspec/specs/transactional-emails/spec.md b/openspec/specs/transactional-emails/spec.md index d88f2c3..4e50bee 100644 --- a/openspec/specs/transactional-emails/spec.md +++ b/openspec/specs/transactional-emails/spec.md @@ -5,7 +5,7 @@ The system SHALL provide a provider-agnostic email sending function that support #### Scenario: Send email in production - **WHEN** the system sends a transactional email in production -- **THEN** the email is delivered via the configured provider (Resend) to the recipient +- **THEN** the email is delivered via Nodemailer with SMTP (configured via `SMTP_URL` connection string and `SMTP_FROM` env var) to the recipient #### Scenario: Dev mode skips sending - **WHEN** the system sends a transactional email in development