Files
memby/admin-ui/src/nav.ts
T
2026-08-14 09:40:03 +12:00

306 lines
8.3 KiB
TypeScript

import type { IconName } from './components/Icon';
/* The console's table of contents, and the only place a page is declared.
*
* The rail, the page search and the router all read it, so a page cannot be in the menu
* and 404, or be reachable and unnamed. It used to have to agree with a matching list in
* Go as well — the gateway rendered the rail and decided which /admin URLs were legal —
* which is a duplication the single-page console removes: the gateway now serves the
* console for any /admin path and the routing is entirely here.
*
* `hidden` marks a destination that is reachable and titled but not in the rail: a page
* about one person or one television belongs to the thing it is about, not to a menu. */
export interface NavItem {
id: string;
path: string;
label: string;
title: string;
intro: string;
icon?: IconName;
hidden?: boolean;
/** badge names a live count the rail should show beside this item — today only the
* unread activity count. */
badge?: 'notifications';
}
export interface NavGroup {
id: string;
label?: string;
items: NavItem[];
}
export const nav: NavGroup[] = [
{
id: 'overview',
items: [
{
id: 'overview',
path: '/admin',
label: 'Overview',
title: 'Overview',
intro: 'What the gateway is doing right now.',
icon: 'overview',
},
{
id: 'activity',
path: '/admin/activity',
label: 'Activity',
title: 'Activity',
intro: 'Every administrative event, newest first.',
icon: 'bell',
badge: 'notifications',
},
],
},
{
id: 'people',
label: 'People',
items: [
{
id: 'accounts',
path: '/admin/accounts',
label: 'Users',
title: 'Memby users',
intro: 'Who uses Memby, and the devices they are signed in on.',
icon: 'people',
},
{
id: 'account',
path: '/admin/accounts/:userId',
label: 'User',
title: 'User',
intro: 'Devices, recommendation setup and synced settings for one person.',
hidden: true,
},
{
id: 'settings-history',
path: '/admin/accounts/:userId/settings',
label: 'Settings history',
title: 'Settings history',
intro: "Every change to one person's synced settings, and which devices took it.",
hidden: true,
},
{
id: 'clients',
path: '/admin/clients',
label: 'Devices',
title: 'Devices',
intro: 'Which sets have reported in, what they are running and what their build understands.',
icon: 'tv',
},
{
id: 'logins',
path: '/admin/logins',
label: 'Sign-ins',
title: 'Sign-in history',
intro: 'Every connection attempt: who, which television, from where, and whether it got in.',
icon: 'key',
},
{
id: 'device',
path: '/admin/devices/:deviceId',
label: 'Device',
title: 'Device',
intro: 'One television: how often it connects, at what times, and from which addresses.',
hidden: true,
},
],
},
{
id: 'content',
label: 'Content',
items: [
{
id: 'library',
path: '/admin/library',
label: 'Library',
title: 'Library',
intro: 'Import and inspect the catalogue Memby ranks.',
icon: 'library',
},
{
id: 'ratings',
path: '/admin/ratings',
label: 'Movie ratings',
title: 'Movie ratings',
intro: 'Optional MDBList scores on films and shows.',
icon: 'star',
},
{
id: 'requests',
path: '/admin/requests',
label: 'Media requests',
title: 'Media requests',
intro: 'Who can ask for something the library does not have.',
icon: 'inbox',
},
],
},
{
id: 'personalisation',
label: 'Personalisation',
items: [
{
id: 'recommendations',
path: '/admin/recommendations',
label: 'For You',
title: 'For You',
intro: 'The prepared pools personalised rows are drawn from.',
icon: 'sparkle',
},
{
id: 'inspector',
path: '/admin/inspector',
label: 'Score inspector',
title: 'Score inspector',
intro: 'Re-run the ranker for one person and read every component.',
icon: 'search',
},
],
},
{
id: 'experience',
label: 'Experience',
items: [
{
id: 'hero',
path: '/admin/hero',
label: 'Home hero',
title: 'Home hero',
intro:
'Choose films or television shows for the launcher spotlight while recent releases fill the remaining places.',
icon: 'star',
},
{
id: 'features',
path: '/admin/features',
label: 'Features',
title: 'Features',
intro: 'Roll out, stop and recover optional behaviour with no app release.',
icon: 'sliders',
},
{
id: 'playback',
path: '/admin/playback',
label: 'Playback',
title: 'Playback',
intro: 'Presentation policy sent with every playback launch.',
icon: 'play',
},
{
id: 'subtitles',
path: '/admin/subtitles',
label: 'Subtitles',
title: 'Subtitles',
intro: 'Which providers a viewer may fetch a missing subtitle from.',
icon: 'captions',
},
{
id: 'updates',
path: '/admin/updates',
label: 'App updates',
title: 'App updates',
intro: 'Publish an optional or a required client update.',
icon: 'upload',
},
],
},
{
id: 'operations',
label: 'Operations',
items: [
{
id: 'tasks',
path: '/admin/tasks',
label: 'Scheduled tasks',
title: 'Scheduled tasks',
intro: 'What the gateway does in the background, when it last ran and whether it worked.',
icon: 'clock',
},
{
id: 'integrations',
path: '/admin/integrations',
label: 'Integrations',
title: 'Integrations',
intro: 'Send administrative events to Discord and, in time, elsewhere.',
icon: 'plug',
},
{
id: 'maintenance',
path: '/admin/maintenance',
label: 'Maintenance',
title: 'Maintenance',
intro: 'Take Memby offline for every television.',
icon: 'wrench',
},
{
id: 'imports',
path: '/admin/imports',
label: 'Imports',
title: 'Imports',
intro: 'Catalogue synchronisation history.',
icon: 'database',
},
{
id: 'logs',
path: '/admin/logs',
label: 'Server logs',
title: 'Server logs',
intro: 'Structured gateway events as they happen.',
icon: 'list',
},
],
},
{
id: 'reporting',
label: 'Reporting',
items: [
{
id: 'views',
path: '/admin/views',
label: 'Views',
title: 'App views',
intro: 'Home-screen visits, viewers and the times Memby is used.',
icon: 'overview',
},
{
id: 'journeys',
path: '/admin/journeys',
label: 'Journeys',
title: 'User journeys',
intro: 'How viewers move through Memby, use features and complete flows.',
icon: 'journey',
},
{
id: 'engagement',
path: '/admin/engagement',
label: 'Row engagement',
title: 'Row engagement',
intro: 'Impressions, focus, dwell and selections per launcher row.',
icon: 'chart',
},
{
id: 'searches',
path: '/admin/searches',
label: 'Searches',
title: 'Searches',
intro: 'What the household has been looking for, and what it searched just now.',
icon: 'search',
},
],
},
];
export const allNavItems: NavItem[] = nav.flatMap((group) => group.items);
/** searchableNavItems is what the omni search offers: every page that has a URL of its
* own, which is every page that is not addressed by an id in the path. */
export const searchableNavItems = nav.flatMap((group) =>
group.items.filter((item) => !item.hidden).map((item) => ({ ...item, group: group.label ?? '' })),
);
export function navItem(id: string): NavItem | undefined {
return allNavItems.find((item) => item.id === id);
}