feat(notifications): durable per-user inbox (P1) + maintenance schema
Add a server-backed notification center that coexists with the client-side transient bell: a per-user `notifications` table (type/title/body/data JSON/ read/dismissed), a `/api/me/notifications` CRUD API (list, unread_count, read, read_all, dismiss, clear), and a left-nav inbox module with a live unread badge polled via useLiveQuery. Known types render trilingual text from type+data (English stored text is the fallback); read rows are trimmed past a soft cap. Also adds the schema the maintenance job builds on: videos.list?part=status columns (embeddable/privacy_status/upload_status) and the validation lifecycle columns (last_checked_at, unavailable_since, unavailable_reason). Page-id validation is centralized in one PAGES source of truth (isPage) so the new page survives reload without a second allowlist to keep in sync.
This commit is contained in:
parent
a11a8db278
commit
b9a3a9012d
14 changed files with 649 additions and 22 deletions
|
|
@ -78,17 +78,29 @@ export function hasFilterParams(params: URLSearchParams): boolean {
|
|||
return KEYS.some((k) => params.has(k));
|
||||
}
|
||||
|
||||
export type Page = "feed" | "channels" | "stats" | "playlists" | "settings" | "scheduler";
|
||||
// The single source of truth for valid page ids; `Page` and the runtime validator both
|
||||
// derive from it, so adding a page is a one-line change (no parallel allowlists to keep in
|
||||
// sync). "feed" is the default/fallback.
|
||||
export const PAGES = [
|
||||
"feed",
|
||||
"channels",
|
||||
"stats",
|
||||
"playlists",
|
||||
"settings",
|
||||
"scheduler",
|
||||
"notifications",
|
||||
] as const;
|
||||
|
||||
export type Page = (typeof PAGES)[number];
|
||||
|
||||
/** Narrow an arbitrary string to a known Page (else null). */
|
||||
export function isPage(p: string | null | undefined): p is Page {
|
||||
return !!p && (PAGES as readonly string[]).includes(p);
|
||||
}
|
||||
|
||||
export function readPage(): Page {
|
||||
const p = new URLSearchParams(window.location.search).get("page");
|
||||
return p === "channels" ||
|
||||
p === "stats" ||
|
||||
p === "playlists" ||
|
||||
p === "settings" ||
|
||||
p === "scheduler"
|
||||
? p
|
||||
: "feed";
|
||||
return isPage(p) ? p : "feed";
|
||||
}
|
||||
|
||||
/** Build a shareable absolute URL that reproduces the current filters (+ page). Used by the
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue