feat: T4 wave 3 - notifications per user, suggestions API, varroa bands, split/queen events

- Schema III: per-user notifications (broadcast fan-out migration,
  category, link, requiredPermissions filter), notification preferences
  and settings endpoints, calendar withdrawal switch, occupiedFrames
- Suggestions API: list/summary with permission visibility and paging,
  accept/dismiss/reopen incl. bulk, SuggestionView on all responses
- Varroa traffic light by season and method, young colonies, treatment
  effect window, sample-based percentage; shared withdrawal-window helper
  on Berlin calendar days; GET /varroa-measurements/thresholds
- ColonySplit and QueenAssigned domain events with calendar and webhook
  handlers
- Outbox claims compare against now() rounded to timestamp(3); fixes the
  flaky e2e claims of rows written with SQL now()

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-14 14:25:24 +02:00
1 parent d99a697739
commit f468dc5f75
87 files changed
+6022 -510

No files matched your search

@@ -0,0 +1,18 @@
/**
* A notification link is a relative app path: it starts with "/", not with
* "//" or "/\", carries no scheme and is at most 300 characters long. Anything
* else is a programming error, never user input (the code that records the
* notification builds the link).
*/
const APP_LINK_PATTERN = /^\/(?![/\\])[A-Za-z0-9\-._~/?=&%]{0,299}$/;
export function assertAppLink(link: string): void {
if (!APP_LINK_PATTERN.test(link)) {
throw new Error(`Notification link is not a relative app path: ${link}`);
}
}
/** One key per recipient, so a fan-out repeated for the same event writes nothing new. */
export function notificationDedupeKey(prefix: string, userId: string): string {
return `${prefix}:${userId}`;
}