Add 8 proposed changes and old trails analysis

Proposed changes (all with proposal, design, specs, tasks):
- app-navigation (9 tasks) — nav bars for both apps
- planner-landing-page (7 tasks) — standalone landing page
- planner-multiplayer-awareness (13 tasks) — participants, cursors, names
- changelog (13 tasks) — public changelog with "what's new"
- transactional-emails (15 tasks) — magic link + welcome emails
- planner-features (18 tasks) — no-go areas, notes, recovery, rate limits
- komoot-import (23 tasks) — Komoot tour import
- route-features (37 tasks) — sharing, multi-day, spatial, photos

Also adds docs/old-trails-analysis.md with feature analysis from the
older trails project.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-03-25 03:57:20 +01:00
parent 57141cdfab
commit c7c1c275df
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
55 changed files with 1704 additions and 0 deletions

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-25

View file

@ -0,0 +1,72 @@
## Context
trails.cool Journal supports manual route creation and GPX import. Users coming
from Komoot have hundreds of tours they'd lose by switching. The old trails
project had a working Komoot integration using basic auth against Komoot's
undocumented API (`api.komoot.de`).
## Goals / Non-Goals
**Goals:**
- Connect Komoot account via email + password
- Import all tours (paginated) as activities + routes
- Track import progress with batch status
- Deduplicate on re-import (same tour never imported twice)
- Fetch GPX geometry per tour (not just metadata)
**Non-Goals:**
- OAuth flow (Komoot has no public OAuth — basic auth only)
- Real-time sync or webhook-based updates
- Strava/other providers (future iteration, but design the schema generically)
- Background job queue like Inngest (keep it simple — synchronous import with progress)
## Decisions
### D1: Generic integrations table with provider column
Store connections in a `journal.integrations` table with a `provider` enum
(`komoot`, and later `strava` etc). Credentials encrypted at rest. This avoids
a separate table per provider.
### D2: Import batches for progress tracking
Each import creates an `import_batches` row tracking: status (running,
completed, failed), total found, imported count, duplicate count, error message.
The UI polls this for progress.
### D3: Deduplication via composite key
Activities get a `dedupeKey` column. For Komoot: `komoot:{tourId}`. Combined
with `ownerId`, a unique constraint prevents duplicates. Insert uses
`onConflictDoNothing`.
### D4: Synchronous import with streaming progress
No background job queue. The import runs in an API route action that:
1. Fetches all tour pages from Komoot
2. For each tour, fetches GPX and creates activity + route
3. Updates the batch row with progress
4. Client polls `/api/integrations/komoot/import-status` for live updates
This keeps the architecture simple. If imports are too slow (>100 tours), we
can add a background worker later.
### D5: Encrypt credentials with AES-256-GCM
Use Node's `crypto.createCipheriv` with a server-side key derived from
`INTEGRATION_SECRET` env var. Decrypt on use, never log plaintext.
**Alternative considered**: Store only the API token (not email+password).
Rejected because the token may expire and re-auth requires the original
credentials.
## Risks / Trade-offs
- **Komoot API is undocumented** → Could break without notice. Mitigation: wrap
all API calls in error handling, mark integration as "needs reauth" on 401.
- **Storing user passwords for third-party service** → Security risk.
Mitigation: AES-256-GCM encryption, separate `INTEGRATION_SECRET`, document
in privacy manifest.
- **Synchronous import may timeout for large accounts** → Mitigation: paginate
and commit per-page. If a page fails, the batch is marked partial and can be
resumed.

View file

@ -0,0 +1,32 @@
## Why
Users switching to trails.cool have years of tours on Komoot. Without import,
they start with an empty Journal — no history, no motivation to switch. A
one-click Komoot import lets users bring their existing tours and start using
trails.cool immediately.
## What Changes
- Add Komoot account connection flow (email + password → API credentials)
- Import Komoot tours as Journal activities with route data
- Batch import with progress tracking, deduplication, and error handling
- Integration settings page to manage connected accounts and trigger imports
- Fetch full tour GPX (not just metadata) so imported routes have geometry
## Capabilities
### New Capabilities
- `komoot-import`: Connect a Komoot account, import tours as activities with routes, track import progress, deduplicate on re-import
### Modified Capabilities
- `route-management`: Routes can now be created via import (not just manual creation), with an external source tracking field
## Impact
- **Database**: New tables for integration connections and import batches in `journal` schema
- **Files**: New routes (`/integrations`, `/api/integrations/*`), new server utilities for Komoot API, new DB schema tables
- **Dependencies**: No new packages needed — uses fetch for Komoot API, existing Drizzle for DB
- **Privacy**: Komoot credentials stored encrypted. Must be documented in privacy manifest.
- **External**: Komoot API (api.komoot.de) — basic auth, no official developer program

View file

