Admin → Diagnostics reads the server's stored log: one list, newest first, where a line opens in place to its full record (time, source and logger, account, request and job ids, version, message, attributes, stack). A request or job id narrows the list to that request or job with every other filter off, shown as a removable chip. Filters: level group, source, account, event, period and a free-text search; all of them live in the URL (`l*` keys, which setPage clears like the Plex and Channels keys), so a filtered view is a link and Back restores it. "Live" re-reads the newest page every few seconds while the tab is visible; paging into older rows turns it off. Copy puts the loaded lines on the clipboard as JSON lines. The facet counts now honour every filter but their own, as the feed's do: a request pivot narrows them, and a picked level does not zero its siblings. A value in use stays offered even when the counts drop it. "Saját napló" and the console draw a line with one shared component, `LogLine`, whose phone layout gives the message its own full-width line (sharing the code's line left it a few characters). The level groups moved to lib/log.ts for both. The console's own reads are logged at DEBUG on success, like the healthcheck: at INFO its live tail filled the log with itself.
162 lines
6.1 KiB
TypeScript
162 lines
6.1 KiB
TypeScript
import {
|
|
Activity,
|
|
BarChart3,
|
|
Bell,
|
|
Clapperboard,
|
|
Download,
|
|
Home,
|
|
ListVideo,
|
|
MessageSquare,
|
|
ScrollText,
|
|
FileSearch,
|
|
Settings,
|
|
SlidersHorizontal,
|
|
Tv,
|
|
Users,
|
|
Youtube,
|
|
type LucideIcon,
|
|
} from "lucide-react";
|
|
import type { Me } from "./api";
|
|
import type { Page } from "./urlState";
|
|
|
|
// ── The module registry ───────────────────────────────────────────────────────────────────────
|
|
// One ordered source of truth for every navigable module: its nav label, icon, admin/system
|
|
// placement, whether it carries a live badge, and its availability gate. The nav rail, the header's
|
|
// ◀/▶ stepper, and (as the registry grows) the page render all read from here, so a new or newly
|
|
// gated module shows up everywhere at once — nothing hard-codes the sequence or duplicates the icon.
|
|
export type ModuleDef = {
|
|
id: Page;
|
|
// i18n key for the SHORT display name — the same label the nav rail and the header pill show.
|
|
labelKey: string;
|
|
// i18n key for the LONGER, descriptive browser-tab title (e.g. "Channel manager" vs the rail's
|
|
// "Channels"). Absent → the tab title falls back to `labelKey`. Read via `pageTitleKey`.
|
|
titleKey?: string;
|
|
icon: LucideIcon;
|
|
// Admin/system modules render in their own section (below a divider) in the nav rail.
|
|
system?: boolean;
|
|
// Carries a live unread/active count in the rail (the number itself is computed in NavSidebar,
|
|
// which owns the polling queries; this only flags which modules have one).
|
|
hasBadge?: boolean;
|
|
// Availability for THIS account, derived from language-independent account state (plex toggle,
|
|
// demo restrictions, admin role). Absent = always available. Drives both the reachable module
|
|
// set (nav + stepper) and, later, the render gate.
|
|
gate?: (me: Me) => boolean;
|
|
};
|
|
|
|
const isAdmin = (me: Me) => me.role === "admin";
|
|
const notDemo = (me: Me) => !me.is_demo;
|
|
|
|
// Ordered in nav-rail order — this array IS the sequence the rail and the stepper walk.
|
|
const MODULES: readonly ModuleDef[] = [
|
|
// Home leads: it is the landing (see navigationInit.loadInitialPage), and the module order IS the
|
|
// nav order and the header stepper's sequence. The feed takes the YouTube mark its own toolbar
|
|
// already uses, so the two are apart by shape and not just by position.
|
|
{ id: "home", labelKey: "home.navLabel", titleKey: "home.title", icon: Home },
|
|
{ id: "feed", labelKey: "header.account.feed", icon: Youtube },
|
|
{
|
|
id: "channels",
|
|
labelKey: "header.account.channels",
|
|
titleKey: "header.channelManager",
|
|
icon: Tv,
|
|
},
|
|
{ id: "playlists", labelKey: "header.account.playlists", icon: ListVideo },
|
|
{ id: "plex", labelKey: "plex.navLabel", icon: Clapperboard, gate: (me) => me.plex_enabled },
|
|
{ id: "notifications", labelKey: "inbox.navLabel", icon: Bell, hasBadge: true },
|
|
{
|
|
id: "messages",
|
|
labelKey: "messages.navLabel",
|
|
icon: MessageSquare,
|
|
hasBadge: true,
|
|
gate: notDemo,
|
|
},
|
|
{
|
|
id: "downloads",
|
|
labelKey: "downloads.navLabel",
|
|
icon: Download,
|
|
hasBadge: true,
|
|
gate: notDemo,
|
|
},
|
|
{ id: "stats", labelKey: "header.account.stats", titleKey: "header.usageStats", icon: BarChart3 },
|
|
{
|
|
id: "scheduler",
|
|
labelKey: "header.account.scheduler",
|
|
titleKey: "header.scheduler",
|
|
icon: Activity,
|
|
system: true,
|
|
gate: isAdmin,
|
|
},
|
|
{
|
|
id: "config",
|
|
labelKey: "header.account.config",
|
|
titleKey: "header.configuration",
|
|
icon: SlidersHorizontal,
|
|
system: true,
|
|
gate: isAdmin,
|
|
},
|
|
{
|
|
id: "users",
|
|
labelKey: "header.account.users",
|
|
titleKey: "header.users",
|
|
icon: Users,
|
|
system: true,
|
|
gate: isAdmin,
|
|
},
|
|
{
|
|
id: "audit",
|
|
labelKey: "header.account.audit",
|
|
titleKey: "audit.title",
|
|
icon: ScrollText,
|
|
system: true,
|
|
gate: isAdmin,
|
|
},
|
|
{
|
|
id: "logs",
|
|
labelKey: "header.account.logs",
|
|
titleKey: "adminLogs.title",
|
|
icon: FileSearch,
|
|
system: true,
|
|
gate: isAdmin,
|
|
},
|
|
];
|
|
|
|
// Settings is reachable (its own rail button + the ◀/▶ wrap target) but is not a primary module in
|
|
// the ordered nav list, so it lives beside the array rather than in it.
|
|
const SETTINGS_MODULE: ModuleDef = {
|
|
id: "settings",
|
|
labelKey: "header.account.settings",
|
|
titleKey: "settings.title",
|
|
icon: Settings,
|
|
};
|
|
|
|
// Registering a NEW Page means adding it to MODULES (or SETTINGS_MODULE) above — that's the single
|
|
// place. If a Page is left unregistered, moduleDef()/moduleLabelKey[] return undefined and NavSidebar
|
|
// throws on `.icon`; the coverage isn't compiler-enforced here because the ordered array can't also
|
|
// be an exhaustive Page map, so treat the array as the checklist when the Page union grows.
|
|
const BY_ID = Object.fromEntries([...MODULES, SETTINGS_MODULE].map((m) => [m.id, m])) as Record<
|
|
Page,
|
|
ModuleDef
|
|
>;
|
|
|
|
// Look up a module's definition by page id (every Page is covered, incl. settings).
|
|
export function moduleDef(id: Page): ModuleDef {
|
|
return BY_ID[id];
|
|
}
|
|
|
|
// The i18n key for each module's SHORT display name. Single source so the nav rail and the header's
|
|
// ModuleName pill always read identically (the longer, descriptive `pageTitleKey` names are kept
|
|
// only for the browser tab title).
|
|
export const moduleLabelKey: Record<Page, string> = Object.fromEntries(
|
|
[...MODULES, SETTINGS_MODULE].map((m) => [m.id, m.labelKey]),
|
|
) as Record<Page, string>;
|
|
|
|
// Admin/system modules — rendered in their own section (below a divider) in the nav rail.
|
|
export const SYSTEM_PAGES: readonly Page[] = MODULES.filter((m) => m.system).map((m) => m.id);
|
|
|
|
// The ordered list of primary module pages available to THIS user, in nav-rail order. Single
|
|
// source of truth for both the nav rail (NavSidebar) and the header's ◀/▶ module stepper, so a
|
|
// new module (or a newly-gated one) shows up in both at once. Availability is derived from the
|
|
// account via each module's `gate`, so the reachable set changes with the account state, not the UI.
|
|
export function moduleOrder(me: Me): Page[] {
|
|
return MODULES.filter((m) => !m.gate || m.gate(me)).map((m) => m.id);
|
|
}
|