6.2.5 Filter subscriptions — schedule schema + Inngest cron delivery (results email, 50-row cap, deep link) + subscribe/unsubscribe UI
Estimate: 32m · Depends on: 6.2.1, 6.2.2, 6.2.4
The emailed-results loop, on the Story 1.6 jobs substrate.
Schema: saved_filter_subscription — filter FK (cascade per 6.2.1's delete), user FK, schedule preset (daily | weekdays | weekly(day)) + hour (workspace-timezone semantics documented), unique per (filter, user).
Delivery: an Inngest cron job (the dailyHealthCheck precedent) ticking hourly → due subscriptions fan out as events (one delivery per subscription — retries/DLQ ride the shipped 1.6 machinery); each delivery resolves the filter via the 6.2.1 read AS THE SUBSCRIBER (the permission matrix applies — a subscriber who lost browse access gets no mail, and a filter gone private stops delivering to non-owners), renders lib/emailTemplates/filterSubscription.tsx — filter name, first 50 results (our page unit; Jira caps at 200) as identifier + title + status, the total count, a deep link to the applied ?filter=v1: URL, and an unsubscribe link — and sends via lib/email.ts. Zero-result deliveries still send (the mirror's subscription is a report, not an alert; note it in the template copy).
UI: the subscribe control + preset editor per the 6.2.2 design (dropdown row action + directory row action), the subscribed state, unsubscribe (in-app + the email link, token-authenticated like the shipped unsubscribe patterns). Strings via next-intl.
Acceptance criteria
- Schedule semantics tested with a frozen clock: daily/weekdays/weekly(day) at the configured hour each compute due-ness correctly across DST boundaries (documented timezone rule).
- The delivery resolves as the subscriber (permission-loss and gone-private paths asserted: no mail), caps at 50 + true total count, deep-links to the exact applied state, and survives a stale-referent AST (the email renders the degraded condition, never crashes the job — DLQ stays empty in the happy path).
- Template snapshot-tested (subject + text + html; unsubscribe link verbatim in plain text per the email-template contract); subscribe/unsubscribe round-trips from both mounts; filter deletion cascades subscriptions.
pnpm test:coverage≥90% on the new service/job files.
Context refs
lib/jobs/(defineJob/registry/retries/DLQ) +lib/jobs/definitions/dailyHealthCheck.ts— the cron precedentlib/emailTemplates/contract inmotir-core/CLAUDE.md(pure templates, hand-written plain text) +lib/email.ts- 6.2.1 resolve-as-user read;
design/work-items/saved-filters.mock.html(the subscription editor panels) - The verified Jira subscription facts (presets/cron, 200-cap) in the Story 6.2 description