The audit log, the server log and the admin quota view were three pages (the quota one a Stats tab),
each with its own filters, and the background runs had no list at all. Now one page ("Napló" /
"Activity") reads all four, each still its own table on the server: a shared bar (text, a period of
1 h to 90 days defaulting to 7, an account) and each tab's own filters, the whole view in the URL.
The old page ids open their tab.
Server side: the audit log and the new run list (`/api/admin/runs`) filter and page like the server
log, through one keyset helper (`routes/_keyset`) the log now shares; an audit row keeps the request
it was made in (migration 0111), so its log lines are a pivot away, as a run's are (`lrun`). The
quota breakdown takes since/until and one account. The Scheduler rows get a history button to their
job's runs.
Also fixed on the way: re-saving the value in force on the Configuration page staged its override
without committing it (the commit sat inside the audit branch), so the save was lost.
103 lines
5.3 KiB
TypeScript
103 lines
5.3 KiB
TypeScript
import type { ComponentType } from "react";
|
|
import { lazyWithPreload, type PreloadableComponent } from "../lib/lazyWithPreload";
|
|
import type { Page } from "../lib/urlState";
|
|
import type { PanelId, PanelLayout } from "../lib/panelLayout";
|
|
import Sidebar from "./Sidebar";
|
|
import PlaylistsRail from "./PlaylistsRail";
|
|
import { Feed, FeedPage } from "./modulePages";
|
|
|
|
// The RENDER half of the module registry: which component (and side rail) each page renders. The
|
|
// metadata half — nav order, label, icon, gate — lives in lib/modules.ts (imported by NavSidebar /
|
|
// Header / App's gate check). Together they let App drive the whole shell from `page` alone, with no
|
|
// `page === X` chrome conditionals.
|
|
//
|
|
// Every content entry is a PROP-FREE component: it reads `me` from useMe() and its module-local state
|
|
// from the scoped providers, so App renders `<Content />` uniformly. The two that need per-render
|
|
// wiring get a thin wrapper in modulePages (FeedPage supplies the GLOBAL feed scope; the channel page
|
|
// threads its own isolated scope to <Feed> directly — see the scope note in FeedFiltersProvider).
|
|
|
|
// Route-level code splitting: each page is its own lazy chunk (admin-only pages never reach
|
|
// non-admins; the logged-out landing pulls only the shell). Rendered inside App's <Suspense>. Each
|
|
// one is preloadable: App fetches the chunks of every page the account can reach once the first page
|
|
// is on screen (preloadPages below), so switching modules never shows the chunk fallback.
|
|
const Home = lazyWithPreload(() => import("./Home"));
|
|
const Channels = lazyWithPreload(() => import("./Channels"));
|
|
const Playlists = lazyWithPreload(() => import("./Playlists"));
|
|
const PlexBrowse = lazyWithPreload(() => import("./PlexBrowse"));
|
|
const PlexSidebar = lazyWithPreload(() => import("./PlexSidebar"));
|
|
const NotificationsPanel = lazyWithPreload(() => import("./NotificationsPanel"));
|
|
const Files = lazyWithPreload(() => import("./Files"));
|
|
const FilesRail = lazyWithPreload(() => import("./FilesRail"));
|
|
const Stats = lazyWithPreload(() => import("./Stats"));
|
|
const Scheduler = lazyWithPreload(() => import("./Scheduler"));
|
|
const ConfigPanel = lazyWithPreload(() => import("./ConfigPanel"));
|
|
const AdminUsers = lazyWithPreload(() => import("./AdminUsers"));
|
|
const Activity = lazyWithPreload(() => import("./Activity"));
|
|
const SettingsPanel = lazyWithPreload(() => import("./SettingsPanel"));
|
|
|
|
export type PageDef = {
|
|
// Prop-free by construction: reads `me` + module state from hooks/providers. Preloadable by type, so
|
|
// a new page cannot silently opt out of preloadPages.
|
|
Component: PreloadableComponent<object>;
|
|
// The content column's max width. The header + toolbar centre on this same width (via --content-mw)
|
|
// so the floating chrome lines up with the centred content instead of spanning the whole column.
|
|
// MUST match the max-w the page's own content wrapper uses. Omit for a full-width page (feed).
|
|
maxWidth?: string;
|
|
};
|
|
|
|
// page id → the component App renders in the content column (+ its shell chrome config).
|
|
export const PAGE_CONTENT: Record<Page, PageDef> = {
|
|
home: { Component: Home, maxWidth: "80rem" },
|
|
// The two wrappers preload the lazy chunk they render.
|
|
feed: { Component: Object.assign(FeedPage, { preload: Feed.preload }) },
|
|
channels: { Component: Channels, maxWidth: "96rem" },
|
|
playlists: { Component: Playlists, maxWidth: "64rem" },
|
|
plex: { Component: PlexBrowse, maxWidth: "100rem" },
|
|
notifications: { Component: NotificationsPanel, maxWidth: "48rem" },
|
|
files: { Component: Files, maxWidth: "80rem" },
|
|
stats: { Component: Stats },
|
|
scheduler: { Component: Scheduler },
|
|
config: { Component: ConfigPanel },
|
|
users: { Component: AdminUsers, maxWidth: "48rem" },
|
|
activity: { Component: Activity },
|
|
settings: { Component: SettingsPanel },
|
|
};
|
|
|
|
// The uniform side-rail contract — shell layout/collapse only; the rail reads its module state from
|
|
// its provider and `me` from useMe().
|
|
type RailProps = {
|
|
layout: PanelLayout;
|
|
setLayout: (l: PanelLayout) => void;
|
|
collapsed: boolean;
|
|
onToggleCollapse: () => void;
|
|
};
|
|
|
|
// Which collapse flag a rail uses (feed + plex share the filter rail's flag; playlists and files
|
|
// have their own).
|
|
type RailCollapseKey = "filter" | "playlists" | "files";
|
|
|
|
export type RailDef = {
|
|
Rail: ComponentType<RailProps>;
|
|
// The persisted panel-layout record this rail edits (App owns the panelLayouts map).
|
|
panelId: PanelId;
|
|
collapse: RailCollapseKey;
|
|
};
|
|
|
|
// page id → its side rail (only the pages that have one). App renders it in the rail slot.
|
|
export const PAGE_RAIL: Partial<Record<Page, RailDef>> = {
|
|
feed: { Rail: Sidebar, panelId: "feed", collapse: "filter" },
|
|
plex: { Rail: PlexSidebar, panelId: "plex", collapse: "filter" },
|
|
playlists: { Rail: PlaylistsRail, panelId: "playlists", collapse: "playlists" },
|
|
files: { Rail: FilesRail, panelId: "files", collapse: "files" },
|
|
};
|
|
|
|
/** Fetch the code of these pages and of their side rails now, ahead of their first visit. Errors are
|
|
* swallowed: a chunk that fails to preload is simply fetched (and its failure surfaced) on visit. */
|
|
export function preloadPages(pages: Page[]): void {
|
|
for (const p of pages) {
|
|
PAGE_CONTENT[p].Component.preload().catch(() => {});
|
|
const rail = PAGE_RAIL[p]?.Rail as Partial<PreloadableComponent<RailProps>> | undefined;
|
|
rail?.preload?.().catch(() => {});
|
|
}
|
|
}
|