Add transactional emails (SMTP) and planner features (no-go areas, notes, crash recovery)

Transactional emails:
- Add nodemailer SMTP email module with dev-mode console logging
- Magic link template and welcome template with HTML + plain text
- Wire sendMagicLink into login flow, sendWelcome into registration
- Update privacy page and deploy docs for SMTP configuration

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

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-03-26 01:00:42 +01:00
parent 05b5f6febf
commit 0a8dd0b766
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
27 changed files with 1030 additions and 69 deletions

View file

@ -1,36 +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
- [x] 1.1 Add `noGoAreas` Y.Array to Yjs doc in use-yjs.ts
- [x] 1.2 Add leaflet-draw (or geoman) dependency for polygon drawing
- [x] 1.3 Create NoGoAreaLayer component — renders polygons as red overlays, handles draw/delete
- [x] 1.4 Add no-go area toolbar button to PlannerMap
- [x] 1.5 Pass no-go areas to BRouter API as `nogo` parameters in brouter.ts
- [x] 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)
- [x] 2.1 Add `notes` Y.Text to Yjs doc in use-yjs.ts
- [x] 2.2 Create NotesPanel component — textarea bound to Y.Text with real-time sync
- [x] 2.3 Add "Notes" tab to sidebar (alongside waypoints)
- [x] 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
- [x] 3.1 Add periodic localStorage save (every 10s) of Yjs state in use-yjs.ts
- [x] 3.2 On session reconnect, check localStorage for saved state and apply as Yjs update
- [x] 3.3 Clear localStorage entry after successful sync or session close
- [x] 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
- [x] 4.1 Create rate limiter utility in apps/planner/app/lib/rate-limit.ts (already exists — extend for IP tracking)
- [x] 4.2 Add session creation rate limit (10/IP/hour) in the /new route or API
- [x] 4.3 Add BRouter proxy rate limit (60/session/hour) in the route computation handler
- [x] 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
- [x] 5.1 Test no-go areas: draw polygon, verify route avoids it, delete polygon
- [x] 5.2 Test notes: type in two windows, verify real-time sync
- [x] 5.3 Test crash recovery: make changes, kill browser, reopen — verify recovery
- [x] 5.4 Test rate limiting: exceed limits, verify 429 response

View file

@ -22,16 +22,15 @@ to console — users never receive it. No email infrastructure exists.
## Decisions
### D1: Resend as email provider
### D1: SMTP via nodemailer
Use [Resend](https://resend.com) — simple API, generous free tier (3k
emails/month), good developer experience. Single dependency: `resend` npm
package.
Use [nodemailer](https://nodemailer.com) with SMTP — standard, provider-agnostic,
works with any SMTP server. Single dependency: `nodemailer` npm package.
Configured via `SMTP_URL` env var (e.g., `smtp://user:pass@mail.example.com:587`).
**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.
**Alternative considered**: Resend API. Simpler initial setup but creates vendor
dependency. SMTP is a standard protocol that works with any provider and aligns
with the self-hostable philosophy.
### D2: Email module in apps/journal/app/lib/email.server.ts
@ -50,13 +49,14 @@ 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.
Requires DNS records (DKIM/SPF/DMARC) for deliverability. 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.
- **SMTP server required** → Must provide an SMTP server. Can use any provider
(Mailgun, Amazon SES, Postfix, etc.) or a simple relay.
- **DNS records required** → Must add SPF/DKIM/DMARC records for deliverability.
- **No built-in retry** → nodemailer doesn't queue. If SMTP is down, the send
fails. Acceptable for current scale — welcome emails are fire-and-forget,
magic links can be re-requested.

View file

@ -1,30 +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)
- [x] 1.1 Add `nodemailer` package to Journal dependencies
- [x] 1.2 Create `apps/journal/app/lib/email.server.ts` with sendEmail(to, subject, html, text) — uses SMTP in production, logs to console in dev
- [x] 1.3 Add `SMTP_URL` and `SMTP_FROM` env vars to docker-compose.yml
- [x] 1.4 Write unit test for sendEmail (mock nodemailer, 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
- [x] 2.1 Create magicLinkTemplate(link: string) returning { html, text } — includes link, 15-min expiry note, trails.cool branding
- [x] 2.2 Create welcomeTemplate(username: string) returning { html, text } — greeting, what they can do, link to routes
- [x] 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)
- [x] 3.1 Update api.auth.login.ts: call sendMagicLink in production instead of just logging
- [x] 3.2 Update registration flow: call sendWelcome after successful passkey registration
- [x] 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)
- [x] 4.1 Update /privacy page: document email sending, provider (Resend), what data is shared
- [x] 4.2 Add `SMTP_URL` to server env and deploy documentation
- [x] 4.3 Document sender domain DNS setup (SPF/DKIM/DMARC 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)
- [x] 5.1 Test magic link email delivery locally with SMTP
- [x] 5.2 Test welcome email on registration
- [x] 5.3 Verify existing E2E tests still pass (dev mode unchanged)