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:
parent
57141cdfab
commit
c7c1c275df
55 changed files with 1704 additions and 0 deletions
235
docs/old-trails-analysis.md
Normal file
235
docs/old-trails-analysis.md
Normal file
|
|
@ -0,0 +1,235 @@
|
||||||
|
# Feature Analysis: /Users/ullrich/Projects/trails
|
||||||
|
|
||||||
|
Analysis of the older trails Remix app for features and patterns that could be
|
||||||
|
ported or adapted for trails-cool. Organized by category with implementation
|
||||||
|
references.
|
||||||
|
|
||||||
|
## Social Features
|
||||||
|
|
||||||
|
### Reactions/Kudos System
|
||||||
|
Allow users to react to activities with configurable reaction types (currently
|
||||||
|
"kudos"). Generic implementation supports any target type (profile, activity,
|
||||||
|
etc.). UI shows reaction counts on activity cards.
|
||||||
|
|
||||||
|
- `drizzle/models/social-interactions.ts`
|
||||||
|
- `app/routes/api+/activities.$activityId.reactions.ts`
|
||||||
|
|
||||||
|
### Comments/Replies
|
||||||
|
Thread-based comment system on activities. Comments stored as social
|
||||||
|
interactions with "reply" kind. Shows reply counts alongside reactions.
|
||||||
|
|
||||||
|
- `app/routes/api+/activities.$activityId.replies.ts`
|
||||||
|
- `app/routes/feed.tsx`
|
||||||
|
|
||||||
|
### User Following
|
||||||
|
Follow/unfollow user profiles with mutual follow tracking and uniqueness
|
||||||
|
constraints. Can be extended to follow specific routes or activities.
|
||||||
|
|
||||||
|
- `app/routes/api+/follows.ts`
|
||||||
|
- `app/utils/social.server.ts`
|
||||||
|
|
||||||
|
### Personalized Activity Feed
|
||||||
|
Feed showing activities from followed users, filtered by visibility (public,
|
||||||
|
followers-only, private). Shows interaction counts (reactions, replies).
|
||||||
|
|
||||||
|
- `app/routes/feed.tsx`
|
||||||
|
- `app/utils/social.server.ts`
|
||||||
|
|
||||||
|
## Import & Integration
|
||||||
|
|
||||||
|
### Activity Import from External Services
|
||||||
|
Komoot integration for importing tours. Batch import tracking with status
|
||||||
|
(queued, running, completed, failed). Automatic deduplication using dedupeKey
|
||||||
|
prevents duplicate imports. Detailed statistics (totalFound, importedCount,
|
||||||
|
duplicateCount).
|
||||||
|
|
||||||
|
- `app/inngest/importActivities.ts`
|
||||||
|
- `app/utils/integrations.server.ts`
|
||||||
|
|
||||||
|
### Third-Party Sync Jobs
|
||||||
|
Background Inngest jobs for syncing external platforms. Handles credential
|
||||||
|
validation and error recovery. Tracks lastSyncedAt for incremental syncing.
|
||||||
|
|
||||||
|
- `app/inngest/komootSync.ts`
|
||||||
|
|
||||||
|
### Integration Connections Management
|
||||||
|
Store encrypted credentials for multiple service integrations. Track connection
|
||||||
|
status and last sync time. Support for multiple users connecting the same
|
||||||
|
integration independently.
|
||||||
|
|
||||||
|
- `app/routes/integrations.tsx`
|
||||||
|
- `app/routes/api+/integrations.ts`
|
||||||
|
|
||||||
|
## Route Planning
|
||||||
|
|
||||||
|
### Ride Plan Scheduling
|
||||||
|
Schedule rides with timezone-aware start/end times. Privacy levels
|
||||||
|
(followers-only, private, public). Group ride flag for collaborative planning.
|
||||||
|
|
||||||
|
- `drizzle/models/ride-plans.ts`
|
||||||
|
|
||||||
|
### Route Geometry Encoding
|
||||||
|
Encoded polyline storage for efficient geometry. Route metrics: distance,
|
||||||
|
elevation gain, origin, destination. Multiple routes per ride plan.
|
||||||
|
|
||||||
|
- `drizzle/models/ride-routes.ts`
|
||||||
|
- `app/components/ride-plans/route-editor.tsx`
|
||||||
|
|
||||||
|
### Ride Plan Participants
|
||||||
|
Invite users to group rides. Track participation status (invited, accepted,
|
||||||
|
declined). Enables collaborative ride planning.
|
||||||
|
|
||||||
|
- `drizzle/models/ride-plan-participants.ts`
|
||||||
|
|
||||||
|
## Activity Tracking
|
||||||
|
|
||||||
|
### Live Activity Recording
|
||||||
|
Start/pause/finish live recordings. Records start time, end time, duration,
|
||||||
|
distance, elevation gain. Manual form completion for recorded activities.
|
||||||
|
Activity status tracking (recording, paused, completed).
|
||||||
|
|
||||||
|
- `app/components/activities/activity-recorder.tsx`
|
||||||
|
- `app/routes/api+/activities.recordings.start.ts`
|
||||||
|
|
||||||
|
### Activity Visibility Levels
|
||||||
|
Public, followers-only, and private visibility options. Source tracking (native
|
||||||
|
recording vs. import). Link activities to specific ride plans and routes.
|
||||||
|
|
||||||
|
- `drizzle/models/ride-activities.ts`
|
||||||
|
|
||||||
|
## User Profile & Content
|
||||||
|
|
||||||
|
### Profile Images
|
||||||
|
S3-compatible storage for profile pictures. Signed PUT requests for direct
|
||||||
|
client uploads. Image serving via openimg library for optimization.
|
||||||
|
|
||||||
|
- `app/utils/storage.server.ts`
|
||||||
|
- `drizzle/models/user-images.ts`
|
||||||
|
|
||||||
|
### User Notes/Blog
|
||||||
|
Rich note creation with title and markdown content. Image attachments to notes
|
||||||
|
(up to 5 images per note). Note listing and detail views. Image form validation
|
||||||
|
(3MB max per file).
|
||||||
|
|
||||||
|
- `drizzle/models/notes.ts`
|
||||||
|
- `app/routes/users+/$username_+/notes.tsx`
|
||||||
|
|
||||||
|
### Note Images
|
||||||
|
Upload and manage images within notes. Alt text support for accessibility. S3
|
||||||
|
storage with organized paths.
|
||||||
|
|
||||||
|
- `drizzle/models/note-images.ts`
|
||||||
|
- `app/routes/users+/$username_+/__note-editor.tsx`
|
||||||
|
|
||||||
|
## Search & Discovery
|
||||||
|
|
||||||
|
### User Search
|
||||||
|
Full-text search over username and display name. Pagination with limit of 50
|
||||||
|
results. Search results with profile images. Case-insensitive matching.
|
||||||
|
|
||||||
|
- `app/routes/users+/index.tsx`
|
||||||
|
|
||||||
|
### Search Bar Component
|
||||||
|
Debounced auto-submit search. Reusable search UI component. Query parameter
|
||||||
|
preservation.
|
||||||
|
|
||||||
|
- `app/components/search-bar.tsx`
|
||||||
|
|
||||||
|
## Authentication & Settings
|
||||||
|
|
||||||
|
### Two-Factor Authentication
|
||||||
|
TOTP-based 2FA on top of passkeys. Settings UI for enabling/disabling.
|
||||||
|
Verification flow.
|
||||||
|
|
||||||
|
- `app/routes/settings+/profile.two-factor.tsx`
|
||||||
|
|
||||||
|
### Email Verification
|
||||||
|
Email-based account verification. Verification token tracking. Email change
|
||||||
|
flow with verification.
|
||||||
|
|
||||||
|
- `drizzle/models/verifications.ts`
|
||||||
|
|
||||||
|
### Profile Management
|
||||||
|
Change email, password, name. Photo upload. Passkey management. Connected
|
||||||
|
OAuth providers (GitHub).
|
||||||
|
|
||||||
|
- `app/routes/settings+/profile.tsx`
|
||||||
|
|
||||||
|
## Federation
|
||||||
|
|
||||||
|
### ActivityPub Foundation
|
||||||
|
Inbox/outbox endpoints for federated social network. Background publishing via
|
||||||
|
Inngest. Federation record tracking for local/federated entity mapping.
|
||||||
|
|
||||||
|
- `app/routes/api+/activitypub.inbox.ts`
|
||||||
|
- `app/routes/api+/activitypub.outbox.ts`
|
||||||
|
|
||||||
|
## Data Management
|
||||||
|
|
||||||
|
### Activity Files Association
|
||||||
|
Attach files/media to activities. Track file metadata and relationships.
|
||||||
|
|
||||||
|
- `drizzle/models/activity-files.ts`
|
||||||
|
|
||||||
|
### Import Batch Tracking
|
||||||
|
Comprehensive import job tracking. Status lifecycle: queued, running,
|
||||||
|
completed, failed. Timing and statistics per batch. Progress monitoring in UI.
|
||||||
|
|
||||||
|
- `drizzle/models/import-batches.ts`
|
||||||
|
|
||||||
|
## Architectural Patterns
|
||||||
|
|
||||||
|
### Role-Based Access Control
|
||||||
|
User roles and permission system. Basis for admin controls. Permission groups
|
||||||
|
for feature access.
|
||||||
|
|
||||||
|
- `drizzle/models/roles.ts`
|
||||||
|
- `drizzle/models/permissions.ts`
|
||||||
|
|
||||||
|
### Background Job Queue (Inngest)
|
||||||
|
Event-driven async processing. Reliable job retry with exponential backoff.
|
||||||
|
Step-based workflow for complex operations. Progress monitoring and error
|
||||||
|
handling.
|
||||||
|
|
||||||
|
- `app/inngest/`
|
||||||
|
|
||||||
|
### Deduplication Strategy
|
||||||
|
Unique composite keys (ownerId + dedupeKey) for imports. onConflictDoNothing
|
||||||
|
pattern prevents duplicates. Useful for imports from multiple sources.
|
||||||
|
|
||||||
|
- `drizzle/models/ride-activities.ts`
|
||||||
|
|
||||||
|
## UX/Component Patterns
|
||||||
|
|
||||||
|
### Status Button
|
||||||
|
Button that displays async operation states (idle, pending, success, error).
|
||||||
|
Useful for form submissions and API calls.
|
||||||
|
|
||||||
|
- `app/components/ui/status-button.tsx`
|
||||||
|
|
||||||
|
### Form Validation (Conform + Zod)
|
||||||
|
Client/server validation with Conform.js and Zod. Constraint generation from
|
||||||
|
schemas. Field error display and form recovery.
|
||||||
|
|
||||||
|
- `app/components/forms.tsx`
|
||||||
|
|
||||||
|
## Priority Suggestions for trails-cool
|
||||||
|
|
||||||
|
**High (directly applicable):**
|
||||||
|
- Reactions/Kudos — simple engagement, minimal schema
|
||||||
|
- Comments — essential social feature
|
||||||
|
- Following — core for Phase 2 federation
|
||||||
|
- Live Activity Recording — core outdoor feature
|
||||||
|
- User Search — discovery
|
||||||
|
|
||||||
|
**Medium (enhances existing):**
|
||||||
|
- Profile images — user experience
|
||||||
|
- Activity visibility levels — privacy controls
|
||||||
|
- Import from Komoot/Strava — integration value
|
||||||
|
- Ride plan participants — collaborative planning
|
||||||
|
|
||||||
|
**Lower (future):**
|
||||||
|
- Notes/Blog — content creation
|
||||||
|
- 2FA — security hardening
|
||||||
|
- Role-based access — admin features
|
||||||
|
- Background jobs (Inngest pattern) — already have infra for it
|
||||||
2
openspec/changes/app-navigation/.openspec.yaml
Normal file
2
openspec/changes/app-navigation/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-25
|
||||||
40
openspec/changes/app-navigation/design.md
Normal file
40
openspec/changes/app-navigation/design.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Navigation audit found: Journal's `/routes` and `/activities` pages are
|
||||||
|
orphaned (no links point to them). The Planner home page has no CTA. Logout
|
||||||
|
is only on the profile page. Users must know URLs to use the apps.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Every non-API route reachable via UI navigation
|
||||||
|
- Logged-in Journal users see: Routes, Activities, profile, logout
|
||||||
|
- Logged-out Journal users see: login, register
|
||||||
|
- Planner home has a clear "start planning" action
|
||||||
|
- Keep it minimal — a thin bar, not a complex sidebar
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Mobile hamburger menu (responsive PR already hides sidebar — nav bar wraps naturally)
|
||||||
|
- Breadcrumbs or nested navigation
|
||||||
|
- Settings page (future)
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### D1: Journal nav bar in root Layout
|
||||||
|
|
||||||
|
Add a `<nav>` element inside `Layout` in `root.tsx`. The root loader already
|
||||||
|
returns the user (added for Sentry context), so the nav bar can conditionally
|
||||||
|
show authenticated vs. unauthenticated links. No new loader needed.
|
||||||
|
|
||||||
|
### D2: Planner gets a CTA on home, not a nav bar
|
||||||
|
|
||||||
|
The Planner is session-focused — users arrive, create a session, and work in
|
||||||
|
it. A full nav bar would be overhead. Instead: home page gets a prominent
|
||||||
|
"Start Planning" button linking to `/new`. Session view already has a header —
|
||||||
|
add a small logo/home link to it.
|
||||||
|
|
||||||
|
### D3: Use @trails-cool/ui for shared nav component
|
||||||
|
|
||||||
|
The nav bar itself is Journal-specific (routes, activities, auth). No need
|
||||||
|
to put it in the shared UI package — it goes directly in Journal's root.tsx
|
||||||
|
or a colocated component.
|
||||||
33
openspec/changes/app-navigation/proposal.md
Normal file
33
openspec/changes/app-navigation/proposal.md
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Both apps have pages that are only reachable by typing URLs. The Journal has no
|
||||||
|
navigation bar — logged-in users can't find their routes or activities without
|
||||||
|
knowing the URLs. The Planner home page is a dead end with no way to create a
|
||||||
|
session. This makes the apps unusable for anyone who doesn't know the URL
|
||||||
|
structure.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **Journal**: Add a navigation bar (home, routes, activities, profile, logout)
|
||||||
|
that appears when logged in. Unauthenticated users see login/register links.
|
||||||
|
- **Planner**: Add a "New Session" CTA on the home page. Add a minimal header
|
||||||
|
in session view with a link back to home.
|
||||||
|
- Both apps get consistent, minimal navigation that makes all features
|
||||||
|
discoverable.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
(None — this adds UI chrome to existing features, no new behavioral capabilities.)
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `map-display`: Planner home page gains a CTA and session header gains a home link
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Files**: Journal `root.tsx` (nav bar), Planner `home.tsx` (CTA),
|
||||||
|
Planner `SessionView.tsx` (home link), new `NavBar` component
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **i18n**: New translation keys for navigation labels
|
||||||
12
openspec/changes/app-navigation/specs/map-display/spec.md
Normal file
12
openspec/changes/app-navigation/specs/map-display/spec.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Planner home page
|
||||||
|
The Planner home page SHALL provide a clear call-to-action to create a new planning session and a link back to the home page from within sessions.
|
||||||
|
|
||||||
|
#### Scenario: Home page CTA
|
||||||
|
- **WHEN** a user visits the Planner home page
|
||||||
|
- **THEN** a prominent "Start Planning" button is visible that links to `/new`
|
||||||
|
|
||||||
|
#### Scenario: Session home link
|
||||||
|
- **WHEN** a user is in a planning session
|
||||||
|
- **THEN** the header contains a link back to the Planner home page
|
||||||
18
openspec/changes/app-navigation/tasks.md
Normal file
18
openspec/changes/app-navigation/tasks.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
## 1. Journal Navigation Bar
|
||||||
|
|
||||||
|
- [ ] 1.1 Add nav bar to Journal root Layout — show "Routes", "Activities" links when logged in; "Login", "Register" when logged out
|
||||||
|
- [ ] 1.2 Add user menu to nav bar — username/display name linking to profile, logout button
|
||||||
|
- [ ] 1.3 Highlight active nav item based on current route
|
||||||
|
- [ ] 1.4 Add i18n keys for nav labels (en + de): routes, activities, profile, logout
|
||||||
|
|
||||||
|
## 2. Planner Navigation
|
||||||
|
|
||||||
|
- [ ] 2.1 Add "Start Planning" CTA button on Planner home page linking to `/new`
|
||||||
|
- [ ] 2.2 Add home link (logo or text) to SessionView header
|
||||||
|
- [ ] 2.3 Add i18n keys for planner nav (en + de): startPlanning, home
|
||||||
|
|
||||||
|
## 3. Verify
|
||||||
|
|
||||||
|
- [ ] 3.1 Verify all Journal routes are reachable via navigation (routes, activities, profile, auth, privacy)
|
||||||
|
- [ ] 3.2 Verify Planner home → new session → back to home works
|
||||||
|
- [ ] 3.3 Update E2E tests if navigation changes affect existing selectors
|
||||||
2
openspec/changes/changelog/.openspec.yaml
Normal file
2
openspec/changes/changelog/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-25
|
||||||
65
openspec/changes/changelog/design.md
Normal file
65
openspec/changes/changelog/design.md
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
## Context
|
||||||
|
|
||||||
|
trails.cool ships features regularly but has no user-facing changelog. The
|
||||||
|
project philosophy values transparency and simplicity. We need a changelog
|
||||||
|
that's low-friction to write (just add a markdown file), works in-app, and
|
||||||
|
produces shareable social links.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Zero-friction authoring: drop a `.md` file, deploy, done
|
||||||
|
- Public page at `/changelog` listing all entries newest-first
|
||||||
|
- Individual entry pages at `/changelog/:slug` with OG meta for social sharing
|
||||||
|
- "What's New" indicator in nav for returning users
|
||||||
|
- Works without JavaScript (SSR)
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- CMS or admin interface for writing entries
|
||||||
|
- RSS feed (future, not now)
|
||||||
|
- Email notifications for new entries
|
||||||
|
- Per-entry images or rich media (just markdown text)
|
||||||
|
- Bilingual entries (English only for now)
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### D1: Markdown files in the repo, loaded at build time
|
||||||
|
|
||||||
|
Changelog entries live in `apps/journal/changelog/` as markdown files named
|
||||||
|
by date: `2026-03-25.md`. Vite's `import.meta.glob` loads them at build time.
|
||||||
|
No runtime file system access needed.
|
||||||
|
|
||||||
|
Each file has YAML frontmatter:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
title: "Collaborative route planning goes live"
|
||||||
|
date: 2026-03-25
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
### D2: Single route with dynamic slug
|
||||||
|
|
||||||
|
`/changelog` shows all entries. `/changelog/:date` shows a single entry.
|
||||||
|
Both are one route file using an optional param or two route files. The list
|
||||||
|
page shows titles + dates + first paragraph preview.
|
||||||
|
|
||||||
|
### D3: "What's New" via localStorage timestamp
|
||||||
|
|
||||||
|
Store `changelog:lastSeen` timestamp in localStorage. If the newest entry's
|
||||||
|
date is after this timestamp, show a dot on the nav "Changelog" link. Clicking
|
||||||
|
the changelog page updates the timestamp. No server state needed.
|
||||||
|
|
||||||
|
### D4: Open Graph meta for social sharing
|
||||||
|
|
||||||
|
Each `/changelog/:date` page sets `og:title`, `og:description` (first
|
||||||
|
paragraph), and `og:url`. No og:image for now — text previews are fine.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **Build-time loading means deploy to publish** → Acceptable. We deploy on
|
||||||
|
every merge to main. Adding a changelog entry is: write file, PR, merge,
|
||||||
|
deployed.
|
||||||
|
- **No rich media** → Keeps it simple. Can add images later if needed.
|
||||||
|
- **localStorage "what's new" doesn't sync across devices** → Fine for now.
|
||||||
|
Server-side tracking would require auth and a DB table — overkill.
|
||||||
38
openspec/changes/changelog/proposal.md
Normal file
38
openspec/changes/changelog/proposal.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
## Why
|
||||||
|
|
||||||
|
We ship features but nobody knows. There's no record of what changed, no way to
|
||||||
|
share progress, and no way for users to discover new features. A changelog
|
||||||
|
serves three audiences: existing users (what's new), potential users (the
|
||||||
|
project is alive and improving), and social followers (shareable updates).
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Add a `/changelog` page in the Journal showing dated entries with feature
|
||||||
|
descriptions
|
||||||
|
- Changelog entries stored as markdown files in the repo (not a database) —
|
||||||
|
low friction, version controlled, works with the PR workflow
|
||||||
|
- Each entry renders as a standalone shareable page (`/changelog/2026-03-25`)
|
||||||
|
with Open Graph meta tags for social previews
|
||||||
|
- A "What's New" badge/dot on the nav bar when there are entries the user
|
||||||
|
hasn't seen
|
||||||
|
- The changelog is public (no auth required) — it's part of the product's
|
||||||
|
public face
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `changelog`: Public changelog with dated entries, shareable individual pages with OG tags, "what's new" indicator
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
(None)
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Files**: New routes (`/changelog`, `/changelog/:slug`), markdown entry files
|
||||||
|
in `docs/changelog/` or `apps/journal/changelog/`, nav bar update
|
||||||
|
- **Dependencies**: A markdown renderer (could reuse `react-markdown` or render
|
||||||
|
at build time)
|
||||||
|
- **i18n**: Changelog entries written in English (primary audience), German
|
||||||
|
later if needed
|
||||||
37
openspec/changes/changelog/specs/changelog/spec.md
Normal file
37
openspec/changes/changelog/specs/changelog/spec.md
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Changelog listing page
|
||||||
|
The system SHALL provide a public `/changelog` page showing all entries newest-first.
|
||||||
|
|
||||||
|
#### Scenario: View changelog
|
||||||
|
- **WHEN** a user visits `/changelog`
|
||||||
|
- **THEN** all changelog entries are listed with date, title, and preview text
|
||||||
|
|
||||||
|
### Requirement: Individual entry pages
|
||||||
|
Each changelog entry SHALL have a shareable page at `/changelog/:date`.
|
||||||
|
|
||||||
|
#### Scenario: View single entry
|
||||||
|
- **WHEN** a user visits `/changelog/2026-03-25`
|
||||||
|
- **THEN** the full markdown content of that entry is rendered
|
||||||
|
|
||||||
|
#### Scenario: Social sharing meta
|
||||||
|
- **WHEN** a changelog entry page is loaded
|
||||||
|
- **THEN** Open Graph meta tags are set (og:title, og:description, og:url)
|
||||||
|
|
||||||
|
### Requirement: Markdown file authoring
|
||||||
|
Changelog entries SHALL be authored as markdown files with YAML frontmatter in the repository.
|
||||||
|
|
||||||
|
#### Scenario: Add new entry
|
||||||
|
- **WHEN** a developer adds a `.md` file to the changelog directory with date frontmatter
|
||||||
|
- **THEN** it appears on the changelog page after the next deploy
|
||||||
|
|
||||||
|
### Requirement: What's New indicator
|
||||||
|
The system SHALL show a visual indicator when there are changelog entries the user hasn't seen.
|
||||||
|
|
||||||
|
#### Scenario: New entry available
|
||||||
|
- **WHEN** a user visits the app and a changelog entry is newer than their last visit to /changelog
|
||||||
|
- **THEN** a dot or badge appears on the "Changelog" nav link
|
||||||
|
|
||||||
|
#### Scenario: Indicator dismissed
|
||||||
|
- **WHEN** a user visits the /changelog page
|
||||||
|
- **THEN** the indicator is dismissed (localStorage timestamp updated)
|
||||||
29
openspec/changes/changelog/tasks.md
Normal file
29
openspec/changes/changelog/tasks.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
## 1. Changelog Data Layer
|
||||||
|
|
||||||
|
- [ ] 1.1 Create `apps/journal/changelog/` directory with a sample entry (`2026-03-25.md`) covering the initial launch
|
||||||
|
- [ ] 1.2 Set up Vite `import.meta.glob` to load all `.md` files from the changelog directory with frontmatter parsing
|
||||||
|
- [ ] 1.3 Create `apps/journal/app/lib/changelog.server.ts` with functions: getAllEntries(), getEntry(date) returning parsed markdown + frontmatter
|
||||||
|
|
||||||
|
## 2. Routes
|
||||||
|
|
||||||
|
- [ ] 2.1 Add `/changelog` route showing all entries (date, title, preview) newest-first
|
||||||
|
- [ ] 2.2 Add `/changelog/:date` route rendering full markdown entry
|
||||||
|
- [ ] 2.3 Add Open Graph meta tags on entry pages (og:title, og:description, og:url)
|
||||||
|
- [ ] 2.4 Add both routes to `routes.ts`
|
||||||
|
|
||||||
|
## 3. What's New Indicator
|
||||||
|
|
||||||
|
- [ ] 3.1 Add "Changelog" link to Journal nav bar
|
||||||
|
- [ ] 3.2 Track `changelog:lastSeen` in localStorage, show dot when newest entry is newer
|
||||||
|
- [ ] 3.3 Clear indicator when user visits /changelog
|
||||||
|
|
||||||
|
## 4. Content
|
||||||
|
|
||||||
|
- [ ] 4.1 Write initial changelog entry for the launch (features shipped in Phase 1)
|
||||||
|
- [ ] 4.2 Add markdown rendering (react-markdown or built-in) for entry content
|
||||||
|
|
||||||
|
## 5. Verify
|
||||||
|
|
||||||
|
- [ ] 5.1 Verify /changelog lists entries correctly
|
||||||
|
- [ ] 5.2 Verify /changelog/:date renders full entry with OG tags
|
||||||
|
- [ ] 5.3 Verify "What's New" dot appears and dismisses
|
||||||
2
openspec/changes/komoot-import/.openspec.yaml
Normal file
2
openspec/changes/komoot-import/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-25
|
||||||
72
openspec/changes/komoot-import/design.md
Normal file
72
openspec/changes/komoot-import/design.md
Normal 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.
|
||||||
32
openspec/changes/komoot-import/proposal.md
Normal file
32
openspec/changes/komoot-import/proposal.md
Normal 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
|
||||||
63
openspec/changes/komoot-import/specs/komoot-import/spec.md
Normal file
63
openspec/changes/komoot-import/specs/komoot-import/spec.md
Normal 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
|
||||||
|
|
@ -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
|
||||||
51
openspec/changes/komoot-import/tasks.md
Normal file
51
openspec/changes/komoot-import/tasks.md
Normal 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
|
||||||
2
openspec/changes/planner-features/.openspec.yaml
Normal file
2
openspec/changes/planner-features/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-25
|
||||||
74
openspec/changes/planner-features/design.md
Normal file
74
openspec/changes/planner-features/design.md
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The Planner architecture defines noGoAreas, notes, and crash recovery in the
|
||||||
|
Yjs data model but none are implemented. The public instance also lacks rate
|
||||||
|
limiting — anyone can create unlimited sessions or hammer BRouter.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Draw no-go polygons on the map, synced via Yjs, sent to BRouter
|
||||||
|
- Shared notes (Y.Text) visible in sidebar for planning discussion
|
||||||
|
- localStorage backup of Yjs state, merged on reconnect
|
||||||
|
- Rate limits: 10 sessions/IP/hour, 60 BRouter calls/session/hour
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Complex polygon editing (holes, multi-polygon) — just simple polygons
|
||||||
|
- Rich text in notes (just plain text)
|
||||||
|
- Cross-tab Yjs sync via localStorage (handled by WebSocket)
|
||||||
|
- Rate limiting dashboard or admin UI
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### D1: No-go areas as Yjs Y.Array of polygon coordinates
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Yjs doc structure addition
|
||||||
|
noGoAreas: Y.Array<Y.Map<{
|
||||||
|
points: Array<{lat: number, lon: number}>,
|
||||||
|
name?: string
|
||||||
|
}>>
|
||||||
|
```
|
||||||
|
|
||||||
|
Each polygon is a closed ring of lat/lon points. BRouter accepts no-go areas
|
||||||
|
as a `nogo` parameter: `lon,lat,radius` for circles or polygon coordinates
|
||||||
|
depending on the BRouter version. We'll convert our polygons to BRouter's
|
||||||
|
expected format in the routing request.
|
||||||
|
|
||||||
|
### D2: Leaflet.draw for polygon drawing
|
||||||
|
|
||||||
|
Use `leaflet-draw` (or `@geoman-io/leaflet-geoman-free`) for drawing polygons
|
||||||
|
on the map. When a polygon is completed, add it to the Y.Array. Existing
|
||||||
|
polygons render as semi-transparent red overlays.
|
||||||
|
|
||||||
|
### D3: Notes as Y.Text in sidebar tab
|
||||||
|
|
||||||
|
Add a "Notes" tab to the sidebar (alongside waypoints). Uses Y.Text for
|
||||||
|
collaborative editing — simple textarea bound to Y.Text. Changes sync in
|
||||||
|
real-time. No markdown rendering — plain text only.
|
||||||
|
|
||||||
|
### D4: localStorage crash recovery
|
||||||
|
|
||||||
|
Every 10 seconds, save `Y.encodeStateAsUpdate(doc)` to
|
||||||
|
`localStorage['trails:session:${id}']`. On page load, if localStorage has
|
||||||
|
state for the current session, apply it as an update before connecting to
|
||||||
|
the server. Yjs CRDTs merge automatically — no conflicts.
|
||||||
|
|
||||||
|
Clear localStorage entry when session is closed or after successful sync.
|
||||||
|
|
||||||
|
### D5: In-memory rate limiter
|
||||||
|
|
||||||
|
Simple Map-based rate limiter in `server.ts`:
|
||||||
|
- Session creation: Track IP → count + window. Reject with 429 after 10/hour.
|
||||||
|
- BRouter proxy: Track session ID → count + window. Reject after 60/hour.
|
||||||
|
- No Redis needed — single server, in-memory is fine. Resets on restart
|
||||||
|
(acceptable).
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **leaflet-draw adds bundle size** → ~50KB gzipped. Acceptable for polygon
|
||||||
|
drawing functionality. Can lazy-load.
|
||||||
|
- **localStorage size limit** → ~5MB per origin. Yjs state is typically
|
||||||
|
10-100KB. No risk.
|
||||||
|
- **In-memory rate limiter resets on deploy** → Acceptable for current scale.
|
||||||
|
Redis-backed when needed.
|
||||||
40
openspec/changes/planner-features/proposal.md
Normal file
40
openspec/changes/planner-features/proposal.md
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The Planner's core editing works but is missing features from the architecture
|
||||||
|
doc: no-go areas for route avoidance, session notes for collaborative planning
|
||||||
|
communication, localStorage crash recovery for unsaved work, and rate limiting
|
||||||
|
to protect the BRouter API from abuse on the public instance.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **No-go areas**: Draw polygons on the map that BRouter avoids when computing
|
||||||
|
routes. Stored as Y.Array in the Yjs doc, synced across participants.
|
||||||
|
- **Session notes**: Shared text area (Y.Text) for participants to leave notes,
|
||||||
|
discuss the route, or plan logistics. Visible in the sidebar.
|
||||||
|
- **Crash recovery**: Periodically save Yjs state to localStorage. On
|
||||||
|
reconnect, merge local state with server state to recover unsaved changes.
|
||||||
|
- **Rate limiting**: Limit session creation per IP and BRouter API calls per
|
||||||
|
session to prevent abuse on the public instance.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `no-go-areas`: Draw avoidance polygons on the Planner map, passed to BRouter as routing constraints
|
||||||
|
- `session-notes`: Shared collaborative text in Planner sessions via Yjs Y.Text
|
||||||
|
- `crash-recovery`: localStorage backup of Yjs state for reconnection after browser crash
|
||||||
|
- `rate-limiting`: IP-based limits on session creation and route computation
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `planner-session`: Session data model gains noGoAreas and notes fields
|
||||||
|
- `brouter-integration`: Routing requests include no-go area polygons
|
||||||
|
- `map-display`: Map gains polygon drawing tools for no-go areas
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Planner Yjs doc**: New fields (noGoAreas: Y.Array, notes: Y.Text)
|
||||||
|
- **BRouter API**: No-go areas passed as `nogo` parameter
|
||||||
|
- **Map**: Leaflet.draw or custom polygon tool for no-go areas
|
||||||
|
- **Dependencies**: May need `leaflet-draw` for polygon editing
|
||||||
|
- **Server**: Rate limiting middleware (in-memory store)
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: BRouter routing with constraints
|
||||||
|
Route computation SHALL include no-go area polygons as avoidance constraints.
|
||||||
|
|
||||||
|
#### Scenario: Route with no-go areas
|
||||||
|
- **WHEN** the routing host computes a route and no-go areas exist
|
||||||
|
- **THEN** the BRouter request includes nogo parameters for each polygon
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: localStorage crash recovery
|
||||||
|
The Planner SHALL periodically save Yjs state to localStorage and recover it on reconnect.
|
||||||
|
|
||||||
|
#### Scenario: Browser crash recovery
|
||||||
|
- **WHEN** a user's browser crashes and they reopen the session
|
||||||
|
- **THEN** unsaved changes from localStorage are merged with the server state
|
||||||
|
|
||||||
|
#### Scenario: Clean exit
|
||||||
|
- **WHEN** a session syncs successfully
|
||||||
|
- **THEN** the localStorage backup is cleared
|
||||||
12
openspec/changes/planner-features/specs/map-display/spec.md
Normal file
12
openspec/changes/planner-features/specs/map-display/spec.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Map polygon drawing
|
||||||
|
The Planner map SHALL support drawing and displaying no-go area polygons.
|
||||||
|
|
||||||
|
#### Scenario: Polygon tool
|
||||||
|
- **WHEN** a user activates the no-go area tool
|
||||||
|
- **THEN** they can draw a polygon by clicking points on the map
|
||||||
|
|
||||||
|
#### Scenario: Polygon display
|
||||||
|
- **WHEN** no-go areas exist in the session
|
||||||
|
- **THEN** they are rendered as semi-transparent red polygons on the map
|
||||||
16
openspec/changes/planner-features/specs/no-go-areas/spec.md
Normal file
16
openspec/changes/planner-features/specs/no-go-areas/spec.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Draw no-go areas
|
||||||
|
Users SHALL be able to draw polygons on the map that BRouter avoids when computing routes.
|
||||||
|
|
||||||
|
#### Scenario: Draw polygon
|
||||||
|
- **WHEN** a user activates the no-go area tool and draws a polygon on the map
|
||||||
|
- **THEN** the polygon is added to the Yjs doc and visible to all participants
|
||||||
|
|
||||||
|
#### Scenario: Route avoids no-go area
|
||||||
|
- **WHEN** a route is computed and a no-go area intersects the direct path
|
||||||
|
- **THEN** BRouter routes around the no-go area
|
||||||
|
|
||||||
|
#### Scenario: Delete no-go area
|
||||||
|
- **WHEN** a user deletes a no-go area polygon
|
||||||
|
- **THEN** it is removed from the Yjs doc and the route is recomputed
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Planner session data model
|
||||||
|
The Yjs document SHALL include noGoAreas and notes fields alongside waypoints and routeData.
|
||||||
|
|
||||||
|
#### Scenario: Session with all fields
|
||||||
|
- **WHEN** a Planner session is active
|
||||||
|
- **THEN** the Yjs doc contains: waypoints (Y.Array), routeData (Y.Map), noGoAreas (Y.Array), notes (Y.Text)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Session creation rate limit
|
||||||
|
The Planner SHALL limit session creation to 10 per IP per hour.
|
||||||
|
|
||||||
|
#### Scenario: Rate limit exceeded
|
||||||
|
- **WHEN** an IP creates more than 10 sessions in one hour
|
||||||
|
- **THEN** the server responds with 429 Too Many Requests
|
||||||
|
|
||||||
|
### Requirement: BRouter call rate limit
|
||||||
|
The Planner SHALL limit route computations to 60 per session per hour.
|
||||||
|
|
||||||
|
#### Scenario: Routing rate limit exceeded
|
||||||
|
- **WHEN** a session exceeds 60 BRouter calls in one hour
|
||||||
|
- **THEN** the server responds with 429 and the client shows a "slow down" message
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Collaborative session notes
|
||||||
|
Planner sessions SHALL have a shared text area for participants to write notes.
|
||||||
|
|
||||||
|
#### Scenario: Write notes
|
||||||
|
- **WHEN** a user types in the notes area
|
||||||
|
- **THEN** the text syncs in real-time to all other participants via Yjs
|
||||||
|
|
||||||
|
#### Scenario: Notes persist
|
||||||
|
- **WHEN** a user leaves and rejoins a session
|
||||||
|
- **THEN** the notes are still there (stored in Yjs doc)
|
||||||
36
openspec/changes/planner-features/tasks.md
Normal file
36
openspec/changes/planner-features/tasks.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
## 1. No-Go Areas
|
||||||
|
|
||||||
|
- [ ] 1.1 Add `noGoAreas` Y.Array to Yjs doc in use-yjs.ts
|
||||||
|
- [ ] 1.2 Add leaflet-draw (or geoman) dependency for polygon drawing
|
||||||
|
- [ ] 1.3 Create NoGoAreaLayer component — renders polygons as red overlays, handles draw/delete
|
||||||
|
- [ ] 1.4 Add no-go area toolbar button to PlannerMap
|
||||||
|
- [ ] 1.5 Pass no-go areas to BRouter API as `nogo` parameters in brouter.ts
|
||||||
|
- [ ] 1.6 Trigger route recomputation when no-go areas change
|
||||||
|
|
||||||
|
## 2. Session Notes
|
||||||
|
|
||||||
|
- [ ] 2.1 Add `notes` Y.Text to Yjs doc in use-yjs.ts
|
||||||
|
- [ ] 2.2 Create NotesPanel component — textarea bound to Y.Text with real-time sync
|
||||||
|
- [ ] 2.3 Add "Notes" tab to sidebar (alongside waypoints)
|
||||||
|
- [ ] 2.4 Add i18n keys for notes UI (en + de)
|
||||||
|
|
||||||
|
## 3. Crash Recovery
|
||||||
|
|
||||||
|
- [ ] 3.1 Add periodic localStorage save (every 10s) of Yjs state in use-yjs.ts
|
||||||
|
- [ ] 3.2 On session reconnect, check localStorage for saved state and apply as Yjs update
|
||||||
|
- [ ] 3.3 Clear localStorage entry after successful sync or session close
|
||||||
|
- [ ] 3.4 Write unit test for save/restore logic
|
||||||
|
|
||||||
|
## 4. Rate Limiting
|
||||||
|
|
||||||
|
- [ ] 4.1 Create rate limiter utility in apps/planner/app/lib/rate-limit.ts (already exists — extend for IP tracking)
|
||||||
|
- [ ] 4.2 Add session creation rate limit (10/IP/hour) in the /new route or API
|
||||||
|
- [ ] 4.3 Add BRouter proxy rate limit (60/session/hour) in the route computation handler
|
||||||
|
- [ ] 4.4 Return 429 with Retry-After header and show user-friendly message on client
|
||||||
|
|
||||||
|
## 5. Verify
|
||||||
|
|
||||||
|
- [ ] 5.1 Test no-go areas: draw polygon, verify route avoids it, delete polygon
|
||||||
|
- [ ] 5.2 Test notes: type in two windows, verify real-time sync
|
||||||
|
- [ ] 5.3 Test crash recovery: make changes, kill browser, reopen — verify recovery
|
||||||
|
- [ ] 5.4 Test rate limiting: exceed limits, verify 429 response
|
||||||
2
openspec/changes/planner-landing-page/.openspec.yaml
Normal file
2
openspec/changes/planner-landing-page/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-25
|
||||||
44
openspec/changes/planner-landing-page/design.md
Normal file
44
openspec/changes/planner-landing-page/design.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The Planner home page is currently a centered heading ("trails.cool Planner")
|
||||||
|
and subtitle. It has no CTA, no explanation, and no way to start. Users must
|
||||||
|
know to visit `/new` to create a session.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Explain what the Planner does in 5 seconds of reading
|
||||||
|
- One-click session creation (no signup, no form)
|
||||||
|
- Feature highlights: collaborative, BRouter routing, elevation, GPX export
|
||||||
|
- Link to Journal for users who want accounts and saved routes
|
||||||
|
- i18n (English + German)
|
||||||
|
- Fast load, SSR-friendly, no heavy JS
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Marketing copy or elaborate graphics
|
||||||
|
- Pricing or plans
|
||||||
|
- User testimonials
|
||||||
|
- Interactive map demo on the landing page (too heavy)
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### D1: Single-page component in home.tsx
|
||||||
|
|
||||||
|
No new route — rewrite `apps/planner/app/routes/home.tsx`. The page is static
|
||||||
|
content + a CTA link to `/new`. No loader needed.
|
||||||
|
|
||||||
|
### D2: Feature cards with icons
|
||||||
|
|
||||||
|
Show 4-5 feature highlights as simple cards: collaborative editing, route
|
||||||
|
profiles, elevation profile, GPX export, no account needed. Use Tailwind —
|
||||||
|
no icon library, just emoji or simple SVG.
|
||||||
|
|
||||||
|
### D3: Link to Journal as secondary CTA
|
||||||
|
|
||||||
|
"Want to save your routes? Create a free account on trails.cool" — links to
|
||||||
|
the Journal. This bridges the two apps without requiring Journal for basic use.
|
||||||
|
|
||||||
|
### D4: Footer with legal and links
|
||||||
|
|
||||||
|
Privacy link (points to Journal's /privacy), GitHub repo, "Built with
|
||||||
|
BRouter and OpenStreetMap" attribution.
|
||||||
34
openspec/changes/planner-landing-page/proposal.md
Normal file
34
openspec/changes/planner-landing-page/proposal.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The Planner at planner.trails.cool should work as a standalone product — anyone
|
||||||
|
can visit, understand what it does, and start planning a route without needing
|
||||||
|
the Journal. Currently the home page is a blank heading with no explanation and
|
||||||
|
no way to start. Visitors bounce immediately.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Replace the Planner home page with a landing page that explains the tool:
|
||||||
|
collaborative route planning, BRouter routing, GPX export, no account needed
|
||||||
|
- Prominent "Start Planning" CTA that creates a new session
|
||||||
|
- Brief feature highlights (collaborative editing, routing profiles, elevation,
|
||||||
|
GPX export)
|
||||||
|
- Footer with links to trails.cool Journal, privacy page, GitHub repo
|
||||||
|
- The landing page should load fast — no heavy assets, SSR-friendly
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
(None — this replaces an existing page with useful content.)
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `planner-session`: Home page becomes a landing page with session creation CTA
|
||||||
|
- `map-display`: Landing page may show a decorative map or screenshot
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Files**: `apps/planner/app/routes/home.tsx` (complete rewrite), i18n keys
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **Design**: Needs to look good — this is the first impression for standalone
|
||||||
|
Planner users
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Planner home page
|
||||||
|
The Planner home page SHALL explain the tool's purpose and provide a one-click way to start planning.
|
||||||
|
|
||||||
|
#### Scenario: First-time visitor
|
||||||
|
- **WHEN** a user visits planner.trails.cool for the first time
|
||||||
|
- **THEN** they see a landing page explaining collaborative route planning, key features, and a prominent "Start Planning" button
|
||||||
|
|
||||||
|
#### Scenario: Start a session
|
||||||
|
- **WHEN** a user clicks "Start Planning"
|
||||||
|
- **THEN** a new anonymous session is created and the user is redirected to the session view
|
||||||
|
|
||||||
|
#### Scenario: Journal link
|
||||||
|
- **WHEN** a user wants to save routes permanently
|
||||||
|
- **THEN** a secondary CTA links to trails.cool for account creation
|
||||||
16
openspec/changes/planner-landing-page/tasks.md
Normal file
16
openspec/changes/planner-landing-page/tasks.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
## 1. Landing Page Content
|
||||||
|
|
||||||
|
- [ ] 1.1 Rewrite Planner home.tsx with hero section: heading, subtitle, "Start Planning" CTA linking to /new
|
||||||
|
- [ ] 1.2 Add feature highlights section (4-5 cards): collaborative editing, routing profiles, elevation profile, GPX export, no account needed
|
||||||
|
- [ ] 1.3 Add secondary CTA: "Save your routes on trails.cool" linking to Journal
|
||||||
|
- [ ] 1.4 Add footer: privacy link, GitHub repo, BRouter/OSM attribution
|
||||||
|
|
||||||
|
## 2. i18n
|
||||||
|
|
||||||
|
- [ ] 2.1 Add all landing page strings to en.ts and de.ts (hero, features, CTAs, footer)
|
||||||
|
|
||||||
|
## 3. Verify
|
||||||
|
|
||||||
|
- [ ] 3.1 Verify landing page renders correctly at planner.trails.cool
|
||||||
|
- [ ] 3.2 Verify "Start Planning" creates a session and redirects
|
||||||
|
- [ ] 3.3 Update E2E planner test if home page assertions changed
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-25
|
||||||
46
openspec/changes/planner-multiplayer-awareness/design.md
Normal file
46
openspec/changes/planner-multiplayer-awareness/design.md
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Yjs awareness already tracks users with `{ color, name }` state and renders
|
||||||
|
map cursors via `CursorTracker` in `PlannerMap.tsx`. The current implementation
|
||||||
|
has issues: random names can't be changed, cursor labels are unstyled, and
|
||||||
|
there's no participant list showing who's in the session.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- See all participants in the session (name, color, host/participant role)
|
||||||
|
- Edit your own name (saved to localStorage, synced via awareness)
|
||||||
|
- Polished cursor rendering on the map
|
||||||
|
- Visual feedback when someone joins or leaves
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Chat or messaging between participants
|
||||||
|
- Cursor position sharing outside the map (e.g., in the sidebar)
|
||||||
|
- Participant permissions or kicking (admin-only debug feature already exists)
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### D1: Participant list in session header
|
||||||
|
|
||||||
|
Show colored dots + names in the header bar (already has space between profile
|
||||||
|
selector and connection status). Clicking your own name opens an inline edit.
|
||||||
|
Keeps the sidebar free for waypoints.
|
||||||
|
|
||||||
|
### D2: Name persisted in localStorage, synced via awareness
|
||||||
|
|
||||||
|
Current approach: random name from a list, stored in localStorage as
|
||||||
|
`trails:user`. Enhancement: let users edit it. On change, update localStorage
|
||||||
|
AND call `awareness.setLocalStateField("user", { color, name })`. All other
|
||||||
|
clients see the update immediately.
|
||||||
|
|
||||||
|
### D3: Cursor as SVG pointer with name tag
|
||||||
|
|
||||||
|
Replace the current `divIcon` cursor label with: a small SVG arrow in the
|
||||||
|
user's color + a name tag with background, shadow, and rounded corners.
|
||||||
|
Position offset so the pointer tip is at the actual lat/lng.
|
||||||
|
|
||||||
|
### D4: Join/leave toasts
|
||||||
|
|
||||||
|
When awareness detects a new client or a client leaving, show a brief toast
|
||||||
|
("Alice joined" / "Bob left") that auto-dismisses after 3 seconds. Use a
|
||||||
|
simple absolute-positioned div, no toast library.
|
||||||
35
openspec/changes/planner-multiplayer-awareness/proposal.md
Normal file
35
openspec/changes/planner-multiplayer-awareness/proposal.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
## Why
|
||||||
|
|
||||||
|
The Planner supports collaborative editing but the multiplayer experience is
|
||||||
|
bare. Users can't see who else is in the session. Cursor labels are small
|
||||||
|
floating divs that look broken (no proper styling, overlap with map controls).
|
||||||
|
Users get randomly assigned names like "Hiker" or "Explorer" with no way to
|
||||||
|
change them. This makes collaboration confusing — you can't tell who is who.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **Participant list**: Show who's in the session (name, color, host badge)
|
||||||
|
in the sidebar or header
|
||||||
|
- **Name editing**: Let users set their name after joining (persisted in
|
||||||
|
localStorage, synced via Yjs awareness)
|
||||||
|
- **Cursor styling**: Fix cursor labels — proper pointer icon, name tag with
|
||||||
|
shadow, avoid overlap with map controls
|
||||||
|
- **Connection indicators**: Show when participants join/leave
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
(None — this enhances existing Yjs awareness features.)
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `planner-session`: Session view gains participant list and name editing
|
||||||
|
- `map-display`: Map cursor rendering improved
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Files**: `PlannerMap.tsx` (cursor styling), `SessionView.tsx` (participant
|
||||||
|
list), `use-yjs.ts` (name editing), new `ParticipantList` component
|
||||||
|
- **Dependencies**: None
|
||||||
|
- **i18n**: New keys for participant labels and name editing
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Map cursor rendering
|
||||||
|
Other participants' cursors on the map SHALL be clearly visible with proper styling.
|
||||||
|
|
||||||
|
#### Scenario: Cursor appearance
|
||||||
|
- **WHEN** another participant moves their mouse on the map
|
||||||
|
- **THEN** a colored pointer icon with their name tag appears at the cursor position
|
||||||
|
|
||||||
|
#### Scenario: Cursor does not obscure map controls
|
||||||
|
- **WHEN** cursors are rendered
|
||||||
|
- **THEN** they appear below map controls (zoom, layer switcher) in z-index
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Session participant awareness
|
||||||
|
Users in a planning session SHALL see who else is present and be able to identify themselves.
|
||||||
|
|
||||||
|
#### Scenario: Participant list visible
|
||||||
|
- **WHEN** multiple users are in a session
|
||||||
|
- **THEN** the header shows each participant's name and color
|
||||||
|
|
||||||
|
#### Scenario: Host badge
|
||||||
|
- **WHEN** a participant is the routing host
|
||||||
|
- **THEN** their entry in the participant list shows a host indicator
|
||||||
|
|
||||||
|
#### Scenario: Edit own name
|
||||||
|
- **WHEN** a user clicks their own name in the participant list
|
||||||
|
- **THEN** an inline text input appears to change their display name
|
||||||
|
|
||||||
|
#### Scenario: Name persisted
|
||||||
|
- **WHEN** a user changes their name
|
||||||
|
- **THEN** the name is saved to localStorage and immediately visible to all other participants via awareness
|
||||||
|
|
||||||
|
#### Scenario: Join notification
|
||||||
|
- **WHEN** a new participant joins the session
|
||||||
|
- **THEN** a brief toast shows "[name] joined"
|
||||||
|
|
||||||
|
#### Scenario: Leave notification
|
||||||
|
- **WHEN** a participant leaves the session
|
||||||
|
- **THEN** a brief toast shows "[name] left"
|
||||||
27
openspec/changes/planner-multiplayer-awareness/tasks.md
Normal file
27
openspec/changes/planner-multiplayer-awareness/tasks.md
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
## 1. Name Editing
|
||||||
|
|
||||||
|
- [ ] 1.1 Update use-yjs.ts: allow updating user name via a returned setter function
|
||||||
|
- [ ] 1.2 Save updated name to localStorage and sync via awareness.setLocalStateField
|
||||||
|
- [ ] 1.3 Add i18n keys for participant UI (en + de)
|
||||||
|
|
||||||
|
## 2. Participant List
|
||||||
|
|
||||||
|
- [ ] 2.1 Create ParticipantList component showing all awareness states (name, color dot, host badge)
|
||||||
|
- [ ] 2.2 Add inline name edit when clicking own name in the list
|
||||||
|
- [ ] 2.3 Integrate ParticipantList into SessionView header
|
||||||
|
|
||||||
|
## 3. Cursor Styling
|
||||||
|
|
||||||
|
- [ ] 3.1 Replace current divIcon cursor with SVG pointer arrow + styled name tag (background, shadow, rounded)
|
||||||
|
- [ ] 3.2 Set cursor z-index below map controls to avoid overlap
|
||||||
|
|
||||||
|
## 4. Join/Leave Toasts
|
||||||
|
|
||||||
|
- [ ] 4.1 Track awareness changes (added/removed clients) in SessionView
|
||||||
|
- [ ] 4.2 Show auto-dismissing toast when participant joins or leaves (3 second duration)
|
||||||
|
|
||||||
|
## 5. Verify
|
||||||
|
|
||||||
|
- [ ] 5.1 Test with two browser windows: verify participant list, name editing, cursor rendering
|
||||||
|
- [ ] 5.2 Verify join/leave toasts appear correctly
|
||||||
|
- [ ] 5.3 Verify name persists across page refresh (localStorage)
|
||||||
2
openspec/changes/route-features/.openspec.yaml
Normal file
2
openspec/changes/route-features/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-25
|
||||||
71
openspec/changes/route-features/design.md
Normal file
71
openspec/changes/route-features/design.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The architecture doc defines a rich route model with permissions, multi-day
|
||||||
|
support, spatial queries, and photos. Currently routes are owner-only, flat
|
||||||
|
(no days), and have no photos or spatial search. This change adds the route
|
||||||
|
and activity features needed before federation makes them visible to others.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Route visibility (private/public) and sharing with specific users
|
||||||
|
- Fork public routes
|
||||||
|
- Multi-day route support with day-break waypoints
|
||||||
|
- Map-based route discovery via PostGIS
|
||||||
|
- Photo attachments on activities
|
||||||
|
- Contributor tracking on route versions
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Federation of routes (separate change)
|
||||||
|
- Real-time collaborative permission changes
|
||||||
|
- Photo editing or filters
|
||||||
|
- Route recommendations based on preferences
|
||||||
|
- Activity collections (multi-day trip grouping — future)
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### D1: Visibility as enum column on routes and activities
|
||||||
|
|
||||||
|
Add `visibility` column (`public`, `private`, default `private`) to both
|
||||||
|
routes and activities. `followers_only` added later when following exists.
|
||||||
|
Public routes appear in spatial search. Private routes are owner-only.
|
||||||
|
|
||||||
|
### D2: Route shares table for explicit sharing
|
||||||
|
|
||||||
|
```
|
||||||
|
journal.route_shares (routeId, userId, permission: 'view' | 'edit')
|
||||||
|
```
|
||||||
|
|
||||||
|
Owner can share with specific users. Shared users see the route in their
|
||||||
|
"Shared with me" section. Edit permission allows starting Planner sessions.
|
||||||
|
|
||||||
|
### D3: Day breaks as waypoint property
|
||||||
|
|
||||||
|
The Planner's Yjs waypoint Y.Map gets an optional `isDayBreak: true` field.
|
||||||
|
The sidebar and elevation chart show day boundaries. GPX export uses one
|
||||||
|
track segment per day. The Journal stores day-break indices in route metadata.
|
||||||
|
|
||||||
|
### D4: PostGIS spatial search
|
||||||
|
|
||||||
|
Routes already store geometry as PostGIS LineString. Add a `/routes/explore`
|
||||||
|
page with a map — as the user pans/zooms, query public routes within the
|
||||||
|
bounding box using `ST_Intersects`. Show route previews on the map.
|
||||||
|
|
||||||
|
### D5: Photo storage via S3 (Garage)
|
||||||
|
|
||||||
|
Use the Garage S3-compatible storage already in the architecture. Upload flow:
|
||||||
|
server generates presigned PUT URL → client uploads directly to S3 → server
|
||||||
|
stores the S3 key in a `journal.activity_photos` table. Serve via presigned
|
||||||
|
GET URLs or a CDN path.
|
||||||
|
|
||||||
|
### D6: Contributor tracking via array column
|
||||||
|
|
||||||
|
Add `contributors` text array to `journal.route_versions`. When a Planner
|
||||||
|
session saves back via callback, the JWT identifies the actor. The version
|
||||||
|
records the contributor's user ID (or ActivityPub URI for future federation).
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **S3 setup required** → Garage needs to be enabled in docker-compose (currently commented out). Adds operational complexity.
|
||||||
|
- **Spatial search performance** → PostGIS bounding box queries are fast with spatial indexes. Already have the geometry column.
|
||||||
|
- **Multi-day in Planner requires Yjs changes** → Adding `isDayBreak` to waypoints is backward-compatible (optional field). Existing sessions unaffected.
|
||||||
43
openspec/changes/route-features/proposal.md
Normal file
43
openspec/changes/route-features/proposal.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Routes are currently flat — no permissions beyond owner, no way to share with
|
||||||
|
specific people, no forking, no multi-day support, no spatial discovery, and
|
||||||
|
no contributor tracking. The architecture doc specifies all of these for Phase
|
||||||
|
2. Without them, routes are just personal GPX storage with no social or
|
||||||
|
collaborative dimension.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **Route sharing permissions**: Private/public/shared visibility levels with
|
||||||
|
a view/edit permission matrix
|
||||||
|
- **Route forking**: Copy someone else's public route to your own collection
|
||||||
|
- **Multi-day routes**: Day-break waypoints that split a route into stages with
|
||||||
|
per-day stats and GPX segments
|
||||||
|
- **Spatial search**: "Routes near me" or "routes in this area" using PostGIS
|
||||||
|
- **Contributor tracking**: Record who contributed to each route version
|
||||||
|
- **Activity visibility levels**: Public/followers-only/private on activities
|
||||||
|
- **Photo attachments**: Upload photos to activities (requires S3/Garage setup)
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `route-sharing`: Permission model (private/public/shared), view/edit access, route forking
|
||||||
|
- `spatial-search`: Map-based route discovery using PostGIS bounding box and proximity queries
|
||||||
|
- `multi-day-routes`: Day-break markers on waypoints, per-day stats, multi-segment GPX export
|
||||||
|
- `activity-photos`: Photo upload and display on activities via S3-compatible storage
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `route-management`: Add visibility, contributor tracking, forking
|
||||||
|
- `activity-feed`: Add visibility levels (public/followers-only/private)
|
||||||
|
- `planner-session`: Support day-break waypoint markers
|
||||||
|
- `infrastructure`: S3/Garage setup for photo storage
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Database**: New columns (visibility, contributors) on routes and activities, new route_shares table, photo storage metadata
|
||||||
|
- **Storage**: S3-compatible object storage (Garage) for photos
|
||||||
|
- **Files**: Route detail page, activity pages, search page, Planner waypoint UI, sharing UI
|
||||||
|
- **Dependencies**: S3 client library for photo uploads
|
||||||
|
- **Privacy**: Photo storage, route visibility controls documented in manifest
|
||||||
12
openspec/changes/route-features/specs/activity-feed/spec.md
Normal file
12
openspec/changes/route-features/specs/activity-feed/spec.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Activity visibility
|
||||||
|
Activities SHALL have visibility levels controlling who can see them.
|
||||||
|
|
||||||
|
#### Scenario: Private activity
|
||||||
|
- **WHEN** an activity's visibility is "private"
|
||||||
|
- **THEN** only the owner can view it
|
||||||
|
|
||||||
|
#### Scenario: Public activity
|
||||||
|
- **WHEN** an activity's visibility is "public"
|
||||||
|
- **THEN** anyone can view it on the owner's profile
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Photo attachments on activities
|
||||||
|
Users SHALL be able to upload photos to their activities.
|
||||||
|
|
||||||
|
#### Scenario: Upload photo
|
||||||
|
- **WHEN** a user adds a photo to an activity
|
||||||
|
- **THEN** the photo is uploaded to S3 storage and linked to the activity
|
||||||
|
|
||||||
|
#### Scenario: View photos
|
||||||
|
- **WHEN** a user views an activity with photos
|
||||||
|
- **THEN** the photos are displayed in a gallery on the activity page
|
||||||
|
|
||||||
|
#### Scenario: Delete photo
|
||||||
|
- **WHEN** a user deletes a photo from their activity
|
||||||
|
- **THEN** the photo is removed from storage and unlinked
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Day-break waypoints
|
||||||
|
Waypoints in the Planner SHALL support a day-break marker that splits the route into stages.
|
||||||
|
|
||||||
|
#### Scenario: Mark day break
|
||||||
|
- **WHEN** a user marks a waypoint as a day break in the Planner
|
||||||
|
- **THEN** the route is visually split into days at that point
|
||||||
|
|
||||||
|
#### Scenario: Per-day statistics
|
||||||
|
- **WHEN** a route has day-break markers
|
||||||
|
- **THEN** the sidebar shows distance and elevation per day/stage
|
||||||
|
|
||||||
|
#### Scenario: GPX export with day segments
|
||||||
|
- **WHEN** a multi-day route is exported as GPX
|
||||||
|
- **THEN** each day is a separate track segment in the GPX file
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Route metadata
|
||||||
|
Routes SHALL track visibility, contributors, and support forking.
|
||||||
|
|
||||||
|
#### Scenario: Visibility on route creation
|
||||||
|
- **WHEN** a user creates a route
|
||||||
|
- **THEN** the route defaults to "private" visibility
|
||||||
|
|
||||||
|
#### Scenario: Contributor recorded on version
|
||||||
|
- **WHEN** a Planner session saves a new route version via callback
|
||||||
|
- **THEN** the version records the contributor who made the edit
|
||||||
30
openspec/changes/route-features/specs/route-sharing/spec.md
Normal file
30
openspec/changes/route-features/specs/route-sharing/spec.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Route visibility levels
|
||||||
|
Routes SHALL have a visibility setting controlling who can see them.
|
||||||
|
|
||||||
|
#### Scenario: Private route
|
||||||
|
- **WHEN** a route's visibility is "private"
|
||||||
|
- **THEN** only the owner can view it
|
||||||
|
|
||||||
|
#### Scenario: Public route
|
||||||
|
- **WHEN** a route's visibility is "public"
|
||||||
|
- **THEN** anyone can view it and export its GPX
|
||||||
|
|
||||||
|
### Requirement: Share routes with specific users
|
||||||
|
Route owners SHALL be able to share routes with specific users at view or edit permission levels.
|
||||||
|
|
||||||
|
#### Scenario: Share with view access
|
||||||
|
- **WHEN** owner shares a route with another user as "view"
|
||||||
|
- **THEN** that user can see the route and export GPX but cannot edit
|
||||||
|
|
||||||
|
#### Scenario: Share with edit access
|
||||||
|
- **WHEN** owner shares a route with another user as "edit"
|
||||||
|
- **THEN** that user can start Planner sessions and create new versions
|
||||||
|
|
||||||
|
### Requirement: Fork routes
|
||||||
|
Users SHALL be able to fork (copy) public routes to their own collection.
|
||||||
|
|
||||||
|
#### Scenario: Fork a public route
|
||||||
|
- **WHEN** a user clicks "Fork" on a public route
|
||||||
|
- **THEN** a copy is created in their collection with the original credited
|
||||||
12
openspec/changes/route-features/specs/spatial-search/spec.md
Normal file
12
openspec/changes/route-features/specs/spatial-search/spec.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Map-based route discovery
|
||||||
|
Users SHALL be able to discover public routes by browsing a map.
|
||||||
|
|
||||||
|
#### Scenario: Browse routes on map
|
||||||
|
- **WHEN** a user visits the route explore page and pans/zooms the map
|
||||||
|
- **THEN** public routes within the visible area are shown as polylines on the map
|
||||||
|
|
||||||
|
#### Scenario: Route preview
|
||||||
|
- **WHEN** a user clicks a route on the explore map
|
||||||
|
- **THEN** a popup or sidebar shows the route name, distance, elevation, and a link to the detail page
|
||||||
68
openspec/changes/route-features/tasks.md
Normal file
68
openspec/changes/route-features/tasks.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
## 1. Route Visibility & Sharing Schema
|
||||||
|
|
||||||
|
- [ ] 1.1 Add `visibility` enum column (private, public) to `journal.routes`, default private
|
||||||
|
- [ ] 1.2 Add `visibility` enum column (private, public) to `journal.activities`, default private
|
||||||
|
- [ ] 1.3 Create `journal.route_shares` table (routeId, userId, permission: view/edit)
|
||||||
|
- [ ] 1.4 Add `contributors` text array column to `journal.route_versions`
|
||||||
|
- [ ] 1.5 Add `forkedFromId` nullable column to `journal.routes`
|
||||||
|
- [ ] 1.6 Push schema and verify locally
|
||||||
|
|
||||||
|
## 2. Route Permissions Logic
|
||||||
|
|
||||||
|
- [ ] 2.1 Create `apps/journal/app/lib/permissions.server.ts` with canView(routeId, userId), canEdit(routeId, userId) functions
|
||||||
|
- [ ] 2.2 Update route detail loader to check visibility/permissions (show 404 for unauthorized)
|
||||||
|
- [ ] 2.3 Update route list to only show own + shared routes
|
||||||
|
- [ ] 2.4 Add visibility toggle (private/public) to route edit page
|
||||||
|
- [ ] 2.5 Add share dialog — search users, set view/edit permission
|
||||||
|
|
||||||
|
## 3. Route Forking
|
||||||
|
|
||||||
|
- [ ] 3.1 Add "Fork" button on public route detail pages (visible to logged-in users who aren't the owner)
|
||||||
|
- [ ] 3.2 Create fork API route — copies route + latest GPX to user's collection, sets forkedFromId
|
||||||
|
- [ ] 3.3 Show "Forked from [original]" link on forked routes
|
||||||
|
|
||||||
|
## 4. Spatial Search
|
||||||
|
|
||||||
|
- [ ] 4.1 Ensure PostGIS spatial index exists on routes.geometry column
|
||||||
|
- [ ] 4.2 Create `/routes/explore` route with a full-page map
|
||||||
|
- [ ] 4.3 Add API route to query public routes within bounding box (ST_Intersects)
|
||||||
|
- [ ] 4.4 Render matching routes as polylines on the explore map
|
||||||
|
- [ ] 4.5 Add route popup/sidebar with name, stats, link to detail
|
||||||
|
|
||||||
|
## 5. Multi-Day Routes
|
||||||
|
|
||||||
|
- [ ] 5.1 Add `isDayBreak` optional boolean support to Planner waypoint Y.Map
|
||||||
|
- [ ] 5.2 Add day-break toggle in Planner waypoint sidebar (click to mark/unmark)
|
||||||
|
- [ ] 5.3 Show day boundaries in elevation chart
|
||||||
|
- [ ] 5.4 Compute per-day distance and elevation in sidebar
|
||||||
|
- [ ] 5.5 Update GPX export to use one track segment per day
|
||||||
|
- [ ] 5.6 Store dayBreaks array in route metadata on save
|
||||||
|
|
||||||
|
## 6. Activity Photos
|
||||||
|
|
||||||
|
- [ ] 6.1 Enable Garage in docker-compose.yml, create bucket
|
||||||
|
- [ ] 6.2 Create S3 client utility (`apps/journal/app/lib/storage.server.ts`) with presigned URL generation
|
||||||
|
- [ ] 6.3 Create `journal.activity_photos` table (id, activityId, s3Key, altText, createdAt)
|
||||||
|
- [ ] 6.4 Add photo upload UI on activity detail/edit page
|
||||||
|
- [ ] 6.5 Display photo gallery on activity detail page
|
||||||
|
- [ ] 6.6 Add delete photo functionality
|
||||||
|
|
||||||
|
## 7. Contributor Tracking
|
||||||
|
|
||||||
|
- [ ] 7.1 Update Planner→Journal callback to include contributor info from JWT
|
||||||
|
- [ ] 7.2 Store contributor in route_versions on save
|
||||||
|
- [ ] 7.3 Display contributors on route detail page
|
||||||
|
|
||||||
|
## 8. Privacy & i18n
|
||||||
|
|
||||||
|
- [ ] 8.1 Update /privacy page: document photo storage, route visibility, sharing
|
||||||
|
- [ ] 8.2 Add i18n keys for all new UI strings (en + de)
|
||||||
|
|
||||||
|
## 9. Verify
|
||||||
|
|
||||||
|
- [ ] 9.1 Test route visibility: create private and public routes, verify access control
|
||||||
|
- [ ] 9.2 Test sharing: share route with another user, verify view/edit access
|
||||||
|
- [ ] 9.3 Test forking: fork a public route, verify copy created
|
||||||
|
- [ ] 9.4 Test spatial search: create public routes, verify they appear on explore map
|
||||||
|
- [ ] 9.5 Test multi-day: mark day breaks, verify per-day stats and GPX export
|
||||||
|
- [ ] 9.6 Test photos: upload, view, delete photos on activities
|
||||||
2
openspec/changes/transactional-emails/.openspec.yaml
Normal file
2
openspec/changes/transactional-emails/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
schema: spec-driven
|
||||||
|
created: 2026-03-25
|
||||||
62
openspec/changes/transactional-emails/design.md
Normal file
62
openspec/changes/transactional-emails/design.md
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The Journal has passkey auth (primary) and magic link login (fallback). Magic
|
||||||
|
links work in dev (auto-redirect) but in production the link is only logged
|
||||||
|
to console — users never receive it. No email infrastructure exists.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
- Actually deliver magic link emails in production
|
||||||
|
- Send a welcome email after registration
|
||||||
|
- Provider-agnostic email interface (easy to swap providers)
|
||||||
|
- HTML templates with plain-text fallback
|
||||||
|
- Easy to add new email types in the future (just add a template + call)
|
||||||
|
- Dev mode: still log to console / return devLink (no real emails)
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
- Marketing emails or newsletters
|
||||||
|
- Email preferences / unsubscribe (only transactional)
|
||||||
|
- Inline CSS / complex email layouts (keep it simple text-focused)
|
||||||
|
- Queuing or retry logic (provider handles delivery)
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### D1: Resend as email provider
|
||||||
|
|
||||||
|
Use [Resend](https://resend.com) — simple API, generous free tier (3k
|
||||||
|
emails/month), good developer experience. Single dependency: `resend` npm
|
||||||
|
package.
|
||||||
|
|
||||||
|
**Alternative considered**: Nodemailer + SMTP. More complex setup, requires
|
||||||
|
SMTP server. Resend is simpler for a small project.
|
||||||
|
|
||||||
|
**Alternative considered**: Postmark. Similar to Resend but less ergonomic API.
|
||||||
|
|
||||||
|
### D2: Email module in apps/journal/app/lib/email.server.ts
|
||||||
|
|
||||||
|
Not a shared package — only the Journal sends emails. Keep it simple:
|
||||||
|
- `sendEmail(to, subject, html, text)` — low-level sender
|
||||||
|
- `sendMagicLink(email, link)` — template wrapper
|
||||||
|
- `sendWelcome(email, username)` — template wrapper
|
||||||
|
|
||||||
|
In dev mode, `sendEmail` logs to console instead of calling Resend.
|
||||||
|
|
||||||
|
### D3: Inline HTML templates
|
||||||
|
|
||||||
|
Simple functions returning HTML strings. No template engine. Each email type
|
||||||
|
is a function: `magicLinkTemplate(link)`, `welcomeTemplate(username)`. Plain
|
||||||
|
text generated by stripping tags.
|
||||||
|
|
||||||
|
### D4: Sender domain: noreply@trails.cool
|
||||||
|
|
||||||
|
Requires DNS verification (DKIM/SPF records) with Resend. Add TXT records via
|
||||||
|
Terraform or manually in Hetzner DNS.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **Resend vendor lock-in** → Low risk. The `sendEmail` wrapper abstracts it.
|
||||||
|
Swapping to nodemailer later is a one-file change.
|
||||||
|
- **DNS verification required** → Must add DKIM/SPF records before emails work.
|
||||||
|
Can use Resend's test domain initially.
|
||||||
|
- **Free tier limits** → 3k/month is plenty for <100 users.
|
||||||
32
openspec/changes/transactional-emails/proposal.md
Normal file
32
openspec/changes/transactional-emails/proposal.md
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
## Why
|
||||||
|
|
||||||
|
Magic link login doesn't work in production — the link is logged to console
|
||||||
|
but never emailed to the user. Registration works (passkey-based) but the
|
||||||
|
fallback auth flow is broken. We need actual email delivery.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- Add a reusable email sending layer (`@trails-cool/email` or in-app module)
|
||||||
|
with a provider-agnostic interface so future emails are trivial to add
|
||||||
|
- Send magic link emails in production
|
||||||
|
- Send a welcome email after registration
|
||||||
|
- Use a transactional email provider (Resend, Postmark, or SMTP)
|
||||||
|
- HTML email templates with plain-text fallback
|
||||||
|
- Document email sending in the privacy manifest
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `transactional-emails`: Send templated emails via a provider-agnostic interface. Current emails: magic link login, welcome on registration. Designed for easy addition of future email types.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
- `journal-auth`: Magic link flow now actually sends the email instead of logging to console
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
- **Files**: New email module (lib or package), modified `api.auth.login.ts`, modified registration flow, email templates
|
||||||
|
- **Dependencies**: One email provider SDK (e.g., `resend` or `nodemailer`)
|
||||||
|
- **Infrastructure**: Email provider API key as env var, DNS records for sender domain verification
|
||||||
|
- **Privacy**: Email addresses sent to third-party email provider — must document
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: Magic link login
|
||||||
|
The magic link login flow SHALL deliver the link via email in production instead of logging to console.
|
||||||
|
|
||||||
|
#### Scenario: Production email delivery
|
||||||
|
- **WHEN** a user requests a magic link in production
|
||||||
|
- **THEN** the link is emailed to the user (not just logged to console)
|
||||||
|
|
||||||
|
#### Scenario: Dev mode unchanged
|
||||||
|
- **WHEN** a user requests a magic link in development
|
||||||
|
- **THEN** the link is returned directly for auto-redirect (existing behavior preserved)
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Email sending interface
|
||||||
|
The system SHALL provide a provider-agnostic email sending function that supports HTML and plain-text content.
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
|
||||||
|
#### Scenario: Dev mode skips sending
|
||||||
|
- **WHEN** the system sends a transactional email in development
|
||||||
|
- **THEN** the email content is logged to console and no external API is called
|
||||||
|
|
||||||
|
### Requirement: Magic link email
|
||||||
|
The system SHALL send an email containing the magic link when a user requests passwordless login.
|
||||||
|
|
||||||
|
#### Scenario: Magic link delivered
|
||||||
|
- **WHEN** a user requests a magic link login
|
||||||
|
- **THEN** an email is sent with a clickable link that logs them in
|
||||||
|
|
||||||
|
#### Scenario: Email content
|
||||||
|
- **WHEN** a magic link email is sent
|
||||||
|
- **THEN** it includes: the link, expiry time (15 minutes), and plain-text fallback
|
||||||
|
|
||||||
|
### Requirement: Welcome email
|
||||||
|
The system SHALL send a welcome email after successful registration.
|
||||||
|
|
||||||
|
#### Scenario: Welcome on registration
|
||||||
|
- **WHEN** a user completes passkey registration
|
||||||
|
- **THEN** a welcome email is sent to their registered email address
|
||||||
|
|
||||||
|
### Requirement: Email templates
|
||||||
|
Each email type SHALL have an HTML template with a plain-text fallback.
|
||||||
|
|
||||||
|
#### Scenario: Extensible template pattern
|
||||||
|
- **WHEN** a new email type is needed in the future
|
||||||
|
- **THEN** it can be added by creating a template function and calling sendEmail
|
||||||
|
|
||||||
|
### Requirement: Privacy disclosure
|
||||||
|
Email sending SHALL be documented in the privacy manifest.
|
||||||
|
|
||||||
|
#### Scenario: Privacy manifest updated
|
||||||
|
- **WHEN** transactional emails are enabled
|
||||||
|
- **THEN** the /privacy page documents: what emails are sent, that email addresses are shared with the email provider, and which provider is used
|
||||||
30
openspec/changes/transactional-emails/tasks.md
Normal file
30
openspec/changes/transactional-emails/tasks.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
## 1. Email Infrastructure
|
||||||
|
|
||||||
|
- [ ] 1.1 Add `resend` package to Journal dependencies
|
||||||
|
- [ ] 1.2 Create `apps/journal/app/lib/email.server.ts` with sendEmail(to, subject, html, text) — uses Resend in production, logs to console in dev
|
||||||
|
- [ ] 1.3 Add `RESEND_API_KEY` env var to docker-compose.yml
|
||||||
|
- [ ] 1.4 Write unit test for sendEmail (mock Resend, verify dev-mode logging)
|
||||||
|
|
||||||
|
## 2. Email Templates
|
||||||
|
|
||||||
|
- [ ] 2.1 Create magicLinkTemplate(link: string) returning { html, text } — includes link, 15-min expiry note, trails.cool branding
|
||||||
|
- [ ] 2.2 Create welcomeTemplate(username: string) returning { html, text } — greeting, what they can do, link to routes
|
||||||
|
- [ ] 2.3 Wire sendMagicLink(email, link) and sendWelcome(email, username) helper functions
|
||||||
|
|
||||||
|
## 3. Integration
|
||||||
|
|
||||||
|
- [ ] 3.1 Update api.auth.login.ts: call sendMagicLink in production instead of just logging
|
||||||
|
- [ ] 3.2 Update registration flow: call sendWelcome after successful passkey registration
|
||||||
|
- [ ] 3.3 Verify dev mode still works (devLink returned, no email sent)
|
||||||
|
|
||||||
|
## 4. Privacy & Config
|
||||||
|
|
||||||
|
- [ ] 4.1 Update /privacy page: document email sending, provider (Resend), what data is shared
|
||||||
|
- [ ] 4.2 Add `RESEND_API_KEY` to CD secrets and deploy documentation
|
||||||
|
- [ ] 4.3 Document sender domain DNS setup (DKIM/SPF for noreply@trails.cool)
|
||||||
|
|
||||||
|
## 5. Verify
|
||||||
|
|
||||||
|
- [ ] 5.1 Test magic link email delivery locally with Resend test API key
|
||||||
|
- [ ] 5.2 Test welcome email on registration
|
||||||
|
- [ ] 5.3 Verify existing E2E tests still pass (dev mode unchanged)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue