feat: status page unsubscription (#1736)
* feat(db): add unsubscribedAt field to page_subscriber schema
Add unsubscribedAt timestamp field to support email unsubscribe feature.
This enables tracking when subscribers opt out of status page notifications.
- Add unsubscribedAt field to page_subscriber table schema
- Create migration 0053 to ALTER TABLE with new column
- Update drizzle journal and snapshot files
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(api): add unsubscribe mutation to statusPage router
Add public unsubscribe mutation that allows page subscribers to
unsubscribe from status page notifications. The mutation validates
that the token exists, the subscription is verified, and the user
hasn't already unsubscribed before setting the unsubscribedAt timestamp.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(api): add getSubscriberByToken query to statusPage router
Add public query to retrieve subscriber info for the unsubscribe
confirmation page. Returns page name and masked email for valid
subscribers, or null if not found or already unsubscribed.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(server): add RFC 8058 one-click unsubscribe endpoint
Add public POST endpoint at /public/unsubscribe/:token for email clients
that support one-click unsubscribe (RFC 8058). The endpoint validates the
token, checks subscriber status, and sets the unsubscribedAt timestamp.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(emails): add unsubscribe link to status report emails
- Add `unsubscribeUrl` optional prop to StatusReportSchema
- Add footer section with unsubscribe link (conditionally rendered)
- Style footer with small, muted text for visual hierarchy
- Update preview props with sample unsubscribe URL
- Remove old TODO comment
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(emails): add RFC 8058 List-Unsubscribe headers to status report emails
- Update sendStatusReportUpdate to accept subscribers with tokens
- Add List-Unsubscribe and List-Unsubscribe-Post headers per RFC 8058
- Pass unsubscribeUrl to StatusReportEmail template for each subscriber
- Update all callers to pass subscriber tokens instead of just emails
- Filter out subscribers with null tokens for type safety
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(server): filter out unsubscribed users from email notifications
Add isNull(pageSubscriber.unsubscribedAt) to subscriber queries in
statusReports/post.ts and statusReportUpdates/post.ts to ensure
unsubscribed users no longer receive status report emails.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(status-page): add unsubscribe confirmation page
Add a user-facing unsubscribe confirmation page at
/[domain]/unsubscribe/[token] that allows subscribers to
confirm their unsubscription from status page notifications.
Features:
- Loading state while fetching subscriber info
- Error state for invalid/expired tokens
- Confirmation view with masked email and page name
- Success state after unsubscription
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(dashboard): add subscriber status column to table
Add a Status column to the subscribers data table that displays:
- "Active" badge for verified subscribers
- "Pending" badge for unverified subscribers
- "Unsubscribed {date}" badge for users who have unsubscribed
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(api): add re-subscription flow for unsubscribed users
When an unsubscribed user attempts to subscribe again:
- Clear unsubscribedAt field
- Reset acceptedAt to require re-verification
- Regenerate token for security
- Update expiresAt for new verification window
Also handles pending (unverified) re-subscription by regenerating token.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test(api): add unit tests for unsubscribe API endpoints
Add comprehensive unit tests for getSubscriberByToken query and
unsubscribe mutation in statusPage router:
- Test valid token returns masked email and page name
- Test non-existent token returns null/undefined
- Test already unsubscribed user returns null
- Test email masking logic with various edge cases
- Test unsubscribe mutation success and error scenarios
- Test UUID token format validation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test: add integration tests for email headers and subscriber filtering
- Add email client integration tests for List-Unsubscribe headers
- Add subscriber filtering integration tests for email queries
- Test RFC 8058 compliance headers (List-Unsubscribe-Post)
- Test email body contains unsubscribe link with proper styling
- Test unsubscribed users are excluded from notification queries
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test(api): add E2E tests for full unsubscribe flow
Add comprehensive end-to-end integration tests covering the complete
unsubscribe user journey: subscribe -> verify -> receive email -> unsubscribe.
Tests include:
- Full subscribe/verify/unsubscribe flow with proper state transitions
- Confirmation page displaying correct page name and masked email
- Timestamp tracking when user clicks confirm unsubscribe
- Email recipient filtering to exclude unsubscribed users
- Re-subscription flow after unsubscribe
- Invalid token handling
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore: remove ralph prd and progress files
* fix: format
* fix: valid subscribers
* fix: sqlite precision time
* refactor: update List-Unsubscribe to redirect to status page
Change the email unsubscribe URL from the API endpoint (/public/unsubscribe)
to the status page's unsubscribe page. This allows users to see a proper
unsubscribe confirmation UI instead of a direct API response.
- Remove List-Unsubscribe-Post header (no longer using one-click POST)
- Update List-Unsubscribe to use status page URL
- Add pageSlug and customDomain params to sendStatusReportUpdate
- Update all call sites to pass page slug/domain info
- Update integration tests for new URL format
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update packages/emails/src/client.tsx
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* fix: email header
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
authored by