From 05a3d1e3b0815543f467abd1b7d0764f5adf8a58 Mon Sep 17 00:00:00 2001 From: Rephl3x Date: Fri, 27 Feb 2026 13:17:50 +1300 Subject: [PATCH] admin docs and layout refresh, build 2702261314 --- backend/app/build_info.py | 2 +- frontend/app/admin/SettingsPage.tsx | 106 +++-- frontend/app/admin/invites/page.tsx | 323 +++++++++----- frontend/app/admin/system/page.tsx | 211 +++++++++ frontend/app/globals.css | 637 +++++++++++++++++++++++++++- frontend/app/how-it-works/page.tsx | 152 +++++-- frontend/app/ui/AdminShell.tsx | 13 +- frontend/app/ui/AdminSidebar.tsx | 4 +- frontend/app/users/page.tsx | 224 ++++++---- frontend/package.json | 2 +- 10 files changed, 1400 insertions(+), 274 deletions(-) create mode 100644 frontend/app/admin/system/page.tsx diff --git a/backend/app/build_info.py b/backend/app/build_info.py index 5a27354..e55a98a 100644 --- a/backend/app/build_info.py +++ b/backend/app/build_info.py @@ -1,4 +1,4 @@ -BUILD_NUMBER = "2702261153" +BUILD_NUMBER = "2702261314" CHANGELOG = '2026-01-22\\n- Initial commit\\n- Ignore build artifacts\\n- Update README\\n- Update README with Docker-first guide\\n\\n2026-01-23\\n- Fix cache titles via Jellyseerr media lookup\\n- Split search actions and improve download options\\n- Fallback manual grab to qBittorrent\\n- Hide header actions when signed out\\n- Add feedback form and webhook\\n- Fix cache titles and move feedback link\\n- Show available status on landing when in Jellyfin\\n- Add default branding assets when missing\\n- Use bundled branding assets\\n- Remove password fields from users page\\n- Add Docker Hub compose override\\n- Fix backend Dockerfile paths for root context\\n- Copy public assets into frontend image\\n- Use backend branding assets for logo and favicon\\n\\n2026-01-24\\n- Route grabs through Sonarr/Radarr only\\n- Document fix buttons in how-it-works\\n- Clarify how-it-works steps and fixes\\n- Map Prowlarr releases to Arr indexers for manual grab\\n- Improve request handling and qBittorrent categories\\n\\n2026-01-25\\n- Add site banner, build number, and changelog\\n- Automate build number tagging and sync\\n- Improve mobile header layout\\n- Move account actions into avatar menu\\n- Add user stats and activity tracking\\n- Add Jellyfin login cache and admin-only stats\\n- Tidy request sync controls\\n- Seed branding logo from bundled assets\\n- Serve bundled branding assets by default\\n- Harden request cache titles and cache-only reads\\n- Build 2501262041\\n\\n2026-01-26\\n- Fix cache title hydration\\n- Fix sync progress bar animation\\n\\n2026-01-27\\n- Add cache control artwork stats\\n- Improve cache stats performance (build 271261145)\\n- Fix backend cache stats import (build 271261149)\\n- Clarify request sync settings (build 271261159)\\n- Bump build number to 271261202\\n- Fix request titles in snapshots (build 271261219)\\n- Fix snapshot title fallback (build 271261228)\\n- Add cache load spinner (build 271261238)\\n- Bump build number (process 2) 271261322\\n- Add service test buttons (build 271261335)\\n- Fallback to TMDB when artwork cache fails (build 271261524)\\n- Hydrate missing artwork from Jellyseerr (build 271261539)\\n\\n2026-01-29\\n- release: 2901262036\\n- release: 2901262044\\n- release: 2901262102\\n- Hardcode build number in backend\\n- Bake build number and changelog\\n- Update full changelog\\n- Tidy full changelog\\n- Build 2901262240: cache users\n\n2026-01-30\n- Merge backend and frontend into one container' diff --git a/frontend/app/admin/SettingsPage.tsx b/frontend/app/admin/SettingsPage.tsx index 2b1ab5d..e13db1a 100644 --- a/frontend/app/admin/SettingsPage.tsx +++ b/frontend/app/admin/SettingsPage.tsx @@ -1262,6 +1262,86 @@ export default function SettingsPage({ section }: SettingsPageProps) { } } + const cacheSourceLabel = + formValues.requests_data_source === 'always_js' + ? 'Jellyseerr direct' + : formValues.requests_data_source === 'prefer_cache' + ? 'Saved requests only' + : 'Saved requests only' + const cacheTtlLabel = formValues.requests_sync_ttl_minutes || '60' + const cacheRail = showCacheExtras ? ( +
+
+ Cache control +

Saved requests

+

Load and inspect cached request entries from the right rail.

+
+
+ Data source + {cacheSourceLabel} +
+
+ Refresh TTL + {cacheTtlLabel} min +
+
+ Rows loaded + {cacheRows.length} +
+
+ Live updates + {liveStreamConnected ? 'Connected' : 'Polling'} +
+
+ + + {cacheStatus &&
{cacheStatus}
} +
+
+ Artwork +

Cache stats

+
+
+ Missing artwork + {artworkSummary?.missing_artwork ?? '--'} +
+
+ Cache size + {formatBytes(artworkSummary?.cache_bytes)} +
+
+ Cached files + {artworkSummary?.cache_files ?? '--'} +
+
+ Mode + {artworkSummary?.cache_mode ?? '--'} +
+
+
+
+ ) : undefined + if (loading) { return
Loading admin settings...
} @@ -1270,6 +1350,7 @@ export default function SettingsPage({ section }: SettingsPageProps) { router.push('/admin')}> Back to settings @@ -1893,32 +1974,7 @@ export default function SettingsPage({ section }: SettingsPageProps) {

Saved requests (cache)

-
- - -
- {cacheStatus &&
{cacheStatus}
}
Request diff --git a/frontend/app/admin/invites/page.tsx b/frontend/app/admin/invites/page.tsx index 4ad15d5..324cbce 100644 --- a/frontend/app/admin/invites/page.tsx +++ b/frontend/app/admin/invites/page.tsx @@ -70,6 +70,21 @@ type ProfileForm = { } type InviteManagementTab = 'bulk' | 'profiles' | 'invites' | 'trace' +type InviteTraceScope = 'all' | 'invited' | 'direct' +type InviteTraceView = 'list' | 'graph' + +type InviteTraceRow = { + username: string + role: string + authProvider: string + level: number + inviterUsername: string | null + inviteCode: string | null + inviteLabel: string | null + createdAt: string | null + childCount: number + isCycle?: boolean +} type InvitePolicy = { master_invite_id?: number | null @@ -105,6 +120,9 @@ const formatDate = (value?: string | null) => { return date.toLocaleString() } +const isInviteTraceRowInvited = (row: InviteTraceRow) => + Boolean(String(row.inviterUsername || '').trim() || String(row.inviteCode || '').trim()) + export default function AdminInviteManagementPage() { const router = useRouter() const [invites, setInvites] = useState([]) @@ -135,6 +153,8 @@ export default function AdminInviteManagementPage() { const [invitePolicy, setInvitePolicy] = useState(null) const [activeTab, setActiveTab] = useState('bulk') const [traceFilter, setTraceFilter] = useState('') + const [traceScope, setTraceScope] = useState('all') + const [traceView, setTraceView] = useState('graph') const signupBaseUrl = useMemo(() => { if (typeof window === 'undefined') return '/signup' @@ -698,74 +718,116 @@ export default function AdminInviteManagementPage() { ) }, [invites, traceFilter, users]) + const scopedInviteTraceRows = useMemo(() => { + if (traceScope === 'invited') return inviteTraceRows.filter((row) => isInviteTraceRowInvited(row)) + if (traceScope === 'direct') return inviteTraceRows.filter((row) => !isInviteTraceRowInvited(row)) + return inviteTraceRows + }, [inviteTraceRows, traceScope]) + + const traceInvitedCount = useMemo( + () => inviteTraceRows.filter((row) => isInviteTraceRowInvited(row)).length, + [inviteTraceRows] + ) + const traceDirectCount = inviteTraceRows.length - traceInvitedCount + + const inviteTraceGraphColumns = useMemo(() => { + if (scopedInviteTraceRows.length === 0) return [] as Array<{ level: number; rows: InviteTraceRow[] }> + + const minLevel = Math.min(...scopedInviteTraceRows.map((row) => row.level)) + const grouped = new Map() + scopedInviteTraceRows.forEach((row) => { + const level = Math.max(0, row.level - minLevel) + const bucket = grouped.get(level) ?? [] + bucket.push(row) + grouped.set(level, bucket) + }) + + return Array.from(grouped.entries()) + .sort((a, b) => a[0] - b[0]) + .map(([level, rows]) => ({ + level, + rows: [...rows].sort((a, b) => + String(a.username || '').localeCompare(String(b.username || ''), undefined, { + sensitivity: 'base', + }) + ), + })) + }, [scopedInviteTraceRows]) + + const inviteManagementRail = ( +
+
+
+
+ Overview +

Invite stats

+

Live counts for invites, profiles, and managed user defaults.

+
+
+
+
+ Invites +
+ {invites.length} + {usableInvites} usable • {disabledInvites} disabled +
+
+
+ Profiles +
+ {profiles.length} + {activeProfiles} active profiles +
+
+
+ Local non-admin accounts +
+ {nonAdminUsers.length} + {profiledUsers} with profile +
+
+
+ Jellyfin users +
+ {jellyfinUsersCount ?? '—'} + + {jellyfinUsersCount == null ? 'Unavailable/not configured' : 'Current Jellyfin user objects'} + +
+
+
+ Self-service invites +
+ {inviteAccessEnabledUsers} + + {masterInvite + ? `users enabled • master template ${masterInvite.code ?? `#${masterInvite.id}`}` + : 'users enabled • no master template set'} + +
+
+
+ Expiry rules +
+ {expiringUsers} + users with custom expiry +
+
+
+
+
+ ) + return (
{error &&
{error}
} {status &&
{status}
} -
-
-
-

Overview

-

- Quick counts for invite links, profiles, and managed user defaults. -

-
-
-
-
- Invites -
- {invites.length} - {usableInvites} usable • {disabledInvites} disabled -
-
-
- Profiles -
- {profiles.length} - {activeProfiles} active profiles -
-
-
- Local non-admin accounts -
- {nonAdminUsers.length} - {profiledUsers} with profile -
-
-
- Jellyfin users -
- {jellyfinUsersCount ?? '—'} - {jellyfinUsersCount == null ? 'Unavailable/not configured' : 'Current Jellyfin user objects'} -
-
-
- Self-service invites -
- {inviteAccessEnabledUsers} - - {masterInvite - ? `users enabled • master template ${masterInvite.code ?? `#${masterInvite.id}`}` - : 'users enabled • no master template set'} - -
-
-
- Expiry rules -
- {expiringUsers} - users with custom expiry -
-
-
-
-
-
-
-
-
-
- Invites -
-

- Create and manage signup links, assign profiles, and copy shareable URLs. -

-
-
- -
-
-
-
)} @@ -1410,19 +1425,105 @@ export default function AdminInviteManagementPage() { placeholder="Search by username, inviter, or invite code" /> +
+ +
+ + +
+
- {inviteTraceRows.length} rows shown + {scopedInviteTraceRows.length} rows shown + {traceInvitedCount} invited + {traceDirectCount} direct/root {users.length} users loaded {invites.length} invites loaded
{loading ? (
Loading trace map…
- ) : inviteTraceRows.length === 0 ? ( + ) : scopedInviteTraceRows.length === 0 ? (
No trace matches found.
+ ) : traceView === 'graph' ? ( +
+ {inviteTraceGraphColumns.map((column) => ( +
+
+ Level {column.level} + {column.rows.length} +
+
+ {column.rows.map((row) => ( +
+
+
+ {row.username} + {row.role} + {row.authProvider} + {row.isCycle && cycle} +
+

+ {row.inviterUsername + ? `\u2190 Invited by ${row.inviterUsername}` + : row.inviteCode + ? `\u2190 Invited via code ${row.inviteCode}` + : 'Direct/root account'} +

+
+
+ + Invite code + {row.inviteCode || 'None'} + + + Invite label + {row.inviteLabel || 'None'} + + + Children + {row.childCount} + + + Created + {formatDate(row.createdAt)} + +
+
+ ))} +
+
+ ))} +
) : (
- {inviteTraceRows.map((row) => ( + {scopedInviteTraceRows.map((row) => (
-

The pipeline in plain English

+

The pipeline (request to ready)

  1. - You request a title in Jellyseerr. + Request created in Jellyseerr.
  2. - Sonarr/Radarr adds it to the library list. + Approved and sent to Sonarr/Radarr.
  3. - Prowlarr looks for sources and sends results back. + Search runs against indexers via Prowlarr.
  4. - qBittorrent downloads the match. + Grabbed and downloaded by qBittorrent.
  5. - Sonarr/Radarr imports it into your library. + Imported by Sonarr/Radarr.
  6. - 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

+
    +
  1. + Invite created by admin or eligible user. +
  2. +
  3. + User signs up and Magent creates/links the account. +
  4. +
  5. + Profile/defaults apply (role, auto-search, expiry, invite access). +
  6. +
  7. + Admin trace map can show inviter → invited lineage. +
  8. +
+
+ +
+

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 = ( +
+
+
+
+

Directory summary

+

A quick view of user access and account state.

+
+
+
+
+
+ Total users + {users.length} +
+

{adminCount} admin accounts

+
+
+
+ Auto search + {autoSearchEnabledCount} +
+

of {nonAdminUsers.length} non-admin users enabled

+
+
+
+ Blocked + {blockedCount} +
+

+ {blockedCount ? 'Accounts currently blocked' : 'No blocked users'} +

+
+
+
+ Expired + {expiredCount} +
+

+ {expiredCount ? 'Accounts with expired access' : 'No expiries'} +

+
+
+
+
+ ) return ( - - - - - - } + rail={usersRail} >
- {error &&
{error}
} - {jellyseerrSyncStatus &&
{jellyseerrSyncStatus}
} -
-
- Total users - {users.length} - {adminCount} admin -
-
- Auto search - {autoSearchEnabledCount} - of {nonAdminUsers.length} non-admin users -
-
- Blocked - {blockedCount} - {blockedCount ? 'Needs review' : 'No blocked users'} -
-
- Expired - {expiredCount} - {expiredCount ? 'Access expired' : 'No expiries'} -
-
- -
-
-
-
-

Directory search

-

- Filter by username, role, login provider, or assigned profile. -

-
- {filteredCountLabel} -
-
-
- -
-
-
-
-
-
-

Bulk controls

-

- Auto search/download can be enabled or disabled for all non-admin users. -

-
-
-
-
- Auto search/download - - {autoSearchEnabledCount} of {nonAdminUsers.length} non-admin users enabled - -
-
- +
+
+
+ Directory actions +
+
+
+ Jellyseerr sync +
+ + +
+
+
+
+ {error &&
{error}
} + {jellyseerrSyncStatus &&
{jellyseerrSyncStatus}
} +
+
+
+

Bulk controls

+

+ Auto search/download can be enabled or disabled for all non-admin users. +

+
+
+
+
+ Auto search/download + + {autoSearchEnabledCount} of {nonAdminUsers.length} non-admin users enabled + +
+
+ + +
+
+
+
+
+
+

Directory search

+

+ Filter by username, role, login provider, or assigned profile. +

+
+ {filteredCountLabel} +
+
+
+ +
{filteredUsers.length === 0 ? ( diff --git a/frontend/package.json b/frontend/package.json index 35a3f8a..3ac8fe9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "magent-frontend", "private": true, - "version": "0202261541", + "version": "2702261314", "scripts": { "dev": "next dev", "build": "next build",