Reorganize admin configuration sections
Magent CI/CD / verify (push) Successful in 10m37s
Magent CI/CD / deploy-prod (push) Skipped
Magent CI/CD / deploy-beta (push) Successful in 16s

This commit is contained in:
2026-08-31 16:32:24 +12:00
parent 3402e53c31
commit 0e04d219a0
3 changed files with 355 additions and 145 deletions
+267 -122
View File
@@ -33,14 +33,15 @@ const SECTION_LABELS: Record<string, string> = {
jellyseerr: 'Seerr',
jellyfin: 'Jellyfin',
artwork: 'Artwork cache',
cache: 'Cache Control',
cache: 'Request cache',
sonarr: 'Sonarr',
radarr: 'Radarr',
prowlarr: 'Prowlarr',
qbittorrent: 'qBittorrent',
log: 'Activity log',
requests: 'Request sync',
site: 'Site',
logs: 'Activity log',
maintenance: 'Maintenance',
requests: 'Request pipeline',
site: 'Site & login',
}
const BOOL_SETTINGS = new Set([
@@ -114,7 +115,8 @@ const SECTION_DESCRIPTIONS: Record<string, string> = {
prowlarr: 'Prowlarr connection used by Sonarr and Radarr for release searches.',
qbittorrent: 'qBittorrent connection used for collector-owned download progress and diagnostics.',
requests: 'Control how often requests are refreshed and cleaned up.',
log: 'Activity log for troubleshooting.',
logs: 'Control log output and inspect recent application activity for troubleshooting.',
maintenance: 'Repair cached data, clean historical records, and run recovery operations.',
site: 'Sitewide banner, login page visibility, and version details. The changelog is generated from git history during release builds.',
}
@@ -233,10 +235,14 @@ const MAGENT_SECTION_GROUPS: Array<{
'magent_notify_push_token',
'magent_notify_push_user_key',
'magent_notify_push_device',
'magent_notify_webhook_enabled',
'magent_notify_webhook_url',
],
},
{
key: 'magent-notify-webhook',
title: 'Generic Webhook',
description: 'Send notifications to a custom automation or integration endpoint.',
keys: ['magent_notify_webhook_enabled', 'magent_notify_webhook_url'],
},
]
const MAGENT_GROUPS_BY_SECTION: Record<string, Set<string>> = {
@@ -247,6 +253,7 @@ const MAGENT_GROUPS_BY_SECTION: Record<string, Set<string>> = {
'magent-notify-discord',
'magent-notify-telegram',
'magent-notify-push',
'magent-notify-webhook',
]),
}
@@ -281,6 +288,130 @@ const SITE_SECTION_GROUPS: Array<{
},
]
const STANDARD_SECTION_GROUPS: Record<
string,
Array<{ key: string; title: string; description: string; keys: string[] }>
> = {
seerr: [
{
key: 'seerr-connection',
title: 'Connection',
description: 'The Seerr endpoint and API credential Magent uses for request discovery and status.',
keys: ['jellyseerr_base_url', 'jellyseerr_api_key'],
},
],
jellyseerr: [
{
key: 'seerr-connection',
title: 'Connection',
description: 'The Seerr endpoint and API credential Magent uses for request discovery and status.',
keys: ['jellyseerr_base_url', 'jellyseerr_api_key'],
},
],
jellyfin: [
{
key: 'jellyfin-connection',
title: 'Connection',
description: 'Internal Jellyfin endpoint and administrator API credential used for lookups and user sync.',
keys: ['jellyfin_base_url', 'jellyfin_api_key'],
},
{
key: 'jellyfin-playback',
title: 'Playback Links',
description: 'Public address used when a viewer opens an available title from Magent.',
keys: ['jellyfin_public_url'],
},
{
key: 'jellyfin-users',
title: 'Library and User Sync',
description: 'Control cross-service library reconciliation and manually import Jellyfin users.',
keys: ['jellyfin_sync_to_arr'],
},
],
sonarr: [
{
key: 'sonarr-connection',
title: 'Connection',
description: 'Sonarr endpoint and API credential used for TV collection operations.',
keys: ['sonarr_base_url', 'sonarr_api_key'],
},
{
key: 'sonarr-library',
title: 'TV Collection Defaults',
description: 'Default quality profile and destination folder used for TV requests.',
keys: ['sonarr_quality_profile_id', 'sonarr_root_folder'],
},
],
radarr: [
{
key: 'radarr-connection',
title: 'Connection',
description: 'Radarr endpoint and API credential used for movie collection operations.',
keys: ['radarr_base_url', 'radarr_api_key'],
},
{
key: 'radarr-library',
title: 'Movie Collection Defaults',
description: 'Default quality profile and destination folder used for movie requests.',
keys: ['radarr_quality_profile_id', 'radarr_root_folder'],
},
],
prowlarr: [
{
key: 'prowlarr-connection',
title: 'Connection',
description: 'Prowlarr endpoint and API credential used for indexer health and release discovery.',
keys: ['prowlarr_base_url', 'prowlarr_api_key'],
},
],
qbittorrent: [
{
key: 'qbittorrent-connection',
title: 'Connection and Sign-in',
description: 'qBittorrent Web UI endpoint and credentials used for live download progress and recovery.',
keys: ['qbittorrent_base_url', 'qbittorrent_username', 'qbittorrent_password'],
},
],
requests: [
{
key: 'requests-sync',
title: 'Synchronization Schedule',
description: 'Control incremental checks and the scheduled full request-cache rebuild.',
keys: [
'requests_poll_interval_seconds',
'requests_delta_sync_interval_minutes',
'requests_full_sync_time',
],
},
{
key: 'requests-retention',
title: 'History Retention',
description: 'Choose when old status history is cleaned up and how long it is retained.',
keys: ['requests_cleanup_time', 'requests_cleanup_days'],
},
],
logs: [
{
key: 'logs-output',
title: 'Log Output',
description: 'Set the default application verbosity and the active log-file destination.',
keys: ['log_level', 'log_file'],
},
{
key: 'logs-rotation',
title: 'File Rotation',
description: 'Limit log-file growth and choose how many historical files remain on disk.',
keys: ['log_file_max_bytes', 'log_file_backup_count'],
},
{
key: 'logs-components',
title: 'Component Verbosity',
description: 'Tune noisy outbound-service and scheduled-background messages independently.',
keys: ['log_http_client_level', 'log_background_sync_level'],
},
],
}
const SETTING_LABEL_OVERRIDES: Record<string, string> = {
jellyseerr_base_url: 'Seerr base URL',
jellyseerr_api_key: 'Seerr API key',
@@ -322,6 +453,33 @@ const SETTING_LABEL_OVERRIDES: Record<string, string> = {
magent_notify_push_device: 'Device / target',
magent_notify_webhook_enabled: 'Generic webhook notifications enabled',
magent_notify_webhook_url: 'Generic webhook URL',
jellyfin_base_url: 'Internal server URL',
jellyfin_api_key: 'Administrator API key',
jellyfin_public_url: 'Public playback URL',
jellyfin_sync_to_arr: 'Reconcile Jellyfin with Sonarr and Radarr',
sonarr_base_url: 'Sonarr server URL',
sonarr_api_key: 'Sonarr API key',
sonarr_quality_profile_id: 'Default TV quality profile',
sonarr_root_folder: 'Default TV root folder',
radarr_base_url: 'Radarr server URL',
radarr_api_key: 'Radarr API key',
radarr_quality_profile_id: 'Default movie quality profile',
radarr_root_folder: 'Default movie root folder',
prowlarr_base_url: 'Prowlarr server URL',
prowlarr_api_key: 'Prowlarr API key',
qbittorrent_base_url: 'Web UI URL',
qbittorrent_username: 'Web UI username',
qbittorrent_password: 'Web UI password',
requests_sync_ttl_minutes: 'Request cache freshness (minutes)',
requests_poll_interval_seconds: 'Full-sync eligibility check (seconds)',
requests_delta_sync_interval_minutes: 'Recent-change sync interval (minutes)',
requests_full_sync_time: 'Daily full-sync time',
requests_cleanup_time: 'Daily history cleanup time',
requests_cleanup_days: 'History retention (days)',
requests_data_source: 'Request read source',
artwork_cache_mode: 'Artwork delivery mode',
log_level: 'Application log level',
log_file: 'Active log file',
site_login_show_jellyfin_login: 'Login page: Jellyfin sign-in',
site_login_show_local_login: 'Login page: local Magent sign-in',
site_login_show_forgot_password: 'Login page: forgot password',
@@ -390,12 +548,12 @@ type SectionFeedback = {
}
const SERVICE_TEST_ENDPOINTS: Record<string, string> = {
jellyseerr: 'seerr',
jellyfin: 'jellyfin',
sonarr: 'sonarr',
radarr: 'radarr',
prowlarr: 'prowlarr',
qbittorrent: 'qbittorrent',
'seerr-connection': 'seerr',
'jellyfin-connection': 'jellyfin',
'sonarr-connection': 'sonarr',
'radarr-connection': 'radarr',
'prowlarr-connection': 'prowlarr',
'qbittorrent-connection': 'qbittorrent',
}
export default function SettingsPage({ section }: SettingsPageProps) {
@@ -633,8 +791,8 @@ export default function SettingsPage({ section }: SettingsPageProps) {
: null
const isMagentGroupedSection = section === 'magent' || section === 'general' || section === 'notifications'
const isSiteGroupedSection = section === 'site'
const visibleSections = settingsSection ? [settingsSection] : []
const isCacheSection = section === 'cache'
const isArtworkSection = section === 'artwork'
const cacheSettingKeys = new Set(['requests_sync_ttl_minutes', 'requests_data_source'])
const artworkSettingKeys = new Set(['artwork_cache_mode'])
const generatedSettingKeys = new Set(['site_changelog'])
@@ -643,38 +801,59 @@ export default function SettingsPage({ section }: SettingsPageProps) {
'sonarr_qbittorrent_category',
'radarr_qbittorrent_category',
])
const requestSettingOrder = [
'requests_poll_interval_seconds',
'requests_delta_sync_interval_minutes',
'requests_full_sync_time',
'requests_cleanup_time',
'requests_cleanup_days',
]
const siteSettingOrder = [
'site_banner_enabled',
'site_banner_message',
'site_banner_tone',
'site_login_show_jellyfin_login',
'site_login_show_local_login',
'site_login_show_forgot_password',
'site_login_show_signup_link',
'site_nav_show_requests',
]
const sortByOrder = (items: AdminSetting[], order: string[]) => {
const position = new Map(order.map((key, index) => [key, index]))
return [...items].sort((a, b) => {
const aIndex = position.get(a.key) ?? Number.POSITIVE_INFINITY
const bIndex = position.get(b.key) ?? Number.POSITIVE_INFINITY
if (aIndex !== bIndex) return aIndex - bIndex
return a.key.localeCompare(b.key)
})
}
const cacheSettings = settings.filter((setting) => cacheSettingKeys.has(setting.key))
const artworkSettings = settings.filter((setting) => artworkSettingKeys.has(setting.key))
const buildDefinedSections = (
definitions: Array<{ key: string; title: string; description: string; keys: string[] }>,
sourceItems: AdminSetting[],
includeUnassigned = true,
): SettingsSectionGroup[] => {
const byKey = new Map(sourceItems.map((item) => [item.key, item]))
const assignedKeys = new Set(definitions.flatMap((group) => group.keys))
const groups = definitions.map((group) => ({
key: group.key,
title: group.title,
description: group.description,
items: group.keys
.map((key) => byKey.get(key))
.filter((item): item is AdminSetting => Boolean(item)),
}))
if (includeUnassigned) {
const unassigned = sourceItems.filter((item) => !assignedKeys.has(item.key))
if (unassigned.length > 0) {
groups.push({
key: `${section}-additional`,
title: 'Additional Settings',
description: 'Settings returned by Magent that do not yet belong to a dedicated subsection.',
items: unassigned.sort((a, b) => a.key.localeCompare(b.key)),
})
}
}
return groups
}
const standardDefinitions = STANDARD_SECTION_GROUPS[section]
const standardItems = settingsSection
? (groupedSettings[settingsSection] ?? []).filter(
(setting) => !obsoleteSettingKeys.has(setting.key) && !hiddenSettingKeys.has(setting.key),
)
: []
const settingsSections: SettingsSectionGroup[] = isCacheSection
? [
{ key: 'cache', title: 'Cache control', items: cacheSettings },
{ key: 'artwork', title: 'Artwork cache', items: artworkSettings },
{
key: 'cache',
title: 'Request Cache Strategy',
description: 'Choose where request pages read from and how long cached request records remain fresh.',
items: cacheSettings,
},
]
: isArtworkSection
? [
{
key: 'artwork',
title: 'Artwork Delivery and Storage',
description: 'Choose how posters and backdrops are delivered, then inspect or rebuild the local artwork cache.',
items: artworkSettings,
},
]
: isMagentGroupedSection
? (() => {
@@ -682,72 +861,31 @@ export default function SettingsPage({ section }: SettingsPageProps) {
return []
}
const magentItems = groupedSettings.magent ?? []
const byKey = new Map(magentItems.map((item) => [item.key, item]))
const allowedGroupKeys = MAGENT_GROUPS_BY_SECTION[section] ?? new Set<string>()
const groups: SettingsSectionGroup[] = MAGENT_SECTION_GROUPS.filter((group) =>
allowedGroupKeys.has(group.key),
).map((group) => {
const items = group.keys
.map((key) => byKey.get(key))
.filter((item): item is AdminSetting => Boolean(item))
return {
key: group.key,
title: group.title,
description: group.description,
items,
}
})
return groups
const definitions = MAGENT_SECTION_GROUPS.filter((group) => allowedGroupKeys.has(group.key))
return buildDefinedSections(definitions, magentItems, false)
})()
: isSiteGroupedSection
? (() => {
const siteItems = groupedSettings.site ?? []
const byKey = new Map(siteItems.map((item) => [item.key, item]))
return SITE_SECTION_GROUPS.map((group) => {
const items = group.keys
.map((key) => byKey.get(key))
.filter((item): item is AdminSetting => Boolean(item))
return {
key: group.key,
title: group.title,
description: group.description,
items,
}
})
})()
: visibleSections.map((sectionKey) => ({
key: sectionKey,
title: SECTION_LABELS[sectionKey] ?? sectionKey,
items: (() => {
const sectionItems = groupedSettings[sectionKey] ?? []
const filtered = sectionItems.filter((setting) => {
if (obsoleteSettingKeys.has(setting.key)) return false
if (sectionKey === 'requests' || sectionKey === 'artwork' || sectionKey === 'site') {
return !hiddenSettingKeys.has(setting.key)
}
return true
})
if (sectionKey === 'requests') {
return sortByOrder(filtered, requestSettingOrder)
}
if (sectionKey === 'site') {
return sortByOrder(filtered, siteSettingOrder)
}
return filtered
})(),
}))
? buildDefinedSections(
SITE_SECTION_GROUPS,
(groupedSettings.site ?? []).filter((setting) => !hiddenSettingKeys.has(setting.key)),
)
: standardDefinitions
? buildDefinedSections(standardDefinitions, standardItems)
: []
const showLogs = section === 'logs'
const showMaintenance = section === 'maintenance'
const showRequestsExtras = section === 'requests'
const showArtworkExtras = section === 'cache'
const showArtworkExtras = section === 'artwork'
const showCacheExtras = section === 'cache'
const shouldRenderSection = (sectionGroup: { key: string; items?: AdminSetting[] }) => {
if (sectionGroup.items && sectionGroup.items.length > 0) return true
if (showArtworkExtras && sectionGroup.key === 'artwork') return true
if (showCacheExtras && sectionGroup.key === 'cache') return true
if (showRequestsExtras && sectionGroup.key === 'requests') return true
if (showRequestsExtras && sectionGroup.key === 'requests-sync') return true
return false
}
const renderedSettingsSections = settingsSections.filter(shouldRenderSection)
useEffect(() => {
requestsSyncRef.current = requestsSync
@@ -1632,10 +1770,6 @@ export default function SettingsPage({ section }: SettingsPageProps) {
<span>Maintenance job</span>
<strong>{maintenanceBusy ? 'Running' : 'Idle'}</strong>
</div>
<div className="cache-rail-metric">
<span>Live updates</span>
<strong>{liveStreamConnected ? 'Connected' : 'Polling'}</strong>
</div>
<div className="cache-rail-metric">
<span>Log lines in view</span>
<strong>{logsLines.length}</strong>
@@ -1649,7 +1783,6 @@ export default function SettingsPage({ section }: SettingsPageProps) {
</div>
) : undefined
const cacheRail = showCacheExtras ? (
<div className="admin-rail-stack">
<div className="admin-rail-card cache-rail-card">
<span className="admin-rail-eyebrow">Cache control</span>
<h2>Saved requests</h2>
@@ -1696,6 +1829,8 @@ export default function SettingsPage({ section }: SettingsPageProps) {
</button>
{cacheStatus && <div className="error-banner">{cacheStatus}</div>}
</div>
) : undefined
const artworkRail = showArtworkExtras ? (
<div className="admin-rail-card cache-rail-card">
<span className="admin-rail-eyebrow">Artwork</span>
<h2>Cache stats</h2>
@@ -1718,7 +1853,6 @@ export default function SettingsPage({ section }: SettingsPageProps) {
</div>
</div>
</div>
</div>
) : undefined
if (loading) {
@@ -1729,7 +1863,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
<AdminShell
title={SECTION_LABELS[section] ?? 'Settings'}
subtitle={SECTION_DESCRIPTIONS[section] ?? 'Manage settings.'}
rail={maintenanceRail ?? cacheRail}
rail={maintenanceRail ?? cacheRail ?? artworkRail}
actions={
<button type="button" onClick={() => router.push('/admin')}>
Back to settings
@@ -1770,27 +1904,39 @@ export default function SettingsPage({ section }: SettingsPageProps) {
</div>
</section>
) : null}
{settingsSections.length > 0 ? (
{renderedSettingsSections.length > 1 ? (
<nav className="config-subsection-nav" aria-label={`${SECTION_LABELS[section] ?? 'Settings'} subsections`}>
<span>On this page</span>
<div>
{renderedSettingsSections.map((sectionGroup, index) => (
<a key={sectionGroup.key} href={`#config-${sectionGroup.key}`}>
<small>{String(index + 1).padStart(2, '0')}</small>
{sectionGroup.title}
</a>
))}
</div>
</nav>
) : null}
{renderedSettingsSections.length > 0 ? (
<div className="admin-form admin-zone-stack">
{settingsSections
.filter(shouldRenderSection)
.map((sectionGroup) => (
<section key={sectionGroup.key} className="admin-section admin-zone">
{renderedSettingsSections.map((sectionGroup, sectionIndex) => (
<section id={`config-${sectionGroup.key}`} key={sectionGroup.key} className="admin-section admin-zone config-subsection">
<div className="section-header">
<h2>
{sectionGroup.key === 'requests' ? 'Request sync controls' : sectionGroup.title}
</h2>
{sectionGroup.key === 'sonarr' && (
<div className="config-subsection-heading">
<span className="section-kicker">Subsection {String(sectionIndex + 1).padStart(2, '0')}</span>
<h2>{sectionGroup.title}</h2>
</div>
{sectionGroup.key === 'sonarr-library' && (
<button type="button" onClick={() => loadOptions('sonarr')}>
Refresh Sonarr options
</button>
)}
{sectionGroup.key === 'radarr' && (
{sectionGroup.key === 'radarr-library' && (
<button type="button" onClick={() => loadOptions('radarr')}>
Refresh Radarr options
</button>
)}
{sectionGroup.key === 'jellyfin' && (
{sectionGroup.key === 'jellyfin-users' && (
<button type="button" onClick={syncJellyfinUsers}>
Import Jellyfin users
</button>
@@ -1809,7 +1955,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
</button>
</div>
) : null}
{showRequestsExtras && sectionGroup.key === 'requests' && (
{showRequestsExtras && sectionGroup.key === 'requests-sync' && (
<div className="sync-actions-block">
<div className="sync-actions">
<button type="button" onClick={syncRequests}>
@@ -1826,8 +1972,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
</div>
)}
</div>
{(sectionGroup.description || SECTION_DESCRIPTIONS[sectionGroup.key]) &&
(!settingsSection || isMagentGroupedSection || isSiteGroupedSection) && (
{(sectionGroup.description || SECTION_DESCRIPTIONS[sectionGroup.key]) && (
<p className="section-subtitle">
{sectionGroup.description || SECTION_DESCRIPTIONS[sectionGroup.key]}
</p>
@@ -1838,13 +1983,13 @@ export default function SettingsPage({ section }: SettingsPageProps) {
restarts may still be required before bind/port changes take effect.
</div>
)}
{sectionGroup.key === 'sonarr' && sonarrError && (
{sectionGroup.key === 'sonarr-library' && sonarrError && (
<div className="error-banner">{sonarrError}</div>
)}
{sectionGroup.key === 'radarr' && radarrError && (
{sectionGroup.key === 'radarr-library' && radarrError && (
<div className="error-banner">{radarrError}</div>
)}
{sectionGroup.key === 'jellyfin' && jellyfinSyncStatus && (
{sectionGroup.key === 'jellyfin-users' && jellyfinSyncStatus && (
<div className="status-banner">{jellyfinSyncStatus}</div>
)}
{showArtworkExtras && sectionGroup.key === 'artwork' && artworkPrefetchStatus && (
@@ -1879,10 +2024,10 @@ export default function SettingsPage({ section }: SettingsPageProps) {
</div>
</div>
)}
{showRequestsExtras && sectionGroup.key === 'requests' && requestsSyncStatus && (
{showRequestsExtras && sectionGroup.key === 'requests-sync' && requestsSyncStatus && (
<div className="status-banner">{requestsSyncStatus}</div>
)}
{showRequestsExtras && sectionGroup.key === 'requests' && (
{showRequestsExtras && sectionGroup.key === 'requests-sync' && (
<div className="status-banner">
Full refresh checks only decide when to run a full refresh. The delta sync interval
polls for new or updated requests.
@@ -1914,7 +2059,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {
{artworkPrefetch.message && <div className="meta">{artworkPrefetch.message}</div>}
</div>
)}
{showRequestsExtras && sectionGroup.key === 'requests' && requestsSync && (
{showRequestsExtras && sectionGroup.key === 'requests-sync' && requestsSync && (
<div className="sync-progress">
<div className="sync-meta">
<span>Status: {requestsSync.status}</span>
+60
View File
@@ -1126,6 +1126,66 @@ button:disabled,
gap: 16px;
}
.config-subsection-nav {
display: grid;
gap: 11px;
padding: 14px 16px;
border: 1px solid var(--ops-line);
border-radius: var(--ops-radius-lg);
background: rgba(255, 255, 255, 0.024);
}
.config-subsection-nav > span {
color: var(--ops-muted);
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.09em;
text-transform: uppercase;
}
.config-subsection-nav > div {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.config-subsection-nav a {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 36px;
padding: 7px 11px;
border: 1px solid var(--ops-line-soft);
border-radius: var(--ops-radius);
background: rgba(255, 255, 255, 0.022);
color: var(--ops-text);
font-size: 0.76rem;
font-weight: 700;
text-decoration: none;
transition: border-color 0.18s ease, background 0.18s ease, transform 0.18s ease;
}
.config-subsection-nav a:hover {
transform: translateY(-1px);
border-color: rgba(126, 215, 255, 0.38);
background: rgba(14, 165, 233, 0.09);
}
.config-subsection-nav a small {
color: var(--ops-cyan);
font-family: "JetBrains Mono", Consolas, monospace;
font-size: 0.64rem;
}
.config-subsection {
scroll-margin-top: 124px;
}
.config-subsection-heading {
display: grid;
gap: 4px;
}
.fleet-status-panel {
display: grid;
gap: 16px;
+21 -16
View File
@@ -4,18 +4,17 @@ import { usePathname } from 'next/navigation'
const NAV_GROUPS = [
{
title: 'Operations',
title: 'Configuration',
items: [
{ href: '/admin', label: 'Overview' },
{ href: '/admin/diagnostics', label: 'System health' },
{ href: '/new-requests', label: 'New requests' },
{ href: '/admin/issues', label: 'Issue tracking' },
{ href: '/admin', label: 'Config overview' },
{ href: '/admin/general', label: 'Application & proxy' },
{ href: '/admin/site', label: 'Site & login' },
{ href: '/admin/notifications', label: 'Notifications' },
],
},
{
title: 'Services',
title: 'Media Services',
items: [
{ href: '/admin/general', label: 'General' },
{ href: '/admin/seerr', label: 'Seerr' },
{ href: '/admin/jellyfin', label: 'Jellyfin' },
{ href: '/admin/sonarr', label: 'Sonarr' },
@@ -25,24 +24,28 @@ const NAV_GROUPS = [
],
},
{
title: 'Requests',
title: 'Request Pipeline',
items: [
{ href: '/admin/requests', label: 'Request sync' },
{ href: '/admin/requests-all', label: 'All requests' },
{ href: '/admin/cache', label: 'Cache Control' },
{ href: '/admin/requests', label: 'Sync & retention' },
{ href: '/admin/cache', label: 'Request cache' },
{ href: '/admin/artwork', label: 'Artwork cache' },
{ href: '/admin/requests-all', label: 'All requests' },
],
},
{
title: 'Admin',
title: 'Users & Access',
items: [
{ href: '/admin/notifications', label: 'Notifications' },
{ href: '/admin/system', label: 'How it works' },
{ href: '/admin/site', label: 'Site' },
{ href: '/users', label: 'Users' },
{ href: '/admin/invites', label: 'Invite management' },
],
},
{
title: 'System',
items: [
{ href: '/admin/diagnostics', label: 'System health' },
{ href: '/admin/logs', label: 'Activity log' },
{ href: '/admin/maintenance', label: 'Maintenance' },
{ href: '/admin/system', label: 'How it works' },
],
},
]
@@ -59,7 +62,9 @@ export default function AdminSidebar() {
{group.items.map((item) => {
const isActive =
pathname === item.href ||
(item.href !== '/' && pathname.startsWith(item.href))
(item.href !== '/' &&
item.href !== '/admin' &&
pathname.startsWith(`${item.href}/`))
return (
<a key={item.href} href={item.href} className={isActive ? 'is-active' : ''}>
{item.label}