Start Magent beta overhaul
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,33 @@
|
||||
import { notFound } from 'next/navigation'
|
||||
import SettingsPage from '../SettingsPage'
|
||||
|
||||
const ALLOWED_SECTIONS = new Set([
|
||||
'seerr',
|
||||
'jellyseerr',
|
||||
'jellyfin',
|
||||
'artwork',
|
||||
'sonarr',
|
||||
'radarr',
|
||||
'prowlarr',
|
||||
'qbittorrent',
|
||||
'requests',
|
||||
'cache',
|
||||
'logs',
|
||||
'maintenance',
|
||||
'magent',
|
||||
'general',
|
||||
'notifications',
|
||||
'site',
|
||||
])
|
||||
|
||||
type PageProps = {
|
||||
params: Promise<{ section: string }>
|
||||
}
|
||||
|
||||
export default async function AdminSectionPage({ params }: PageProps) {
|
||||
const { section } = await params
|
||||
if (!ALLOWED_SECTIONS.has(section)) {
|
||||
notFound()
|
||||
}
|
||||
return <SettingsPage section={section} />
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
'use client'
|
||||
|
||||
import AdminShell from '../../ui/AdminShell'
|
||||
import AdminDiagnosticsPanel from '../../ui/AdminDiagnosticsPanel'
|
||||
|
||||
export default function AdminDiagnosticsPage() {
|
||||
return (
|
||||
<AdminShell
|
||||
title="Diagnostics"
|
||||
subtitle="Run connectivity, delivery, and platform health checks for every configured dependency."
|
||||
rail={
|
||||
<div className="admin-rail-stack">
|
||||
<div className="admin-rail-card">
|
||||
<span className="admin-rail-eyebrow">Diagnostics</span>
|
||||
<h2>Shared console</h2>
|
||||
<p>
|
||||
This page and Maintenance now use the same diagnostics panel, so every test target and
|
||||
notification ping stays in one source of truth.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<AdminDiagnosticsPanel />
|
||||
</AdminShell>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
import PortalClient from '../../portal/PortalClient'
|
||||
|
||||
export default function AdminIssuesPage() {
|
||||
return <PortalClient workspace="issue" />
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { authFetch, clearToken, getApiBase, getToken } from '../lib/auth'
|
||||
import AdminShell from '../ui/AdminShell'
|
||||
|
||||
type ServiceState = {
|
||||
name: string
|
||||
status: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
type RecentRequest = {
|
||||
id: number
|
||||
title?: string | null
|
||||
year?: number | null
|
||||
statusLabel?: string | null
|
||||
requestedBy?: string | null
|
||||
createdAt?: string | null
|
||||
}
|
||||
|
||||
type PortalOverview = {
|
||||
overview?: {
|
||||
total_items?: number
|
||||
total_comments?: number
|
||||
by_kind?: Record<string, number>
|
||||
by_status?: Record<string, number>
|
||||
}
|
||||
my_items?: number
|
||||
}
|
||||
|
||||
const formatDateTime = (value?: string | null) => {
|
||||
if (!value) return 'Unknown'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.valueOf())) return value
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
const normalizeRecent = (items: any[]): RecentRequest[] =>
|
||||
items
|
||||
.filter((item) => item?.id)
|
||||
.map((item) => ({
|
||||
id: Number(item.id),
|
||||
title: item.title ?? null,
|
||||
year: item.year ?? null,
|
||||
statusLabel: item.statusLabel ?? null,
|
||||
requestedBy: item.requestedBy ?? null,
|
||||
createdAt: item.createdAt ?? null,
|
||||
}))
|
||||
|
||||
export default function AdminLandingPage() {
|
||||
const router = useRouter()
|
||||
const [services, setServices] = useState<ServiceState[]>([])
|
||||
const [serviceOverall, setServiceOverall] = useState('unknown')
|
||||
const [recent, setRecent] = useState<RecentRequest[]>([])
|
||||
const [portalOverview, setPortalOverview] = useState<PortalOverview | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!getToken()) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
|
||||
const load = async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const [meResponse, serviceResponse, recentResponse, overviewResponse] = await Promise.all([
|
||||
authFetch(`${baseUrl}/auth/me`),
|
||||
authFetch(`${baseUrl}/status/services`),
|
||||
authFetch(`${baseUrl}/requests/recent?take=8&days=0`),
|
||||
authFetch(`${baseUrl}/portal/overview`),
|
||||
])
|
||||
|
||||
if (meResponse.status === 401) {
|
||||
clearToken()
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
if (meResponse.status === 403) {
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
const me = await meResponse.json()
|
||||
if (me?.role !== 'admin') {
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
|
||||
if (serviceResponse.ok) {
|
||||
const data = await serviceResponse.json()
|
||||
setServiceOverall(data?.overall ?? 'unknown')
|
||||
setServices(Array.isArray(data?.services) ? data.services : [])
|
||||
}
|
||||
|
||||
if (recentResponse.ok) {
|
||||
const data = await recentResponse.json()
|
||||
setRecent(Array.isArray(data?.results) ? normalizeRecent(data.results) : [])
|
||||
}
|
||||
|
||||
if (overviewResponse.ok) {
|
||||
const data = await overviewResponse.json()
|
||||
setPortalOverview(data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError('Unable to load the operations dashboard.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
void load()
|
||||
}, [router])
|
||||
|
||||
const serviceCounts = useMemo(() => {
|
||||
const up = services.filter((service) => service.status === 'up').length
|
||||
const down = services.filter((service) => service.status === 'down').length
|
||||
const degraded = services.filter((service) => service.status === 'degraded').length
|
||||
const notConfigured = services.filter((service) => service.status === 'not_configured').length
|
||||
return { up, down, degraded, notConfigured, total: services.length }
|
||||
}, [services])
|
||||
|
||||
const issueCount = Number(portalOverview?.overview?.by_kind?.issue ?? 0)
|
||||
const requestItemCount = Number(portalOverview?.overview?.by_kind?.request ?? 0)
|
||||
const commentCount = Number(portalOverview?.overview?.total_comments ?? 0)
|
||||
|
||||
const rail = (
|
||||
<div className="admin-rail-stack">
|
||||
<div className="admin-rail-card">
|
||||
<span className="admin-rail-eyebrow">Service ecosystem</span>
|
||||
<div className="service-ecosystem">
|
||||
{services.length === 0 ? (
|
||||
<div className="status-banner">Service status is not available yet.</div>
|
||||
) : (
|
||||
services.map((service) => (
|
||||
<a
|
||||
key={service.name}
|
||||
className="service-row"
|
||||
href={`/admin/${service.name.toLowerCase().replace(/[^a-z0-9]/g, '')}`}
|
||||
>
|
||||
<span className={`system-dot system-dot-${service.status}`} />
|
||||
<span>
|
||||
<strong>{service.name}</strong>
|
||||
<small>{service.message ?? 'No message reported'}</small>
|
||||
</span>
|
||||
<span className={`small-pill system-pill-${service.status}`}>{service.status}</span>
|
||||
</a>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-rail-card">
|
||||
<span className="admin-rail-eyebrow">Quick actions</span>
|
||||
<div className="quick-action-grid">
|
||||
<a href="/admin/requests-all">Review requests</a>
|
||||
<a href="/admin/issues">Manage issues</a>
|
||||
<a href="/users">User directory</a>
|
||||
<a href="/admin/logs">Activity log</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<AdminShell
|
||||
title="Operations Center"
|
||||
subtitle="Live Magent controls, request movement, issue intake, and service health."
|
||||
rail={rail}
|
||||
actions={
|
||||
<button type="button" onClick={() => router.push('/')}>
|
||||
View health
|
||||
</button>
|
||||
}
|
||||
>
|
||||
{loading ? <div className="status-banner">Loading operations dashboard...</div> : null}
|
||||
{error ? <div className="error-banner">{error}</div> : null}
|
||||
|
||||
<section className="ops-metric-grid">
|
||||
<div className="ops-metric-card">
|
||||
<span className="section-kicker">Services online</span>
|
||||
<strong>
|
||||
{serviceCounts.up}/{serviceCounts.total || 0}
|
||||
</strong>
|
||||
<p>{serviceOverall === 'up' ? 'All configured services are responding.' : 'Some services need review.'}</p>
|
||||
</div>
|
||||
<div className="ops-metric-card">
|
||||
<span className="section-kicker">Recent requests</span>
|
||||
<strong>{recent.length}</strong>
|
||||
<p>Loaded from the live request cache.</p>
|
||||
</div>
|
||||
<div className="ops-metric-card">
|
||||
<span className="section-kicker">Open issue items</span>
|
||||
<strong>{issueCount}</strong>
|
||||
<p>{commentCount} portal comments recorded.</p>
|
||||
</div>
|
||||
<div className="ops-metric-card">
|
||||
<span className="section-kicker">Portal requests</span>
|
||||
<strong>{requestItemCount}</strong>
|
||||
<p>Tracked in the dedicated request workflow.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="admin-zone">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Recent activity</h2>
|
||||
<p className="section-subtitle">Live request cache entries, newest first.</p>
|
||||
</div>
|
||||
</div>
|
||||
{recent.length === 0 ? (
|
||||
<div className="status-banner">No recent requests were returned.</div>
|
||||
) : (
|
||||
<div className="admin-table dashboard-activity-table">
|
||||
<div className="admin-table-head">
|
||||
<span>Request</span>
|
||||
<span>Status</span>
|
||||
<span>User</span>
|
||||
<span>Created</span>
|
||||
</div>
|
||||
{recent.map((row) => (
|
||||
<button
|
||||
key={row.id}
|
||||
type="button"
|
||||
className="admin-table-row"
|
||||
onClick={() => router.push(`/requests/${row.id}`)}
|
||||
>
|
||||
<span>
|
||||
{row.title || `Request #${row.id}`}
|
||||
{row.year ? ` (${row.year})` : ''}
|
||||
</span>
|
||||
<span>{row.statusLabel || 'Unknown'}</span>
|
||||
<span>{row.requestedBy || 'Unknown'}</span>
|
||||
<span>{formatDateTime(row.createdAt)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="admin-zone">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
<h2>Attention states</h2>
|
||||
<p className="section-subtitle">Service states that affect request processing.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ops-status-strip">
|
||||
<span>{serviceCounts.down} down</span>
|
||||
<span>{serviceCounts.degraded} degraded</span>
|
||||
<span>{serviceCounts.notConfigured} not configured</span>
|
||||
</div>
|
||||
</section>
|
||||
</AdminShell>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
export default function AdminProfilesRedirectPage() {
|
||||
redirect('/admin/invites')
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { authFetch, clearToken, getApiBase, getToken } from '../../lib/auth'
|
||||
import AdminShell from '../../ui/AdminShell'
|
||||
|
||||
type RequestRow = {
|
||||
id: number
|
||||
title?: string | null
|
||||
year?: number | null
|
||||
type?: string | null
|
||||
statusLabel?: string | null
|
||||
requestedBy?: string | null
|
||||
createdAt?: string | null
|
||||
}
|
||||
|
||||
const REQUEST_STAGE_OPTIONS = [
|
||||
{ value: 'all', label: 'All stages' },
|
||||
{ value: 'pending', label: 'Waiting for approval' },
|
||||
{ value: 'approved', label: 'Approved' },
|
||||
{ value: 'in_progress', label: 'In progress' },
|
||||
{ value: 'working', label: 'Working on it' },
|
||||
{ value: 'partial', label: 'Partially ready' },
|
||||
{ value: 'ready', label: 'Ready to watch' },
|
||||
{ value: 'declined', label: 'Declined' },
|
||||
]
|
||||
|
||||
const formatDateTime = (value?: string | null) => {
|
||||
if (!value) return 'Unknown'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.valueOf())) return value
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
export default function AdminRequestsAllPage() {
|
||||
const router = useRouter()
|
||||
const [rows, setRows] = useState<RequestRow[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
const [page, setPage] = useState(1)
|
||||
const [stage, setStage] = useState('all')
|
||||
|
||||
const pageCount = useMemo(() => {
|
||||
if (!total || pageSize <= 0) return 1
|
||||
return Math.max(1, Math.ceil(total / pageSize))
|
||||
}, [total, pageSize])
|
||||
|
||||
const load = async () => {
|
||||
if (!getToken()) {
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const baseUrl = getApiBase()
|
||||
const skip = (page - 1) * pageSize
|
||||
const params = new URLSearchParams({
|
||||
take: String(pageSize),
|
||||
skip: String(skip),
|
||||
})
|
||||
if (stage !== 'all') {
|
||||
params.set('stage', stage)
|
||||
}
|
||||
const response = await authFetch(
|
||||
`${baseUrl}/admin/requests/all?${params.toString()}`
|
||||
)
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
clearToken()
|
||||
router.push('/login')
|
||||
return
|
||||
}
|
||||
if (response.status === 403) {
|
||||
router.push('/')
|
||||
return
|
||||
}
|
||||
throw new Error(`Load failed: ${response.status}`)
|
||||
}
|
||||
const data = await response.json()
|
||||
setRows(Array.isArray(data?.results) ? data.results : [])
|
||||
setTotal(Number(data?.total ?? 0))
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setError('Unable to load requests.')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void load()
|
||||
}, [page, pageSize, stage])
|
||||
|
||||
useEffect(() => {
|
||||
if (page > pageCount) {
|
||||
setPage(pageCount)
|
||||
}
|
||||
}, [pageCount, page])
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1)
|
||||
}, [stage])
|
||||
|
||||
return (
|
||||
<AdminShell
|
||||
title="All requests"
|
||||
subtitle="Paginated view of every cached request."
|
||||
actions={
|
||||
<button type="button" onClick={() => router.push('/admin')}>
|
||||
Back to settings
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<section className="admin-section">
|
||||
<div className="admin-toolbar">
|
||||
<div className="admin-toolbar-info">
|
||||
<span>{total.toLocaleString()} total</span>
|
||||
</div>
|
||||
<div className="admin-toolbar-actions">
|
||||
<label className="admin-select">
|
||||
<span>Stage</span>
|
||||
<select value={stage} onChange={(e) => setStage(e.target.value)}>
|
||||
{REQUEST_STAGE_OPTIONS.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="admin-select">
|
||||
<span>Per page</span>
|
||||
<select value={pageSize} onChange={(e) => setPageSize(Number(e.target.value))}>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={200}>200</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="status-banner">Loading requests…</div>
|
||||
) : error ? (
|
||||
<div className="error-banner">{error}</div>
|
||||
) : rows.length === 0 ? (
|
||||
<div className="status-banner">No requests found.</div>
|
||||
) : (
|
||||
<div className="admin-table">
|
||||
<div className="admin-table-head">
|
||||
<span>Request</span>
|
||||
<span>Status</span>
|
||||
<span>Requested by</span>
|
||||
<span>Created</span>
|
||||
</div>
|
||||
{rows.map((row) => (
|
||||
<button
|
||||
key={row.id}
|
||||
type="button"
|
||||
className="admin-table-row"
|
||||
onClick={() => router.push(`/requests/${row.id}`)}
|
||||
>
|
||||
<span>
|
||||
{row.title || `Request #${row.id}`}
|
||||
{row.year ? ` (${row.year})` : ''}
|
||||
</span>
|
||||
<span>{row.statusLabel || 'Unknown'}</span>
|
||||
<span>{row.requestedBy || 'Unknown'}</span>
|
||||
<span>{formatDateTime(row.createdAt)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="admin-pagination">
|
||||
<button type="button" onClick={() => setPage(1)} disabled={page <= 1}>
|
||||
First
|
||||
</button>
|
||||
<button type="button" onClick={() => setPage(page - 1)} disabled={page <= 1}>
|
||||
Previous
|
||||
</button>
|
||||
<span>
|
||||
Page {page} of {pageCount}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPage(page + 1)}
|
||||
disabled={page >= pageCount}
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPage(pageCount)}
|
||||
disabled={page >= pageCount}
|
||||
>
|
||||
Last
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</AdminShell>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,304 @@
|
||||
'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: 'Seerr 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 <main className="card">Loading system guide...</main>
|
||||
}
|
||||
|
||||
if (!authorized) {
|
||||
return null
|
||||
}
|
||||
|
||||
const rail = (
|
||||
<div className="admin-rail-stack">
|
||||
<div className="admin-rail-card">
|
||||
<span className="admin-rail-eyebrow">How it works</span>
|
||||
<h2>Admin flow map</h2>
|
||||
<p>Identity → Request intake → Queue orchestration → Download → Import → Playback.</p>
|
||||
<span className="small-pill">Admin only</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<AdminShell
|
||||
title="How it works"
|
||||
subtitle="Admin-only service wiring, control areas, and recovery flow for Magent."
|
||||
rail={rail}
|
||||
actions={
|
||||
<button type="button" onClick={() => router.push('/admin')}>
|
||||
Back to settings
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<section className="admin-section system-guide">
|
||||
<div className="admin-panel">
|
||||
<h2>End-to-end system flow</h2>
|
||||
<p className="lede">
|
||||
This is the runtime path the platform follows from authentication through to playback
|
||||
availability.
|
||||
</p>
|
||||
<div className="system-flow-track">
|
||||
{REQUEST_FLOW.map((stage, index) => (
|
||||
<div key={stage.title} className="system-flow-segment">
|
||||
<article className="system-flow-card">
|
||||
<div className="system-flow-card-title">{index + 1}. {stage.title}</div>
|
||||
<div className="system-flow-card-row">
|
||||
<span>Input</span>
|
||||
<strong>{stage.input}</strong>
|
||||
</div>
|
||||
<div className="system-flow-card-row">
|
||||
<span>Action</span>
|
||||
<strong>{stage.action}</strong>
|
||||
</div>
|
||||
<div className="system-flow-card-row">
|
||||
<span>Output</span>
|
||||
<strong>{stage.output}</strong>
|
||||
</div>
|
||||
</article>
|
||||
{index < REQUEST_FLOW.length - 1 && <div className="system-flow-arrow" aria-hidden="true">→</div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-panel">
|
||||
<h2>What each service is responsible for</h2>
|
||||
<div className="system-guide-grid">
|
||||
<article className="system-guide-card">
|
||||
<h3>Magent</h3>
|
||||
<p>
|
||||
Handles authentication, request pages, live event updates, invite workflows,
|
||||
diagnostics, notifications, and admin operations.
|
||||
</p>
|
||||
</article>
|
||||
<article className="system-guide-card">
|
||||
<h3>Seerr</h3>
|
||||
<p>
|
||||
Stores the request itself and remains the request-state source for approval and
|
||||
media request metadata.
|
||||
</p>
|
||||
</article>
|
||||
<article className="system-guide-card">
|
||||
<h3>Jellyfin</h3>
|
||||
<p>
|
||||
Provides user sign-in identity and the final playback destination once content is
|
||||
available.
|
||||
</p>
|
||||
</article>
|
||||
<article className="system-guide-card">
|
||||
<h3>Sonarr / Radarr</h3>
|
||||
<p>
|
||||
Control queue placement, quality-profile decisions, import handling, and release
|
||||
monitoring.
|
||||
</p>
|
||||
</article>
|
||||
<article className="system-guide-card">
|
||||
<h3>Prowlarr</h3>
|
||||
<p>Provides search/indexer coverage for Arr-side release searches.</p>
|
||||
</article>
|
||||
<article className="system-guide-card">
|
||||
<h3>qBittorrent</h3>
|
||||
<p>
|
||||
Executes the download and exposes live progress, paused states, and queue
|
||||
visibility.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-panel">
|
||||
<h2>Operational controls by area</h2>
|
||||
<div className="system-guide-grid">
|
||||
<article className="system-guide-card">
|
||||
<h3>General</h3>
|
||||
<p>Application URL, API URL, ports, bind host, proxy base URL, and manual SSL settings.</p>
|
||||
</article>
|
||||
<article className="system-guide-card">
|
||||
<h3>Notifications</h3>
|
||||
<p>Email, Discord, Telegram, push/mobile, and generic webhook delivery channels.</p>
|
||||
</article>
|
||||
<article className="system-guide-card">
|
||||
<h3>Users</h3>
|
||||
<p>Role/profile/expiry, auto-search access, invite access, and cross-system ban/remove actions.</p>
|
||||
</article>
|
||||
<article className="system-guide-card">
|
||||
<h3>Invite management</h3>
|
||||
<p>
|
||||
Master template, profile assignment, invite access policy, invite emails, and trace
|
||||
map lineage.
|
||||
</p>
|
||||
</article>
|
||||
<article className="system-guide-card">
|
||||
<h3>Requests + cache</h3>
|
||||
<p>All-requests view, sync controls, cached request records, and maintenance operations.</p>
|
||||
</article>
|
||||
<article className="system-guide-card">
|
||||
<h3>Maintenance + diagnostics</h3>
|
||||
<p>
|
||||
Connectivity checks, live diagnostics, database repair, cleanup, log review, and
|
||||
nuclear flush/resync operations.
|
||||
</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="admin-panel">
|
||||
<h2>User and invite model</h2>
|
||||
<ol className="system-decision-list">
|
||||
<li>
|
||||
Jellyfin is used for sign-in identity and user presence across the platform.
|
||||
</li>
|
||||
<li>
|
||||
Seerr provides request ownership and request-state data for Magent request pages.
|
||||
</li>
|
||||
<li>
|
||||
Invite links, invite profiles, blanket rules, and invite-access controls are managed
|
||||
inside Magent.
|
||||
</li>
|
||||
<li>
|
||||
If invite tracing is enabled, the lineage view shows who invited whom and how the
|
||||
chain branches.
|
||||
</li>
|
||||
<li>
|
||||
Cross-system removal and ban flows are initiated from Magent admin controls.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="admin-panel">
|
||||
<h2>Stall recovery path (decision flow)</h2>
|
||||
<ol className="system-decision-list">
|
||||
<li>
|
||||
Request approved but not in Arr queue <span>→</span> run <strong>Re-add to Arr</strong>.
|
||||
</li>
|
||||
<li>
|
||||
In queue but no release found <span>→</span> run <strong>Search releases</strong> and inspect options.
|
||||
</li>
|
||||
<li>
|
||||
Release exists and user should not pick manually <span>→</span> run <strong>Search + auto-download</strong>.
|
||||
</li>
|
||||
<li>
|
||||
Download paused/stalled in qBittorrent <span>→</span> run <strong>Resume download</strong>.
|
||||
</li>
|
||||
<li>
|
||||
Imported but not visible to user <span>→</span> validate Jellyfin visibility/link from request page.
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div className="admin-panel">
|
||||
<h2>Live update surfaces</h2>
|
||||
<div className="system-guide-grid">
|
||||
<article className="system-guide-card">
|
||||
<h3>Landing page</h3>
|
||||
<p>Recent requests and service summaries refresh live for signed-in users.</p>
|
||||
</article>
|
||||
<article className="system-guide-card">
|
||||
<h3>Request pages</h3>
|
||||
<p>Timeline state, queue activity, and torrent progress are pushed live without refresh.</p>
|
||||
</article>
|
||||
<article className="system-guide-card">
|
||||
<h3>Admin views</h3>
|
||||
<p>Diagnostics, logs, sync state, and maintenance surfaces stream live operational data.</p>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</AdminShell>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user