Some checks failed
CI / Lint, typecheck, test (push) Successful in 3m41s
CD / Build and push images (push) Successful in 3m44s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m18s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Failing after 2m53s
CI / Import/export fidelity gate (push) Has been skipped
New per-user digestFrequency (hourly default | daily | off) on the profile and in the settings UI. A scheduler job (15 min cadence) mails a user once their oldest unread, unmailed notification exceeds the cadence window: one localized mail per batch, grouped per pond then per page with actor names and change/comment counts, enqueued through the mail outbox. Sending marks the batch mailed — never read — and re-checks page read permission per entry at send time; entries the user can no longer read are dropped from the mail but still marked handled, so revoked content cannot queue forever. Every mail carries a signed, single-purpose unsubscribe link: it only flips the setting to off, renders a session-free confirmation page, and sets no cookie. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
48 lines
1.4 KiB
TypeScript
48 lines
1.4 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
/**
|
|
* In-app notifications (issue #94, data-model.md §notifications): watchers
|
|
* learn about page changes (version snapshots, ADR 0013's change unit) and
|
|
* new comments. Generation excludes the actor and re-checks page read
|
|
* permission at delivery time.
|
|
*/
|
|
|
|
export const NOTIFICATION_TYPES = ['page_changed', 'comment_added'] as const;
|
|
export type NotificationType = (typeof NOTIFICATION_TYPES)[number];
|
|
|
|
export interface NotificationPayload {
|
|
pageId: string;
|
|
pageTitle: string;
|
|
pageSlug: string;
|
|
pondSlug: string;
|
|
pondName: string;
|
|
/** Display names of the acting users (first few, for the list entry). */
|
|
actorNames: string[];
|
|
}
|
|
|
|
export interface NotificationView {
|
|
id: string;
|
|
type: NotificationType;
|
|
payload: NotificationPayload;
|
|
createdAt: string;
|
|
readAt: string | null;
|
|
}
|
|
|
|
export const NOTIFICATION_PAGE_SIZE = 20;
|
|
|
|
export const notificationListQuerySchema = z.object({
|
|
page: z.coerce.number().int().min(1).default(1),
|
|
});
|
|
export type NotificationListQuery = z.infer<typeof notificationListQuerySchema>;
|
|
|
|
export interface NotificationListView {
|
|
notifications: NotificationView[];
|
|
unreadCount: number;
|
|
page: number;
|
|
pageCount: number;
|
|
}
|
|
|
|
/** E-mail digest cadence (issue #95). */
|
|
export const DIGEST_FREQUENCIES = ['hourly', 'daily', 'off'] as const;
|
|
export type DigestFrequency = (typeof DIGEST_FREQUENCIES)[number];
|