@ -0,0 +1,63 @@
## ADDED Requirements
### Requirement: Connect Komoot account
Users SHALL be able to connect their Komoot account by providing email and password. Credentials are validated against the Komoot API and stored encrypted.
#### Scenario: Successful connection
- **WHEN** user enters valid Komoot email and password on the integrations page
- **THEN** the system validates credentials via `api.komoot.de`, stores them encrypted, and shows the connection as active
#### Scenario: Invalid credentials
- **WHEN** user enters incorrect Komoot credentials
- **THEN** the system shows an error message and does not store credentials
#### Scenario: Disconnect account
- **WHEN** user disconnects their Komoot integration
- **THEN** stored credentials are deleted and no further imports can occur
### Requirement: Import Komoot tours
Users SHALL be able to import all their Komoot tours as Journal activities with linked routes.
#### Scenario: Import all tours
- **WHEN** user clicks "Import" on the integrations page
- **THEN** the system fetches all tours (paginated), creates an activity and route for each, and shows progress
#### Scenario: Tour with GPX geometry
- **WHEN** a Komoot tour is imported
- **THEN** the system fetches the tour's GPX/geometry and creates a route with the full track data
#### Scenario: Import progress
- **WHEN** an import is running
- **THEN** the UI shows: total tours found, imported so far, duplicates skipped, and current status
### Requirement: Deduplication
The system SHALL NOT create duplicate activities when re-importing tours that were previously imported.
#### Scenario: Re-import skips existing tours
- **WHEN** user runs import and a tour was already imported previously
- **THEN** the tour is skipped and counted as a duplicate in the import summary
### Requirement: Import batch tracking
Each import run SHALL be tracked as a batch with status and statistics.
#### Scenario: Batch lifecycle
- **WHEN** an import starts
- **THEN** a batch record is created with status "running", and updated to "completed" or "failed" when done
#### Scenario: Batch statistics
- **WHEN** an import completes
- **THEN** the batch shows: totalFound, importedCount, duplicateCount, and duration
### Requirement: Credential encryption
Komoot credentials SHALL be encrypted at rest using AES-256-GCM.
#### Scenario: Credentials stored securely
- **WHEN** a user connects their Komoot account
- **THEN** the password is encrypted before storage and only decrypted when making API calls
### Requirement: Privacy disclosure
The Komoot integration SHALL be documented in the privacy manifest.
#### Scenario: Privacy manifest updated
- **WHEN** Komoot import is available
- **THEN** the /privacy page documents: what credentials are stored, what data is imported, and that credentials are encrypted

View file

@ -0,0 +1,12 @@
## MODIFIED Requirements
### Requirement: Route creation
The system SHALL support creating routes via import from external services, in addition to manual creation and GPX upload.
#### Scenario: Route created from import
- **WHEN** a Komoot tour is imported
- **THEN** a route is created with name, distance, elevation, GPX geometry, and a source field indicating "komoot"
#### Scenario: Route links to activity
- **WHEN** a tour is imported as an activity
- **THEN** the activity is linked to the created route

View file

@ -0,0 +1,51 @@
## 1. Database Schema
- [ ] 1.1 Add `journal.integrations` table (id, userId, provider enum, encryptedCredentials, apiUsername, status, lastSyncedAt, createdAt)
- [ ] 1.2 Add `journal.import_batches` table (id, userId, integrationId, status enum, totalFound, importedCount, duplicateCount, errorMessage, startedAt, completedAt)
- [ ] 1.3 Add `dedupeKey` column to `journal.activities` table with unique constraint on (ownerId, dedupeKey)
- [ ] 1.4 Add `source` column to `journal.routes` table (nullable, e.g. "komoot", "manual", "gpx-upload")
- [ ] 1.5 Run `pnpm db:push` and verify schema locally
## 2. Credential Encryption
- [ ] 2.1 Create `apps/journal/app/lib/crypto.server.ts` with AES-256-GCM encrypt/decrypt using `INTEGRATION_SECRET` env var
- [ ] 2.2 Write unit tests for encrypt/decrypt roundtrip
## 3. Komoot API Client
- [ ] 3.1 Create `apps/journal/app/lib/komoot.server.ts` with login function (email + password → username + token)
- [ ] 3.2 Add fetchTours function (paginated, fetches all pages)
- [ ] 3.3 Add fetchTourGpx function (fetch GPX geometry for a single tour)
- [ ] 3.4 Write unit tests for API response parsing (mock fetch)
## 4. Import Logic
- [ ] 4.1 Create `apps/journal/app/lib/import.server.ts` with importKomootTours function that: creates batch, pages through tours, fetches GPX, creates activities + routes, deduplicates, updates batch
- [ ] 4.2 Write integration test for import with mock Komoot responses
## 5. API Routes
- [ ] 5.1 Create `POST /api/integrations/komoot/connect` — validate credentials, store encrypted
- [ ] 5.2 Create `POST /api/integrations/komoot/disconnect` — delete credentials
- [ ] 5.3 Create `POST /api/integrations/komoot/import` — trigger import, return batch ID
- [ ] 5.4 Create `GET /api/integrations/komoot/import-status` — return current batch progress
## 6. UI
- [ ] 6.1 Create `/integrations` route with Komoot connection form (email + password)
- [ ] 6.2 Show connected status, last sync time, and import button when connected
- [ ] 6.3 Import progress UI — poll import-status, show counts (found, imported, duplicated)
- [ ] 6.4 Add link to integrations page from Journal navigation
- [ ] 6.5 Add i18n keys for all integration strings (en + de)
## 7. Privacy & Config
- [ ] 7.1 Update /privacy page to document Komoot integration (credentials stored encrypted, what data is imported)
- [ ] 7.2 Add `INTEGRATION_SECRET` env var to docker-compose.yml and CI
- [ ] 7.3 Add `INTEGRATION_SECRET` to deploy secrets documentation
## 8. Verify
- [ ] 8.1 Test full flow locally: connect Komoot → import tours → verify activities + routes created
- [ ] 8.2 Verify deduplication: re-import and confirm no duplicates
- [ ] 8.3 Verify disconnect removes credentials