diff --git a/frontend/app/admin/system/page.tsx b/frontend/app/admin/system/page.tsx
new file mode 100644
index 0000000..5c9d596
--- /dev/null
+++ b/frontend/app/admin/system/page.tsx
@@ -0,0 +1,211 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { useRouter } from 'next/navigation'
+import AdminShell from '../../ui/AdminShell'
+import { authFetch, clearToken, getApiBase, getToken } from '../../lib/auth'
+
+type FlowStage = {
+ title: string
+ input: string
+ action: string
+ output: string
+}
+
+const REQUEST_FLOW: FlowStage[] = [
+ {
+ title: 'Identity + access',
+ input: 'Jellyfin/local login',
+ action: 'Magent validates credentials and role',
+ output: 'JWT token + user scope',
+ },
+ {
+ title: 'Request intake',
+ input: 'Jellyseerr request ID',
+ action: 'Magent snapshots request + media metadata',
+ output: 'Unified request state',
+ },
+ {
+ title: 'Queue orchestration',
+ input: 'Approved request',
+ action: 'Sonarr/Radarr add/search operations',
+ output: 'Grab decision',
+ },
+ {
+ title: 'Download execution',
+ input: 'Selected release',
+ action: 'qBittorrent downloads + reports progress',
+ output: 'Import-ready payload',
+ },
+ {
+ title: 'Library import',
+ input: 'Completed download',
+ action: 'Sonarr/Radarr import and finalize',
+ output: 'Available media object',
+ },
+ {
+ title: 'Playback availability',
+ input: 'Imported media',
+ action: 'Jellyfin refresh + link resolution',
+ output: 'Ready-to-watch state',
+ },
+]
+
+export default function AdminSystemGuidePage() {
+ const router = useRouter()
+ const [loading, setLoading] = useState(true)
+ const [authorized, setAuthorized] = useState(false)
+
+ useEffect(() => {
+ let active = true
+ const load = async () => {
+ if (!getToken()) {
+ router.push('/login')
+ return
+ }
+ try {
+ const baseUrl = getApiBase()
+ const response = await authFetch(`${baseUrl}/auth/me`)
+ if (!response.ok) {
+ if (response.status === 401) {
+ clearToken()
+ router.push('/login')
+ return
+ }
+ router.push('/')
+ return
+ }
+ const me = await response.json()
+ if (!active) return
+ if (me?.role !== 'admin') {
+ router.push('/')
+ return
+ }
+ setAuthorized(true)
+ } catch (error) {
+ console.error(error)
+ router.push('/')
+ } finally {
+ if (active) setLoading(false)
+ }
+ }
+ void load()
+ return () => {
+ active = false
+ }
+ }, [router])
+
+ if (loading) {
+ return
Loading system guide...
+ }
+
+ if (!authorized) {
+ return null
+ }
+
+ const rail = (
+
+
+
Guide map
+
Quick path
+
Identity → Intake → Queue → Download → Import → Playback.
+
Admin only
+
+
+ )
+
+ return (
+
router.push('/admin')}>
+ Back to settings
+
+ }
+ >
+
+
+
End-to-end system flow
+
+ This is the exact runtime path for request processing and availability in the current build.
+
+
+ {REQUEST_FLOW.map((stage, index) => (
+
+
+ {index + 1}. {stage.title}
+
+ Input
+ {stage.input}
+
+
+ Action
+ {stage.action}
+
+
+ Output
+ {stage.output}
+
+
+ {index < REQUEST_FLOW.length - 1 &&
→
}
+
+ ))}
+
+
+
+
+
Operational controls by area
+
+
+ General
+ Application URL, API URL, ports, bind host, proxy base URL, and manual SSL settings.
+
+
+ Notifications
+ Email, Discord, Telegram, push/mobile, and generic webhook delivery channels.
+
+
+ Users
+ Role/profile/expiry, auto-search access, invite access, and cross-system ban/remove actions.
+
+
+ Invite management
+ Master template, profile assignment, invite access policy, and invite trace map lineage.
+
+
+ Requests + cache
+ All-requests view, sync controls, cached request records, and maintenance operations.
+
+
+ Live request page
+ Event-stream updates for state, action history, and torrent progress without page refresh.
+
+
+
+
+
+
Stall recovery path (decision flow)
+
+ -
+ Request approved but not in Arr queue → run Re-add to Arr.
+
+ -
+ In queue but no release found → run Search releases and inspect options.
+
+ -
+ Release exists and user should not pick manually → run Search + auto-download.
+
+ -
+ Download paused/stalled in qBittorrent → run Resume download.
+
+ -
+ Imported but not visible to user → validate Jellyfin visibility/link from request page.
+
+
+
+
+
+ )
+}
diff --git a/frontend/app/globals.css b/frontend/app/globals.css
index 63fdad8..591c592 100644
--- a/frontend/app/globals.css
+++ b/frontend/app/globals.css
@@ -4193,7 +4193,7 @@ button:hover:not(:disabled) {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 10px;
- align-items: end;
+ align-items: flex-end;
}
.user-directory-search {
@@ -4531,19 +4531,22 @@ button:hover:not(:disabled) {
.invite-admin-tabbar {
display: grid;
- grid-template-columns: 1fr;
- align-items: start;
- gap: 8px;
+ grid-template-columns: minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 10px 12px;
margin-bottom: 12px;
}
.invite-admin-tabbar .admin-segmented {
margin-bottom: 0;
+ width: max-content;
+ max-width: 100%;
}
.invite-admin-tab-actions {
- width: 100%;
+ width: auto;
justify-content: flex-end;
+ align-self: center;
}
.invite-admin-stack {
@@ -4551,6 +4554,11 @@ button:hover:not(:disabled) {
gap: 12px;
}
+.invite-admin-bulk-panel .user-bulk-groups {
+ display: grid;
+ gap: 10px;
+}
+
.invite-admin-list-panel,
.invite-admin-form-panel {
width: 100%;
@@ -4709,6 +4717,7 @@ button:hover:not(:disabled) {
}
.invite-admin-tabbar {
+ grid-template-columns: 1fr;
align-items: stretch;
}
@@ -4761,12 +4770,16 @@ textarea {
.invite-trace-toolbar {
display: grid;
grid-template-columns: minmax(260px, 1fr) auto;
+ grid-template-areas:
+ 'filter controls'
+ 'summary summary';
gap: 10px 14px;
- align-items: end;
+ align-items: flex-end;
margin-bottom: 10px;
}
.invite-trace-filter {
+ grid-area: filter;
display: grid;
gap: 6px;
}
@@ -4779,6 +4792,7 @@ textarea {
}
.invite-trace-summary {
+ grid-area: summary;
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
@@ -4794,11 +4808,164 @@ textarea {
border-radius: 5px;
}
+.invite-trace-controls {
+ grid-area: controls;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ align-items: flex-end;
+ gap: 10px;
+}
+
+.invite-trace-scope {
+ display: grid;
+ gap: 6px;
+ min-width: 190px;
+}
+
+.invite-trace-scope > span {
+ color: #9ea7b6;
+ font-size: 0.76rem;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+
+.invite-trace-view-toggle {
+ display: inline-flex;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: 5px;
+ overflow: hidden;
+}
+
+.invite-trace-view-toggle button {
+ min-width: 90px;
+ border: 0;
+ border-right: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 0;
+ background: transparent;
+ color: #aeb7c4;
+ padding: 7px 12px;
+}
+
+.invite-trace-view-toggle button:last-child {
+ border-right: 0;
+}
+
+.invite-trace-view-toggle button.is-active {
+ background: rgba(111, 148, 224, 0.22);
+ color: #eef3fb;
+ font-weight: 700;
+}
+
.invite-trace-map {
display: grid;
gap: 8px;
}
+.invite-trace-graph {
+ display: grid;
+ grid-auto-flow: column;
+ grid-auto-columns: minmax(260px, 1fr);
+ align-items: start;
+ gap: 10px;
+ overflow-x: auto;
+ padding-bottom: 2px;
+}
+
+.invite-trace-column {
+ display: grid;
+ gap: 8px;
+ align-content: start;
+ min-width: 260px;
+}
+
+.invite-trace-column-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 8px 10px;
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ background: rgba(255, 255, 255, 0.015);
+ border-radius: 5px;
+ color: #b5c0d2;
+ font-size: 0.8rem;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+
+.invite-trace-column-header strong {
+ color: #edf2f8;
+ font-size: 0.86rem;
+}
+
+.invite-trace-column-body {
+ display: grid;
+ gap: 8px;
+}
+
+.invite-trace-node {
+ display: grid;
+ gap: 8px;
+ padding: 10px;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ background: rgba(255, 255, 255, 0.018);
+ border-radius: 6px;
+}
+
+.invite-trace-node-main {
+ display: grid;
+ gap: 6px;
+}
+
+.invite-trace-node-title {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px;
+}
+
+.invite-trace-node-arrow {
+ margin: 0;
+ color: #c4cfdd;
+ font-size: 0.82rem;
+ letter-spacing: 0.01em;
+}
+
+.invite-trace-node-arrow.is-root {
+ color: #95a2b5;
+}
+
+.invite-trace-node-meta {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 6px;
+}
+
+.invite-trace-node-meta-item {
+ display: grid;
+ gap: 2px;
+ align-content: start;
+ min-height: 44px;
+ padding: 6px 8px;
+ border: 1px solid rgba(255, 255, 255, 0.04);
+ background: rgba(255, 255, 255, 0.01);
+ border-radius: 5px;
+}
+
+.invite-trace-node-meta-item .label {
+ color: #8f9aac;
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+
+.invite-trace-node-meta-item strong {
+ color: #e7edf6;
+ font-size: 0.81rem;
+ word-break: break-word;
+}
+
.invite-trace-row {
display: grid;
grid-template-columns: minmax(260px, 420px) minmax(0, 1fr);
@@ -4876,13 +5043,30 @@ textarea {
@media (max-width: 1180px) {
.invite-trace-toolbar {
grid-template-columns: 1fr;
+ grid-template-areas:
+ 'filter'
+ 'controls'
+ 'summary';
align-items: stretch;
}
+ .invite-trace-controls {
+ justify-content: flex-start;
+ }
+
.invite-trace-summary {
justify-content: flex-start;
}
+ .invite-trace-graph {
+ grid-auto-flow: row;
+ grid-auto-columns: 1fr;
+ }
+
+ .invite-trace-column {
+ min-width: 0;
+ }
+
.invite-trace-row {
grid-template-columns: 1fr;
}
@@ -4893,6 +5077,10 @@ textarea {
}
@media (max-width: 720px) {
+ .invite-trace-node-meta {
+ grid-template-columns: 1fr;
+ }
+
.invite-trace-row-meta {
grid-template-columns: 1fr;
}
@@ -4969,6 +5157,119 @@ textarea {
color: #e7edf6;
}
+/* Admin system guide */
+.system-guide {
+ display: grid;
+ gap: 12px;
+}
+
+.system-flow-track {
+ display: grid;
+ gap: 10px;
+ margin-top: 8px;
+}
+
+.system-flow-segment {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 10px;
+ align-items: center;
+}
+
+.system-flow-card {
+ display: grid;
+ gap: 7px;
+ padding: 11px 12px;
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: 6px;
+}
+
+.system-flow-card-title {
+ color: #edf2f8;
+ font-weight: 700;
+ letter-spacing: 0.01em;
+}
+
+.system-flow-card-row {
+ display: grid;
+ grid-template-columns: 86px minmax(0, 1fr);
+ gap: 8px;
+ align-items: start;
+}
+
+.system-flow-card-row span {
+ color: #8f9aac;
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+}
+
+.system-flow-card-row strong {
+ color: #dfe8f5;
+ font-size: 0.86rem;
+ font-weight: 600;
+}
+
+.system-flow-arrow {
+ color: #7ea1d8;
+ font-size: 1.2rem;
+ font-weight: 700;
+ line-height: 1;
+}
+
+.system-guide-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+ margin-top: 8px;
+}
+
+.system-guide-card {
+ padding: 11px 12px;
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ background: rgba(255, 255, 255, 0.02);
+ border-radius: 6px;
+ display: grid;
+ gap: 6px;
+}
+
+.system-guide-card h3 {
+ color: #eef3fb;
+ font-size: 0.97rem;
+}
+
+.system-guide-card p {
+ color: #a7b2c2;
+ margin: 0;
+}
+
+.system-decision-list {
+ list-style: none;
+ margin: 8px 0 0;
+ padding: 0;
+ display: grid;
+ gap: 7px;
+}
+
+.system-decision-list li {
+ padding: 9px 10px;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ background: rgba(255, 255, 255, 0.015);
+ border-radius: 6px;
+ color: #d3dce9;
+}
+
+.system-decision-list li span {
+ color: #7ea1d8;
+ font-weight: 700;
+ margin: 0 5px;
+}
+
+.system-decision-list li strong {
+ color: #eff5ff;
+}
+
@media (max-width: 980px) {
.profile-invites-layout {
grid-template-columns: 1fr;
@@ -4979,6 +5280,287 @@ textarea {
grid-column: 1 / -1;
}
+/* Admin shell right rail */
+.admin-shell {
+ grid-template-columns: minmax(220px, 260px) minmax(0, 1fr) minmax(300px, 380px);
+ gap: 22px;
+ align-items: start;
+}
+
+.admin-shell-nav {
+ grid-column: 1;
+}
+
+.admin-card {
+ grid-column: 2;
+ min-width: 0;
+}
+
+.admin-shell-rail {
+ grid-column: 3;
+ position: sticky;
+ top: 20px;
+ align-self: start;
+ display: grid;
+ gap: 10px;
+ min-width: 0;
+}
+
+.admin-rail-stack {
+ display: grid;
+ gap: 10px;
+}
+
+.admin-rail-card {
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ background: rgba(255, 255, 255, 0.016);
+ border-radius: 8px;
+ padding: 12px;
+ display: grid;
+ gap: 8px;
+ min-width: 0;
+}
+
+.admin-rail-card h2 {
+ margin: 0;
+ font-size: 1rem;
+}
+
+.admin-rail-card p {
+ margin: 0;
+ color: #9ba5b5;
+}
+
+.admin-rail-eyebrow {
+ font-size: 0.72rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: #9ba5b5;
+ font-weight: 700;
+}
+
+.admin-shell-rail .invite-admin-summary-row {
+ grid-template-columns: 1fr;
+ align-items: start;
+}
+
+.admin-shell-rail .invite-admin-summary-row__value {
+ justify-content: space-between;
+}
+
+.cache-rail-card {
+ gap: 10px;
+}
+
+.cache-rail-metrics {
+ display: grid;
+ gap: 8px;
+}
+
+.cache-rail-metric {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 10px;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ background: rgba(255, 255, 255, 0.012);
+ padding: 8px 10px;
+ border-radius: 6px;
+}
+
+.cache-rail-metric span {
+ color: #9aa4b4;
+ font-size: 0.78rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+.cache-rail-metric strong {
+ color: #eef3f9;
+ font-size: 0.92rem;
+ text-align: right;
+ overflow-wrap: anywhere;
+}
+
+.cache-rail-limit {
+ display: grid;
+ gap: 6px;
+}
+
+.cache-rail-limit > span {
+ color: #9aa4b4;
+ font-size: 0.78rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+}
+
+/* Users page streamline pass */
+.users-page-toolbar {
+ margin-bottom: 12px;
+}
+
+.users-page-toolbar-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.users-page-toolbar-group {
+ display: grid;
+ gap: 8px;
+ min-width: 0;
+ padding: 10px;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ background: rgba(255, 255, 255, 0.016);
+ border-radius: 6px;
+}
+
+.users-page-toolbar-label {
+ font-size: 0.72rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: #9ba5b5;
+ font-weight: 700;
+}
+
+.users-page-toolbar-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ align-items: center;
+}
+
+.users-page-toolbar-actions button {
+ white-space: nowrap;
+}
+
+.users-page-overview-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.2fr) minmax(320px, 0.8fr);
+ gap: 12px;
+ margin: 12px 0;
+ align-items: start;
+}
+
+.users-summary-panel {
+ display: grid;
+ gap: 10px;
+}
+
+.users-rail-summary .users-summary-grid {
+ grid-template-columns: 1fr;
+}
+
+.users-summary-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.users-summary-card {
+ min-width: 0;
+ display: grid;
+ gap: 4px;
+ padding: 10px 12px;
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ background: rgba(255, 255, 255, 0.014);
+ border-radius: 6px;
+}
+
+.users-summary-row {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.users-summary-label {
+ color: #a9b3c2;
+ font-size: 0.85rem;
+ font-weight: 600;
+}
+
+.users-summary-value {
+ color: #edf3fb;
+ font-size: 1.12rem;
+ line-height: 1;
+ font-weight: 700;
+}
+
+.users-summary-meta {
+ margin: 0;
+ color: #98a3b4;
+ font-size: 0.78rem;
+ line-height: 1.35;
+}
+
+.user-directory-search-panel {
+ margin-bottom: 12px;
+}
+
+.user-directory-bulk-panel .user-bulk-toolbar {
+ grid-template-columns: minmax(0, 1fr);
+ align-items: stretch;
+ gap: 10px;
+}
+
+.user-directory-bulk-panel .user-bulk-summary {
+ display: grid;
+ gap: 4px;
+ align-content: start;
+ min-width: 0;
+}
+
+.user-directory-bulk-panel .user-bulk-summary strong {
+ line-height: 1.32;
+ overflow-wrap: anywhere;
+}
+
+.user-directory-bulk-panel .user-bulk-actions {
+ align-self: start;
+ justify-content: flex-start;
+}
+
+.user-directory-bulk-panel .user-bulk-actions button {
+ min-width: 190px;
+}
+
+@media (max-width: 1400px) {
+ .admin-shell {
+ grid-template-columns: minmax(210px, 250px) minmax(0, 1fr) minmax(270px, 320px);
+ }
+}
+
+@media (max-width: 980px) {
+ .admin-shell {
+ grid-template-columns: 1fr;
+ }
+
+ .admin-shell-nav,
+ .admin-card,
+ .admin-shell-rail {
+ grid-column: 1;
+ }
+
+ .users-page-toolbar-grid,
+ .users-summary-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .users-page-toolbar-actions button {
+ flex: 1 1 220px;
+ }
+
+ .user-directory-bulk-panel .user-bulk-actions {
+ width: 100%;
+ }
+
+ .user-directory-bulk-panel .user-bulk-actions button {
+ width: 100%;
+ min-width: 0;
+ }
+}
+
/* Final header account menu stacking override (must be last) */
.page,
.header,
@@ -5022,3 +5604,46 @@ textarea {
position: absolute !important;
z-index: 5000 !important;
}
+
+/* Final width scaling */
+.page {
+ width: min(1680px, calc(100vw - 32px));
+ max-width: 1680px;
+ padding-inline: 16px;
+}
+
+@media (max-width: 1280px) {
+ .page {
+ width: min(1480px, calc(100vw - 24px));
+ max-width: 1480px;
+ padding-inline: 12px;
+ }
+
+ .admin-shell {
+ grid-template-columns: minmax(200px, 240px) minmax(0, 1fr);
+ }
+
+ .admin-shell-rail {
+ grid-column: 2;
+ position: static;
+ top: auto;
+ }
+}
+
+@media (max-width: 980px) {
+ .page {
+ width: min(100%, calc(100vw - 12px));
+ max-width: none;
+ padding-inline: 6px;
+ }
+
+ .admin-shell {
+ grid-template-columns: 1fr;
+ }
+
+ .admin-shell-nav,
+ .admin-card,
+ .admin-shell-rail {
+ grid-column: 1;
+ }
+}
diff --git a/frontend/app/how-it-works/page.tsx b/frontend/app/how-it-works/page.tsx
index 5b131fe..0a81982 100644
--- a/frontend/app/how-it-works/page.tsx
+++ b/frontend/app/how-it-works/page.tsx
@@ -5,10 +5,10 @@ export default function HowItWorksPage() {
How this works
- Your request, step by step
+ How Magent works now
- Magent is a friendly status checker. It looks at a few helper apps, then shows you where
- your request is and what you can safely do next.
+ End-to-end request flow, live status updates, and the exact tools available to users and
+ admins.
@@ -52,90 +52,172 @@ export default function HowItWorksPage() {
- The pipeline in plain English
+ The pipeline (request to ready)
-
- You request a title in Jellyseerr.
+ Request created in Jellyseerr.
-
- Sonarr/Radarr adds it to the library list.
+ Approved and sent to Sonarr/Radarr.
-
- Prowlarr looks for sources and sends results back.
+ Search runs against indexers via Prowlarr.
-
- qBittorrent downloads the match.
+ Grabbed and downloaded by qBittorrent.
-
- Sonarr/Radarr imports it into your library.
+ Imported by Sonarr/Radarr.
-
- Jellyfin shows it when it is ready to watch.
+ Available in Jellyfin.
- Steps and fixes (simple and visual)
+ Live updates (no refresh needed)
+
+
+ 1
+ Request page updates in real time
+
+ Status, timeline hops, and action history update automatically while you are viewing
+ the request.
+
+
+
+ 2
+ Download progress updates live
+
+ Torrent progress, queue state, and downloader details refresh automatically so users
+ do not need to hard refresh.
+
+
+
+ 3
+ Ready state appears as soon as import finishes
+
+ As soon as Sonarr/Radarr import completes and Jellyfin can serve it, the request page
+ shows it as ready.
+
+
+
+
+
+
+ Request actions and when to use them
1
- Request sent
- Jellyseerr holds your request and approval.
- Fixes you can try
+ Re-add to Arr
+ Use when a request is approved but never entered the Arr queue.
+ Best for
- - Add to library queue (if it was approved but never added)
+ - Missing NEEDS_ADD / ADDED state transitions
+ - Queue repair after Arr-side cleanup
2
- Added to the library list
- Sonarr/Radarr decide what quality to get.
- Fixes you can try
+ Search releases
+ Runs a search and shows concrete release options.
+ Best for
- - Search for releases (see options)
- - Search and auto-download (let it pick for you)
+ - Manual selection of a specific release/indexer
+ - Checking whether results currently exist
3
- Searching for sources
- Prowlarr checks your torrent providers.
- Fixes you can try
+ Search + auto-download
+ Runs search and lets Arr pick/grab automatically.
+ Best for
- - Search for releases (show a list to choose)
+ - Fast recovery when users have auto-search access
+ - Hands-off retry of stalled requests
4
- Downloading the file
- qBittorrent downloads the selected match.
- Fixes you can try
+ Resume download
+ Resumes a paused/stopped torrent in qBittorrent.
+ Best for
- - Resume download (only if it already exists there)
+ - Paused queue entries
+ - Downloader restarts
5
- Ready to watch
- Jellyfin shows it in your library.
- What to do next
+ Open in Jellyfin
+ Available when the item is imported and linked to Jellyfin.
+ Best for
- - Open in Jellyfin (watch it)
+ - Immediate playback confirmation
+ - User handoff from request tracking to watching
+
+ Invite and account flow
+
+ -
+ Invite created by admin or eligible user.
+
+ -
+ User signs up and Magent creates/links the account.
+
+ -
+ Profile/defaults apply (role, auto-search, expiry, invite access).
+
+ -
+ Admin trace map can show inviter → invited lineage.
+
+
+
+
+
+ Admin controls available
+
+
+ General
+ App URL/port, API URL/port, bind host, proxy base URL, and manual SSL bind options.
+
+
+ Notifications
+ Email, Discord, Telegram, push/mobile, and generic webhook provider settings.
+
+
+ Users
+ Bulk auto-search control, invite access control, per-user roles/profile/expiry, and system actions.
+
+
+ Invite management
+ Profiles, invites, blanket rules, master template, and trace map (list/graph with lineage).
+
+
+ Request sync + cache
+ Control refresh/sync behavior, view all requests, and manage cached request records.
+
+
+ Maintenance + logs
+ Run cleanup/sync tasks, inspect operations, and diagnose pipeline issues quickly.
+
+
+
+
- Why Magent sometimes says "waiting"
+ Why a request can still wait
- If the search helper cannot find a match yet, Magent will say there is nothing to grab.
- That does not mean it is broken. It usually means the release is not available yet.
+ If indexers do not return a valid release yet, Magent will show waiting/search states.
+ That usually means content availability is the blocker, not a broken pipeline.
diff --git a/frontend/app/ui/AdminShell.tsx b/frontend/app/ui/AdminShell.tsx
index 9aa240e..f195cf2 100644
--- a/frontend/app/ui/AdminShell.tsx
+++ b/frontend/app/ui/AdminShell.tsx
@@ -7,10 +7,11 @@ type AdminShellProps = {
title: string
subtitle?: string
actions?: ReactNode
+ rail?: ReactNode
children: ReactNode
}
-export default function AdminShell({ title, subtitle, actions, children }: AdminShellProps) {
+export default function AdminShell({ title, subtitle, actions, rail, children }: AdminShellProps) {
return (
{children}
+
)
}
diff --git a/frontend/app/ui/AdminSidebar.tsx b/frontend/app/ui/AdminSidebar.tsx
index 41fc318..1547805 100644
--- a/frontend/app/ui/AdminSidebar.tsx
+++ b/frontend/app/ui/AdminSidebar.tsx
@@ -6,7 +6,7 @@ const NAV_GROUPS = [
{
title: 'Services',
items: [
- { href: '/admin/magent', label: 'Magent' },
+ { href: '/admin/general', label: 'General' },
{ href: '/admin/jellyseerr', label: 'Jellyseerr' },
{ href: '/admin/jellyfin', label: 'Jellyfin' },
{ href: '/admin/sonarr', label: 'Sonarr' },
@@ -26,8 +26,8 @@ const NAV_GROUPS = [
{
title: 'Admin',
items: [
- { href: '/admin/general', label: 'General' },
{ href: '/admin/notifications', label: 'Notifications' },
+ { href: '/admin/system', label: 'System guide' },
{ href: '/admin/site', label: 'Site' },
{ href: '/users', label: 'Users' },
{ href: '/admin/invites', label: 'Invite management' },
diff --git a/frontend/app/users/page.tsx b/frontend/app/users/page.tsx
index 762179c..6cd9357 100644
--- a/frontend/app/users/page.tsx
+++ b/frontend/app/users/page.tsx
@@ -250,112 +250,152 @@ export default function UsersPage() {
filteredUsers.length === users.length
? `${users.length} users`
: `${filteredUsers.length} of ${users.length} users`
+ const usersRail = (
+