Build 2602260022: enterprise UI refresh and users bulk auto-search

This commit is contained in:
2026-02-26 00:23:41 +13:00
parent be7b899837
commit 9be0ec75ec
7 changed files with 1938 additions and 50 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -182,82 +182,92 @@ export default function UserDetailPage() {
) : (
<>
<div className="user-detail-card">
<div className="user-detail-header">
<div>
<strong>{user.username}</strong>
<div className="user-detail-meta">
<span className="meta">Jellyseerr ID: {user.jellyseerr_user_id ?? user.id ?? 'Unknown'}</span>
<span className="meta">Role: {user.role}</span>
<span className="meta">Login type: {user.auth_provider || 'local'}</span>
<span className="meta">Last login: {formatDateTime(user.last_login_at)}</span>
<div className="user-detail-layout">
<div className="user-detail-identity">
<div className="user-detail-title-row">
<strong className="user-detail-name">{user.username}</strong>
<span className={`user-grid-pill ${user.is_blocked ? 'is-blocked' : ''}`}>
{user.is_blocked ? 'Blocked' : 'Active'}
</span>
</div>
<div className="user-detail-meta-pills">
<span className="user-detail-chip">
Jellyseerr ID: {user.jellyseerr_user_id ?? user.id ?? 'Unknown'}
</span>
<span className="user-detail-chip">Role: {user.role}</span>
<span className="user-detail-chip">Login type: {user.auth_provider || 'local'}</span>
<span className="user-detail-chip">Last login: {formatDateTime(user.last_login_at)}</span>
</div>
</div>
<div className="user-actions">
<label className="toggle">
<input
type="checkbox"
checked={user.role === 'admin'}
onChange={(event) => updateUserRole(event.target.checked ? 'admin' : 'user')}
/>
<span>Make admin</span>
</label>
<label className="toggle">
<input
type="checkbox"
checked={Boolean(user.auto_search_enabled ?? true)}
disabled={user.role === 'admin'}
onChange={(event) => updateAutoSearchEnabled(event.target.checked)}
/>
<span>Allow auto search/download</span>
</label>
<button
type="button"
className="ghost-button"
onClick={() => toggleUserBlock(!user.is_blocked)}
>
{user.is_blocked ? 'Allow access' : 'Block access'}
</button>
<div className="user-detail-controls">
<div className="user-detail-controls-title">User controls</div>
<div className="user-detail-actions">
<label className="toggle">
<input
type="checkbox"
checked={user.role === 'admin'}
onChange={(event) => updateUserRole(event.target.checked ? 'admin' : 'user')}
/>
<span>Make admin</span>
</label>
<label className="toggle">
<input
type="checkbox"
checked={Boolean(user.auto_search_enabled ?? true)}
disabled={user.role === 'admin'}
onChange={(event) => updateAutoSearchEnabled(event.target.checked)}
/>
<span>Allow auto search/download</span>
</label>
<button
type="button"
className="ghost-button"
onClick={() => toggleUserBlock(!user.is_blocked)}
>
{user.is_blocked ? 'Allow access' : 'Block access'}
</button>
</div>
{user.role === 'admin' && (
<div className="user-detail-helper">
Admins always have auto search/download access.
</div>
)}
</div>
</div>
{user.role === 'admin' && (
<div className="user-detail-meta">
<span className="meta">Admins always have auto search/download access.</span>
</div>
)}
<div className="user-detail-grid">
<div>
<div className="user-detail-stat">
<span className="label">Total</span>
<span className="value">{stats?.total ?? 0}</span>
</div>
<div>
<div className="user-detail-stat">
<span className="label">Ready</span>
<span className="value">{stats?.ready ?? 0}</span>
</div>
<div>
<div className="user-detail-stat">
<span className="label">Pending</span>
<span className="value">{stats?.pending ?? 0}</span>
</div>
<div>
<div className="user-detail-stat">
<span className="label">Approved</span>
<span className="value">{stats?.approved ?? 0}</span>
</div>
<div>
<div className="user-detail-stat">
<span className="label">Working</span>
<span className="value">{stats?.working ?? 0}</span>
</div>
<div>
<div className="user-detail-stat">
<span className="label">Partial</span>
<span className="value">{stats?.partial ?? 0}</span>
</div>
<div>
<div className="user-detail-stat">
<span className="label">Declined</span>
<span className="value">{stats?.declined ?? 0}</span>
</div>
<div>
<div className="user-detail-stat">
<span className="label">In progress</span>
<span className="value">{stats?.in_progress ?? 0}</span>
</div>
<div>
<div className="user-detail-stat user-detail-stat--wide">
<span className="label">Last request</span>
<span className="value">{formatDateTime(stats?.last_request_at)}</span>
</div>

View File

@@ -13,6 +13,7 @@ type AdminUser = {
authProvider?: string | null
lastLoginAt?: string | null
isBlocked?: boolean
autoSearchEnabled?: boolean
stats?: UserStats
}
@@ -74,6 +75,7 @@ export default function UsersPage() {
const [jellyseerrSyncStatus, setJellyseerrSyncStatus] = useState<string | null>(null)
const [jellyseerrSyncBusy, setJellyseerrSyncBusy] = useState(false)
const [jellyseerrResyncBusy, setJellyseerrResyncBusy] = useState(false)
const [bulkAutoSearchBusy, setBulkAutoSearchBusy] = useState(false)
const loadUsers = async () => {
try {
@@ -100,6 +102,7 @@ export default function UsersPage() {
authProvider: user.auth_provider ?? 'local',
lastLoginAt: user.last_login_at ?? null,
isBlocked: Boolean(user.is_blocked),
autoSearchEnabled: Boolean(user.auto_search_enabled ?? true),
id: Number(user.id ?? 0),
stats: normalizeStats(user.stats ?? emptyStats),
}))
@@ -208,6 +211,33 @@ export default function UsersPage() {
}
}
const bulkUpdateAutoSearch = async (enabled: boolean) => {
setBulkAutoSearchBusy(true)
setJellyseerrSyncStatus(null)
try {
const baseUrl = getApiBase()
const response = await authFetch(`${baseUrl}/admin/users/auto-search/bulk`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled }),
})
if (!response.ok) {
const text = await response.text()
throw new Error(text || 'Bulk update failed')
}
const data = await response.json()
setJellyseerrSyncStatus(
`${enabled ? 'Enabled' : 'Disabled'} auto search/download for ${data?.updated ?? 0} non-admin users.`
)
await loadUsers()
} catch (err) {
console.error(err)
setError('Could not update auto search/download for all users.')
} finally {
setBulkAutoSearchBusy(false)
}
}
useEffect(() => {
if (!getToken()) {
router.push('/login')
@@ -220,6 +250,9 @@ export default function UsersPage() {
return <main className="card">Loading users...</main>
}
const nonAdminUsers = users.filter((user) => user.role !== 'admin')
const autoSearchEnabledCount = nonAdminUsers.filter((user) => user.autoSearchEnabled !== false).length
return (
<AdminShell
title="Users"
@@ -241,6 +274,31 @@ export default function UsersPage() {
<section className="admin-section">
{error && <div className="error-banner">{error}</div>}
{jellyseerrSyncStatus && <div className="status-banner">{jellyseerrSyncStatus}</div>}
<div className="user-bulk-toolbar">
<div className="user-bulk-summary">
<strong>Auto search/download</strong>
<span>
{autoSearchEnabledCount} of {nonAdminUsers.length} non-admin users enabled
</span>
</div>
<div className="user-bulk-actions">
<button
type="button"
onClick={() => bulkUpdateAutoSearch(true)}
disabled={bulkAutoSearchBusy}
>
{bulkAutoSearchBusy ? 'Working...' : 'Enable for all users'}
</button>
<button
type="button"
className="ghost-button"
onClick={() => bulkUpdateAutoSearch(false)}
disabled={bulkAutoSearchBusy}
>
{bulkAutoSearchBusy ? 'Working...' : 'Disable for all users'}
</button>
</div>
</div>
{users.length === 0 ? (
<div className="status-banner">No users found yet.</div>
) : (
@@ -260,6 +318,11 @@ export default function UsersPage() {
{user.isBlocked ? 'Blocked' : 'Active'}
</span>
</div>
<div className="user-grid-subpills">
<span className={`user-grid-pill ${user.autoSearchEnabled === false ? 'is-disabled' : ''}`}>
Auto search {user.autoSearchEnabled === false ? 'Off' : 'On'}
</span>
</div>
<div className="user-grid-stats">
<div>
<span className="label">Total</span>