feat(notifications): persistent notification center

- notifications table (goose migration) + sqlc queries; list with unread
  count, mark-one, mark-all endpoints under /notifications.
- Deployment success/failure now writes a notification row for the owner
  (insertUserNotification — best-effort, never breaks the deploy flow).
- Shell bell reads the real persisted feed: unread badge, click-to-read,
  mark-all, 30s polling. Replaces the transient builds+audit derivation.

Verified e2e: migration applies clean, list/read/read-all round-trips.

Authored By: TDvorak <[email protected]>
This commit is contained in:
Tomas Dvorak committed 2026-09-17 17:52:37 +02:00
1 parent 08d92fb10f
commit 07ac54aa05
14 files changed
+685 -45

No files matched your search

+129
View File
@@ -1314,6 +1314,118 @@ export interface paths {
patch?: never;
trace?: never;
};
"/notifications": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** List notifications for the current user */
get: {
parameters: {
query?: {
limit?: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Notifications and unread count */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["NotificationListResponse"];
};
};
};
};
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/notifications/{id}/read": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Mark a notification as read */
post: {
parameters: {
query?: never;
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Marked read */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageResponse"];
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/notifications/read-all": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Mark all notifications as read */
post: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description All marked read */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageResponse"];
};
};
};
};
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/audit-logs": {
parameters: {
query?: never;
@@ -7030,6 +7142,23 @@ export interface components {
*/
created_at?: string;
};
Notification: {
/** Format: uuid */
id?: string;
kind?: string;
title?: string;
body?: string;
resource_type?: string;
resource_id?: string;
/** Format: date-time */
read_at?: string | null;
/** Format: date-time */
created_at?: string;
};
NotificationListResponse: {
notifications?: components["schemas"]["Notification"][];
unread?: number;
};
AuditLogListResponse: {
audit_logs?: components["schemas"]["AuditLog"][];
};