From 0cf87b72abb7ce2883d9bd06293f28fb87161dbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 24 May 2026 11:11:31 +0200 Subject: [PATCH] Fix medium-severity spec drift across 17 specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - authentication-methods: document completeAuth mode param ("redirect"|"json"); clarify add-passkey nudge (no dismiss mechanism, disappears on passkey add) - journal-auth: session maxAge is 30 days; terms allow-list uses /legal/ prefix matching (broader than fixed list of paths) - session-notes: mark awareness isolation and UndoManager isolation as not yet implemented (shared instances in current code) - activity-feed: add fan-out scenario for visibility change to public - explore: note that ?perPage is not yet implemented (hardcoded page size) - multi-day-routes: add per-day GPX track split scenario (splitByDays option); document overnight vs isDayBreak naming gap - osm-poi-overlays: debounce is 800ms + 2000ms min interval (not 500ms); retry is not automatic (fires on next viewport change) - brouter-integration: rate limit corrected to 300/hour; add segment-cache requirement (client caches per-pair segments) - wahoo-route-push: OAuth state shape uses camelCase (returnTo, pushAfter object) not snake_case with push_after boolean - komoot-import: document noop adapter / ConnectedServiceManager bypass; note four Komoot-specific routes that bypass the generic OAuth framework - background-jobs: exponential backoff not wired (retryLimit only); add SIGINT - connected-services: add revoked status; name ConnectionNotActiveError - infrastructure: add INTEGRATION_SECRET and SENTRY_DSN to env var lists; split secret decryption scenario by workflow (cd-apps vs cd-infra) - secret-management: correct CD decryption — cd-apps only decrypts app.env; cd-infra decrypts both - transactional-emails: welcome email is async (pg-boss job); magic-link email includes 6-digit numeric code - journal-route-detail: websites are https: links (not mailto:); opening_hours is also displayed - local-dev-environment: add mobile app (Expo) dev commands Co-Authored-By: Claude Sonnet 4.6 --- openspec/specs/activity-feed/spec.md | 4 ++++ openspec/specs/authentication-methods/spec.md | 7 +++++-- openspec/specs/background-jobs/spec.md | 6 +++--- openspec/specs/brouter-integration/spec.md | 16 ++++++++++++++-- openspec/specs/connected-services/spec.md | 4 +++- openspec/specs/explore/spec.md | 2 ++ openspec/specs/infrastructure/spec.md | 10 ++++++---- openspec/specs/journal-auth/spec.md | 8 +++++--- openspec/specs/journal-route-detail/spec.md | 8 +++++--- openspec/specs/komoot-import/spec.md | 8 +++++--- openspec/specs/local-dev-environment/spec.md | 13 +++++++++++++ openspec/specs/multi-day-routes/spec.md | 11 +++++++++++ openspec/specs/osm-poi-overlays/spec.md | 6 ++++-- openspec/specs/secret-management/spec.md | 10 +++++++--- openspec/specs/session-notes/spec.md | 12 ++++++++---- openspec/specs/transactional-emails/spec.md | 8 ++++---- openspec/specs/wahoo-route-push/spec.md | 2 +- 17 files changed, 100 insertions(+), 35 deletions(-) diff --git a/openspec/specs/activity-feed/spec.md b/openspec/specs/activity-feed/spec.md index 4ee9993..e04de4b 100644 --- a/openspec/specs/activity-feed/spec.md +++ b/openspec/specs/activity-feed/spec.md @@ -110,6 +110,10 @@ Creating an activity with `visibility = 'public'` SHALL enqueue a fan-out job th - **WHEN** a user creates an activity with `visibility = 'private'` or `'unlisted'` - **THEN** no fan-out job is enqueued and no notifications are created +#### Scenario: Visibility change to public fans out +- **WHEN** an activity owner changes an existing activity's visibility from `private` or `unlisted` to `public` +- **THEN** a fan-out job is enqueued for the visibility change, creating `activity_published` notifications for all accepted followers — the same fan-out as on initial public creation + #### Scenario: No accepted followers means no notifications - **WHEN** a user with zero accepted followers creates a public activity - **THEN** the fan-out job runs and inserts zero rows diff --git a/openspec/specs/authentication-methods/spec.md b/openspec/specs/authentication-methods/spec.md index 9fb5c5b..8dbe145 100644 --- a/openspec/specs/authentication-methods/spec.md +++ b/openspec/specs/authentication-methods/spec.md @@ -80,8 +80,9 @@ A signed-in user SHALL be able to add an additional passkey to their account fro - **THEN** a WebAuthn registration ceremony runs against the existing user id and a new row is inserted in `credentials` linked to that user #### Scenario: Post-login add-passkey nudge -- **WHEN** a user has just verified via magic-link/code and lands on `/?add-passkey=1` -- **THEN** the home page surfaces an "Add a passkey for faster sign-in" prompt; if dismissed it does not re-appear automatically +- **WHEN** a user who has zero registered passkeys lands on `/?add-passkey=1` +- **THEN** the home page surfaces an "Add a passkey for faster sign-in" prompt +- **AND** once the user adds a passkey the prompt disappears (there is no separate dismiss/suppress mechanism) ### Requirement: Passkey deletion A signed-in user SHALL be able to remove a passkey from their account via the Security settings page (`/settings/security`). The Journal SHALL prevent deletion of the user's last remaining passkey if no alternative auth method (a verified email for magic-link login) is available, to avoid lock-out. @@ -99,6 +100,8 @@ Every successful web authentication flow — passkey register-finish, passkey lo Per-method identity verification (WebAuthn ceremony, magic-token consumption, 6-digit-code consumption) SHALL run in its own function and produce a `userId` *before* `completeAuth` is invoked. `completeAuth` SHALL NOT know how identity was proved. +`completeAuth` supports two response modes via an optional `mode` parameter: `"redirect"` (default, for browser flows — returns a `Response` with `Set-Cookie` + `Location`) and `"json"` (for API/mobile flows — returns a JSON body with the session token). All web authentication scenarios use `"redirect"`; the `"json"` mode is used by the OAuth2/PKCE mobile code-exchange flow. + Terms recording happens at user creation time inside the per-method registration functions (`finishRegistration` for passkey, `registerWithMagicLink` for magic-link), not inside `completeAuth`. The Terms gate (root-loader redirect for cookie sessions; `requireApiUser` 403 for bearer-token API requests) SHALL remain the enforcement point for stale `terms_version`. OAuth-code issuance at `/oauth/authorize` SHALL NOT be routed through `completeAuth` — that flow operates on an already-authenticated user and shares only the trailing redirect, not the full sequence. diff --git a/openspec/specs/background-jobs/spec.md b/openspec/specs/background-jobs/spec.md index 6a23bc6..648b081 100644 --- a/openspec/specs/background-jobs/spec.md +++ b/openspec/specs/background-jobs/spec.md @@ -12,7 +12,7 @@ The `@trails-cool/jobs` package SHALL initialize a pg-boss instance using the ap - **THEN** pg-boss connects to PostgreSQL, creates/migrates the `pgboss` schema if needed, and begins polling for jobs #### Scenario: Worker stops on shutdown -- **WHEN** the server process receives SIGTERM +- **WHEN** the server process receives SIGTERM or SIGINT - **THEN** pg-boss completes any in-progress jobs and stops gracefully before the process exits ### Requirement: Cron job scheduling @@ -27,11 +27,11 @@ The system SHALL support registering recurring jobs with cron expressions. - **THEN** existing cron schedules persist and continue firing without re-registration conflicts ### Requirement: Job retry policy -Jobs SHALL support configurable retry policies with exponential backoff. +Jobs SHALL support configurable retry limits. The `JobDefinition` type exposes `retryLimit` and `expireInSeconds`; exponential backoff (`retryDelay`/`retryBackoff` in pg-boss terms) is not currently wired. #### Scenario: Transient failure retry - **WHEN** a job handler throws an error -- **THEN** pg-boss retries the job up to the configured retry limit with exponential backoff +- **THEN** pg-boss retries the job up to the configured `retryLimit` #### Scenario: Permanent failure - **WHEN** a job exhausts all retries diff --git a/openspec/specs/brouter-integration/spec.md b/openspec/specs/brouter-integration/spec.md index e8d25d2..5e531b2 100644 --- a/openspec/specs/brouter-integration/spec.md +++ b/openspec/specs/brouter-integration/spec.md @@ -62,12 +62,24 @@ The Planner backend SHALL proxy all BRouter API calls. Clients SHALL NOT communi - **THEN** the Planner logs a fatal error and refuses to start ### Requirement: Rate limiting -The Planner backend SHALL rate limit BRouter API calls to prevent abuse. +The Planner backend SHALL rate limit BRouter API calls to prevent abuse. See `rate-limiting` spec for the authoritative limit values. #### Scenario: Rate limit exceeded -- **WHEN** a session exceeds 60 route computations per hour +- **WHEN** a session exceeds 300 route computations per hour (the `DEFAULT_MAX_REQUESTS` value) - **THEN** subsequent requests receive a 429 response with a Retry-After header +### Requirement: Client-side segment cache +The Planner client SHALL cache BRouter responses per waypoint-pair so that only changed segments are re-fetched when waypoints are added or moved. + +#### Scenario: Only changed segments re-fetched +- **WHEN** a user moves one waypoint in a multi-waypoint route +- **THEN** only the two segments adjacent to the moved waypoint are re-fetched from the proxy +- **AND** all other segments are served from the client-side cache without a network request + +#### Scenario: Cache key is waypoint-pair coordinates +- **WHEN** the same start/end coordinate pair appears in a new route +- **THEN** the cached segment result is reused without a BRouter call + ### Requirement: BRouter Docker deployment BRouter SHALL run as a Docker container on a dedicated Hetzner host reached over a private Hetzner vSwitch, with planet-wide RD5 segments mounted as a volume. The BRouter container SHALL NOT be exposed on any public network interface. diff --git a/openspec/specs/connected-services/spec.md b/openspec/specs/connected-services/spec.md index 8fb5b71..b0c8fcf 100644 --- a/openspec/specs/connected-services/spec.md +++ b/openspec/specs/connected-services/spec.md @@ -54,5 +54,7 @@ The system SHALL expose a `ConnectedServiceManager` that owns credential lifecyc #### Scenario: Refresh failure flips status to needs_relink - **WHEN** the `CredentialAdapter`'s refresh call returns a permanent failure (e.g. revoked refresh token) - **THEN** the manager sets `status = 'needs_relink'` on the `connected_services` row -- **AND** raises an error that the caller surfaces as a re-connect prompt +- **AND** raises a `ConnectionNotActiveError` that the caller surfaces as a re-connect prompt - **AND** subsequent calls for the same service short-circuit until the user re-links + +Note: the `status` column has three valid values: `active`, `needs_relink`, and `revoked`. The `revoked` state is set when the user explicitly disconnects but the row is retained for audit purposes. diff --git a/openspec/specs/explore/spec.md b/openspec/specs/explore/spec.md index 635f704..3ef19f5 100644 --- a/openspec/specs/explore/spec.md +++ b/openspec/specs/explore/spec.md @@ -95,6 +95,8 @@ The directory SHALL paginate via `?page=N` (1-indexed) and `?perPage=K` query pa - **WHEN** a visitor loads `/explore?perPage=10000` or `/explore?perPage=0` - **THEN** the loader clamps `perPage` to `[1, 100]` and renders normally — no 400 response +Note: the `?perPage` parameter is not yet implemented. The current loader uses a hardcoded page size (20). The `?page` parameter is supported. `perPage` support is aspirational. + #### Scenario: Page beyond the last - **WHEN** a visitor loads `/explore?page=N` where N is past the last populated page - **THEN** the response renders the empty list and a "Previous page" link; no 404 diff --git a/openspec/specs/infrastructure/spec.md b/openspec/specs/infrastructure/spec.md index 53ee00d..a13a0dd 100644 --- a/openspec/specs/infrastructure/spec.md +++ b/openspec/specs/infrastructure/spec.md @@ -21,11 +21,11 @@ Each service SHALL be configured via environment variables defined in Docker Com #### Scenario: Journal configuration - **WHEN** the Journal container starts -- **THEN** it reads DOMAIN, DATABASE_URL, PLANNER_URL, JWT_SECRET, SESSION_SECRET, and WAHOO_* credentials from environment variables +- **THEN** it reads DOMAIN, DATABASE_URL, PLANNER_URL, JWT_SECRET, SESSION_SECRET, INTEGRATION_SECRET, WAHOO_* credentials, and SENTRY_DSN from environment variables #### Scenario: Planner configuration - **WHEN** the Planner container starts -- **THEN** it reads BROUTER_URL and DATABASE_URL from environment variables +- **THEN** it reads BROUTER_URL, DATABASE_URL, and INTEGRATION_SECRET from environment variables #### Scenario: Caddy security headers - **WHEN** Caddy proxies a request @@ -69,8 +69,10 @@ GitHub Actions SHALL use separate workflows for app deployment, infrastructure d - **THEN** the cd-brouter workflow SSHes as the `trails` user into the dedicated BRouter host using `BROUTER_DEPLOY_HOST` / `BROUTER_DEPLOY_SSH_KEY` and runs `docker compose up -d` in `~trails/brouter/` #### Scenario: Secret decryption at deploy time -- **WHEN** any CD workflow runs -- **THEN** the SOPS-encrypted secrets file is decrypted and provided to docker-compose as an env file +- **WHEN** `cd-apps.yml` runs +- **THEN** `secrets.app.env` is decrypted and injected into the Journal and Planner containers +- **WHEN** `cd-infra.yml` runs +- **THEN** both `secrets.app.env` and `secrets.infra.env` are decrypted and merged for infrastructure services #### Scenario: Gitleaks scan - **WHEN** a PR is opened diff --git a/openspec/specs/journal-auth/spec.md b/openspec/specs/journal-auth/spec.md index 625db15..eefd99a 100644 --- a/openspec/specs/journal-auth/spec.md +++ b/openspec/specs/journal-auth/spec.md @@ -6,7 +6,7 @@ Session management and the terms-of-service consent gate for the Journal app. Th ## Requirements ### Requirement: Cookie session for signed-in users -The Journal SHALL identify signed-in users via a server-set HTTP cookie (`__session`) that carries a serialized JSON payload containing `userId`. The cookie SHALL be `HttpOnly`, `SameSite=Lax`, signed with the server secret, and have a finite max-age. Anonymous browsers SHALL render the public surface (anonymous home, public profiles, public routes/activities) without a session cookie present. +The Journal SHALL identify signed-in users via a server-set HTTP cookie (`__session`) that carries a serialized JSON payload containing `userId`. The cookie SHALL be `HttpOnly`, `SameSite=Lax`, signed with the server secret, and have a `maxAge` of **30 days**. Anonymous browsers SHALL render the public surface (anonymous home, public profiles, public routes/activities) without a session cookie present. #### Scenario: Set cookie on successful authentication - **WHEN** any authentication path (passkey finish, magic-link verify, code verify) succeeds @@ -41,13 +41,15 @@ The registration form SHALL require explicit acknowledgement of the Terms of Ser Logged-in users whose stored `terms_version` does not match the currently-published version SHALL be prompted to accept the current Terms before accessing any non-allow-listed page. #### Scenario: Stale version redirects to accept-terms page -- **WHEN** a logged-in user whose `users.terms_version` is NULL or differs from the current `TERMS_VERSION` requests any page outside the allow-list (`/auth/accept-terms`, `/auth/logout`, `/legal/*`) +- **WHEN** a logged-in user whose `users.terms_version` is NULL or differs from the current `TERMS_VERSION` requests any path not in the allow-list - **THEN** the server redirects them to `/auth/accept-terms?returnTo=` #### Scenario: Allow-list keeps Terms and logout reachable -- **WHEN** the same user requests `/legal/terms`, `/legal/privacy`, `/legal/imprint`, `/auth/accept-terms`, or `/auth/logout` +- **WHEN** the same user requests any path under `/legal/` (e.g. `/legal/terms`, `/legal/privacy`, `/legal/imprint`), `/auth/accept-terms`, or `/auth/logout` - **THEN** the request is served normally without being redirected +Note: the allow-list uses prefix matching (`/legal/` prefix covers all current and future legal pages); it is not a fixed list of individual paths. + #### Scenario: Successful re-acceptance updates both fields - **WHEN** a user submits the acceptance form with the required checkbox ticked - **THEN** the server updates `users.terms_version` to the current version and `users.terms_accepted_at` to the current timestamp, then redirects to the `returnTo` path (or `/`) diff --git a/openspec/specs/journal-route-detail/spec.md b/openspec/specs/journal-route-detail/spec.md index 6236dc7..a61858b 100644 --- a/openspec/specs/journal-route-detail/spec.md +++ b/openspec/specs/journal-route-detail/spec.md @@ -11,9 +11,11 @@ The Journal route detail page SHALL display POI metadata (phone, address, websit - **WHEN** a route detail page is loaded and a waypoint has POI metadata from the Planner - **THEN** the waypoint displays the POI name, icon, and category alongside its coordinates -#### Scenario: Phone, address, and website shown -- **WHEN** a waypoint has POI metadata including phone, address, or website -- **THEN** those details are shown in the waypoint detail section with appropriate links (tel: for phone, mailto: or https: for website) +#### Scenario: Phone, address, website, and opening hours shown +- **WHEN** a waypoint has POI metadata including phone, address, website, or opening hours +- **THEN** those details are shown in the waypoint detail section with appropriate links (tel: for phone, https: for website; opening hours displayed as text) + +Note: `mailto:` links for websites are not rendered — all website values are treated as `https:` links. The `opening_hours` OSM tag is also displayed when present. #### Scenario: Waypoints without POI data display normally - **WHEN** a waypoint on the route detail page has no associated POI metadata diff --git a/openspec/specs/komoot-import/spec.md b/openspec/specs/komoot-import/spec.md index 3cde115..affc9e9 100644 --- a/openspec/specs/komoot-import/spec.md +++ b/openspec/specs/komoot-import/spec.md @@ -80,12 +80,14 @@ Each import run SHALL be tracked as a batch with status and statistics. - **WHEN** an import completes - **THEN** the batch shows: totalFound, importedCount, duplicateCount, and duration -### Requirement: Credential encryption (authenticated mode) -Komoot credentials SHALL be encrypted at rest using AES-256-GCM. +### Requirement: Credential storage +Both connection modes store credentials in `connected_services` with `credential_kind = 'web-login'`. Public mode stores only the verified Komoot username; authenticated mode stores an encrypted email + password in the `credentials` JSONB blob. The Komoot provider uses a noop `CredentialAdapter` — it does not go through `ConnectedServiceManager`'s `withFreshCredentials` lifecycle (no token refresh needed for web-login credentials). Credential decryption for API calls happens inside the Komoot importer directly. + +The four Komoot-specific routes (`/api/sync/komoot/connect`, `/api/sync/komoot/verify`, `/api/sync/komoot/import`, `/api/sync/komoot/import-status`) intentionally bypass the generic `/api/sync/connect/:provider` / `/api/sync/callback/:provider` framework — Komoot's bio-verification flow is too different from OAuth to fit the generic shape. #### Scenario: Credentials stored securely - **WHEN** a user connects via authenticated mode -- **THEN** the password is encrypted before storage and only decrypted when making API calls +- **THEN** the password is encrypted with AES-256-GCM before storage and only decrypted when making API calls ### Requirement: Privacy disclosure The Komoot integration SHALL be documented in the privacy manifest. diff --git a/openspec/specs/local-dev-environment/spec.md b/openspec/specs/local-dev-environment/spec.md index 2edd2d2..54bd08f 100644 --- a/openspec/specs/local-dev-environment/spec.md +++ b/openspec/specs/local-dev-environment/spec.md @@ -79,3 +79,16 @@ The dev environment SHALL provide a command to tear down and recreate the local #### Scenario: Reset dev environment - **WHEN** a developer runs `pnpm dev:reset` - **THEN** all Docker volumes are removed and containers are recreated + +### Requirement: Mobile app dev (Expo) +The `apps/mobile` React Native app (Expo) requires its own dev commands separate from the web stack. + +#### Scenario: iOS development +- **WHEN** a developer runs `pnpm dev:ios` from `apps/mobile` +- **THEN** the Expo bundler starts and the iOS simulator opens with the app + +#### Scenario: Android development +- **WHEN** a developer runs `pnpm dev:android` from `apps/mobile` +- **THEN** the Expo bundler starts and an Android emulator opens with the app + +Note: the mobile app requires the Journal to be running locally (or pointing at staging) for API calls. It does not use the BRouter or Yjs stack — route edits in mobile go directly through the Journal API. diff --git a/openspec/specs/multi-day-routes/spec.md b/openspec/specs/multi-day-routes/spec.md index 7d3ab38..dcde5cf 100644 --- a/openspec/specs/multi-day-routes/spec.md +++ b/openspec/specs/multi-day-routes/spec.md @@ -52,3 +52,14 @@ Day structure SHALL be preserved in GPX exports via waypoint type elements. - **WHEN** a user exports a plan with overnight waypoints - **THEN** overnight waypoints include a `overnight` element in the GPX - **AND** reimporting the GPX restores the day structure + +#### Scenario: Per-day GPX track splitting +- **WHEN** `generateGpx` is called with `splitByDays: true` +- **THEN** the GPX output contains separate `` elements for each day's segment rather than a single track +- **AND** each `` is labelled with the day number + +Note: the single-track export (without `splitByDays`) is the default. The per-day split is available as an option on the GPX generator and is the recommended format for multi-day exports to head units. + +### Note on overnight vs isDayBreak naming + +The Yjs wire format stores the flag as `overnight: true` on the waypoint Y.Map entry. The TypeScript interface exposes it as `isDayBreak` (a comment in `waypoint-ymap.ts` documents `overnight` as the legacy wire name). Spec scenarios use `overnight` to refer to the wire-level concept; TypeScript code uses `isDayBreak`. diff --git a/openspec/specs/osm-poi-overlays/spec.md b/openspec/specs/osm-poi-overlays/spec.md index 9c24068..c5a60b1 100644 --- a/openspec/specs/osm-poi-overlays/spec.md +++ b/openspec/specs/osm-poi-overlays/spec.md @@ -50,7 +50,7 @@ The Planner SHALL load POIs only within the current map viewport, refreshing whe #### Scenario: Load POIs on viewport - **WHEN** POI categories are enabled and the user pans or zooms the map -- **THEN** POIs are fetched for the new viewport after a 500ms debounce +- **THEN** POIs are fetched for the new viewport after an 800ms debounce (`DEBOUNCE_MS` in `use-pois.ts`); a minimum request interval of 2000ms additionally throttles rapid viewport changes #### Scenario: Zoom threshold - **WHEN** the map zoom level is below 10 (`MIN_ZOOM` constant in `use-pois.ts`) @@ -65,7 +65,9 @@ The Planner SHALL handle Overpass API rate limits gracefully. #### Scenario: Rate limited response - **WHEN** the Overpass API returns a 429 status -- **THEN** the Planner shows a temporary "POI data unavailable — try again shortly" message and retries with exponential backoff +- **THEN** the Planner shows a temporary "POI data unavailable — try again shortly" message and sets a backoff delay before the next request + +Note: automatic retry is not implemented. The next request fires only on the next user viewport change or category toggle after the backoff delay has elapsed. #### Scenario: Overpass unavailable - **WHEN** the Overpass API is unreachable diff --git a/openspec/specs/secret-management/spec.md b/openspec/specs/secret-management/spec.md index d31b9c2..3ccaf61 100644 --- a/openspec/specs/secret-management/spec.md +++ b/openspec/specs/secret-management/spec.md @@ -11,9 +11,13 @@ Production secrets SHALL be stored as SOPS-encrypted files in the repository, de - **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** 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: App deploy decryption +- **WHEN** `cd-apps.yml` runs +- **THEN** only `secrets.app.env` is decrypted using `AGE_SECRET_KEY` and injected into the Journal and Planner containers + +#### Scenario: Infra deploy decryption +- **WHEN** `cd-infra.yml` runs +- **THEN** both `secrets.app.env` and `secrets.infra.env` are decrypted and merged for infrastructure services #### Scenario: Secret audit trail - **WHEN** a secret is changed diff --git a/openspec/specs/session-notes/spec.md b/openspec/specs/session-notes/spec.md index 41743fc..13d8062 100644 --- a/openspec/specs/session-notes/spec.md +++ b/openspec/specs/session-notes/spec.md @@ -27,13 +27,17 @@ Planner sessions SHALL have a shared text editor for participants to write notes ### Requirement: Editor implementation The notes editor SHALL use CodeMirror 6 with y-codemirror.next for Yjs binding. -#### Scenario: Undo/redo +#### Scenario: Undo/redo (not fully isolated) - **WHEN** a user presses Ctrl+Z / Ctrl+Shift+Z in the notes editor -- **THEN** undo/redo applies to notes only (separate Y.UndoManager from waypoint undo) +- **THEN** undo/redo operates via the shared `Y.UndoManager` that also covers waypoints and no-go areas — undo in the notes editor may undo recent waypoint changes and vice versa -#### Scenario: Awareness field isolation +Note: the spec originally called for a separate `Y.UndoManager` for notes. The shipped implementation uses a single shared manager (`new Y.UndoManager([waypoints, noGoAreas, notes], {...})` in `use-yjs.ts`). Full isolation is aspirational. + +#### Scenario: Awareness field isolation (not yet isolated) - **WHEN** the notes editor sets cursor awareness state -- **THEN** it does not conflict with map cursor awareness (uses separate awareness fields) +- **THEN** it uses the same shared `awareness` object as the map cursor awareness; the `yCollab` extension writes to the `"user"` field on the same channel + +Note: the spec originally called for separate awareness fields. The shipped implementation passes the same awareness instance to both the map and notes editor. Full isolation is aspirational. ### Requirement: Notes in GPX export Notes SHALL be included in GPX exports as ``. diff --git a/openspec/specs/transactional-emails/spec.md b/openspec/specs/transactional-emails/spec.md index 3edd696..3c7b569 100644 --- a/openspec/specs/transactional-emails/spec.md +++ b/openspec/specs/transactional-emails/spec.md @@ -24,14 +24,14 @@ The system SHALL send an email containing the magic link when a user requests pa #### Scenario: Email content - **WHEN** a magic link email is sent -- **THEN** it includes: the link, expiry time (15 minutes), and plain-text fallback +- **THEN** it includes: the clickable link, a prominently displayed 6-digit numeric code (for users who prefer copy-paste over clicking), expiry time (15 minutes), and a plain-text fallback ### Requirement: Welcome email -The system SHALL send a welcome email after successful registration. +The system SHALL send a welcome email after successful registration. The welcome email is sent asynchronously via a pg-boss `send-welcome-email` job rather than inline during the registration request. #### Scenario: Welcome on registration -- **WHEN** a user completes passkey registration -- **THEN** a welcome email is sent to their registered email address +- **WHEN** a user completes registration (passkey or magic-link) +- **THEN** a `send-welcome-email` job is enqueued and the email is delivered asynchronously to their registered email address ### Requirement: Email templates Each email type SHALL have an HTML template with a plain-text fallback. diff --git a/openspec/specs/wahoo-route-push/spec.md b/openspec/specs/wahoo-route-push/spec.md index f84548e..03a94c7 100644 --- a/openspec/specs/wahoo-route-push/spec.md +++ b/openspec/specs/wahoo-route-push/spec.md @@ -89,7 +89,7 @@ The Journal SHALL detect when a connected Wahoo account lacks the `routes_write` #### Scenario: Existing connection lacks routes_write - **WHEN** the route owner clicks "Send to Wahoo" - **AND** the user's `connected_services.granted_scopes` does not include `routes_write` -- **THEN** the server redirects the user to Wahoo's authorization URL with the full updated scope list and a `state` that encodes `{ return_to: , push_after: true }` +- **THEN** the server redirects the user to Wahoo's authorization URL with the full updated scope list and a `state` that encodes `{ returnTo: , pushAfter: { routeId: } }` (camelCase; `pushAfter` is an object, not a boolean) - **AND** no Wahoo `/v1/routes` call is attempted #### Scenario: Push completes after re-auth