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
|
|
@ -10,7 +10,7 @@ import {
|
|||
saveLocalTheme,
|
||||
type ThemePrefs,
|
||||
} from "./lib/theme";
|
||||
import { hasFilterParams, paramsToFilters, readPage, stripUrlParams, type Page } from "./lib/urlState";
|
||||
import { hasFilterParams, isPage, paramsToFilters, readPage, stripUrlParams, type Page } from "./lib/urlState";
|
||||
import {
|
||||
loadLayout,
|
||||
normalizeLayout,
|
||||
|
|
@ -29,6 +29,7 @@ import Playlists from "./components/Playlists";
|
|||
import Stats from "./components/Stats";
|
||||
import Scheduler from "./components/Scheduler";
|
||||
import SettingsPanel from "./components/SettingsPanel";
|
||||
import NotificationsPanel from "./components/NotificationsPanel";
|
||||
import OnboardingWizard from "./components/OnboardingWizard";
|
||||
import { shouldAutoOpenOnboarding } from "./lib/onboarding";
|
||||
import Toaster from "./components/Toaster";
|
||||
|
|
@ -60,15 +61,7 @@ function loadInitialPage(): Page {
|
|||
const params = new URLSearchParams(window.location.search);
|
||||
if (params.has("page")) return readPage();
|
||||
const stored = localStorage.getItem(PAGE_KEY);
|
||||
if (
|
||||
stored === "channels" ||
|
||||
stored === "stats" ||
|
||||
stored === "playlists" ||
|
||||
stored === "settings" ||
|
||||
stored === "scheduler"
|
||||
)
|
||||
return stored;
|
||||
return "feed";
|
||||
return isPage(stored) ? stored : "feed";
|
||||
}
|
||||
|
||||
function loadStoredFilters(): FeedFilters {
|
||||
|
|
@ -311,6 +304,8 @@ export default function App() {
|
|||
<Scheduler />
|
||||
) : page === "playlists" ? (
|
||||
<Playlists canWrite={meQuery.data!.can_write} />
|
||||
) : page === "notifications" ? (
|
||||
<NotificationsPanel />
|
||||
) : page === "settings" ? (
|
||||
<SettingsPanel
|
||||
me={meQuery.data!}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue