feat(web): notifications bell, unread badge and inbox panel in the header (#5342)

- The bell sits between search and the user menu. Its badge is
  parchment on obsidian, not the accent, which the house style keeps
  off general chrome. It counts up to 9, then shows 9+.
- The panel lists the server-rendered title, body and relative time,
  newest first, with unread rows marked. Clicking a row marks it read
  and opens its link. "Mark all read" appears while anything is unread,
  and an empty inbox says "Nothing waiting for you."
- createNotificationsQuery and createUnreadCountQuery poll every 60s
  while the tab is visible. The `notification.created` live event
  invalidates ['notifications'] so the badge and list refresh promptly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 07:23:32 -04:00
co-authored by Claude Opus 5.5
parent 1e9408c835
commit 956058d4a0
7 changed files with 412 additions and 1 deletions
+5
View File
@@ -61,6 +61,11 @@ export const qk = {
adminDiagnosticDevices: (userId?: string) =>
['adminDiagnosticDevices', { userId: userId ?? 'all' }] as const,
smtpConfig: () => ['smtpConfig'] as const,
// Notifications inbox (M489). Both keys sit under ['notifications'] so one
// prefix invalidation refreshes the list and the badge together.
notifications: () => ['notifications', 'list'] as const,
notificationsUnread: () => ['notifications', 'unread'] as const,
notificationSettings: () => ['notificationSettings'] as const,
suggestions: (limit?: number) =>
['suggestions', { limit: limit ?? 12 }] as const,
suggestionSnoozes: () => ['suggestionSnoozes'] as const,