From 9be0ec75ec7371aa5594ea8393175fcf1cea44f9 Mon Sep 17 00:00:00 2001 From: Rephl3x Date: Thu, 26 Feb 2026 00:23:41 +1300 Subject: [PATCH] Build 2602260022: enterprise UI refresh and users bulk auto-search --- .build_number | 2 +- backend/app/build_info.py | 2 +- backend/app/db.py | 11 + backend/app/routers/admin.py | 15 + frontend/app/globals.css | 1791 +++++++++++++++++++++++++++++- frontend/app/users/[id]/page.tsx | 104 +- frontend/app/users/page.tsx | 63 ++ 7 files changed, 1938 insertions(+), 50 deletions(-) diff --git a/.build_number b/.build_number index a6dc9ee..6a90235 100644 --- a/.build_number +++ b/.build_number @@ -1 +1 @@ -2502262321 +2602260022 diff --git a/backend/app/build_info.py b/backend/app/build_info.py index e22aa88..a213b51 100644 --- a/backend/app/build_info.py +++ b/backend/app/build_info.py @@ -1,2 +1,2 @@ -BUILD_NUMBER = "2502262321" +BUILD_NUMBER = "2602260022" 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/backend/app/db.py b/backend/app/db.py index a994a6d..ede28db 100644 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -569,6 +569,17 @@ def set_user_auto_search_enabled(username: str, enabled: bool) -> None: ) +def set_auto_search_enabled_for_non_admin_users(enabled: bool) -> int: + with _connect() as conn: + cursor = conn.execute( + """ + UPDATE users SET auto_search_enabled = ? WHERE role != 'admin' + """, + (1 if enabled else 0,), + ) + return cursor.rowcount + + def verify_user_password(username: str, password: str) -> Optional[Dict[str, Any]]: user = get_user_by_username(username) if not user: diff --git a/backend/app/routers/admin.py b/backend/app/routers/admin.py index 320ec4e..db20644 100644 --- a/backend/app/routers/admin.py +++ b/backend/app/routers/admin.py @@ -25,6 +25,7 @@ from ..db import ( set_setting, set_user_blocked, set_user_auto_search_enabled, + set_auto_search_enabled_for_non_admin_users, set_user_password, set_user_role, run_integrity_check, @@ -673,6 +674,20 @@ async def update_user_auto_search(username: str, payload: Dict[str, Any]) -> Dic return {"status": "ok", "username": username, "auto_search_enabled": enabled} +@router.post("/users/auto-search/bulk") +async def update_users_auto_search_bulk(payload: Dict[str, Any]) -> Dict[str, Any]: + enabled = payload.get("enabled") if isinstance(payload, dict) else None + if not isinstance(enabled, bool): + raise HTTPException(status_code=400, detail="enabled must be true or false") + updated = set_auto_search_enabled_for_non_admin_users(enabled) + return { + "status": "ok", + "enabled": enabled, + "updated": updated, + "scope": "non-admin-users", + } + + @router.post("/users/{username}/password") async def update_user_password(username: str, payload: Dict[str, Any]) -> Dict[str, Any]: new_password = payload.get("password") if isinstance(payload, dict) else None diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 384e710..22358e0 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;600&display=swap'); :root { color-scheme: light; @@ -1213,6 +1213,18 @@ button span { background: rgba(255, 82, 82, 0.2); } +.user-grid-pill.is-disabled { + border-color: rgba(255, 200, 87, 0.45); + background: rgba(255, 200, 87, 0.16); + color: var(--ink-muted); +} + +.user-grid-subpills { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + .user-grid-stats { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -1258,12 +1270,135 @@ button span { flex-wrap: wrap; } +.user-bulk-toolbar { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 14px; + align-items: center; + padding: 14px 16px; + border-radius: 14px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.04); + margin-bottom: 14px; +} + +.user-bulk-summary { + display: grid; + gap: 4px; +} + +.user-bulk-summary strong { + font-size: 14px; +} + +.user-bulk-summary span { + font-size: 13px; + color: var(--ink-muted); +} + +.user-bulk-actions { + display: flex; + gap: 10px; + flex-wrap: wrap; + justify-content: flex-end; +} + +.user-detail-layout { + display: grid; + grid-template-columns: minmax(0, 1.6fr) minmax(260px, 360px); + gap: 14px; + align-items: start; +} + +.user-detail-identity { + display: grid; + gap: 12px; + min-width: 0; +} + +.user-detail-title-row { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 10px; + flex-wrap: wrap; +} + +.user-detail-name { + font-size: 24px; + line-height: 1.1; +} + +.user-detail-meta-pills { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.user-detail-chip { + display: inline-flex; + align-items: center; + min-height: 30px; + padding: 6px 10px; + border-radius: 999px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.03); + color: var(--ink-muted); + font-size: 13px; + line-height: 1.2; +} + +.user-detail-controls { + display: grid; + gap: 10px; + padding: 14px; + border-radius: 14px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.03); +} + +.user-detail-controls-title { + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink-muted); +} + +.user-detail-actions { + display: grid; + gap: 10px; + justify-items: start; +} + +.user-detail-actions .ghost-button { + width: 100%; +} + +.user-detail-helper { + font-size: 12px; + color: var(--ink-muted); + line-height: 1.35; +} + .user-detail-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; } +.user-detail-stat { + display: grid; + gap: 4px; + padding: 10px 12px; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.06); + background: rgba(255, 255, 255, 0.02); +} + +.user-detail-stat--wide { + grid-column: 1 / -1; +} + .user-detail-grid .label { font-size: 12px; color: var(--ink-muted); @@ -1275,6 +1410,24 @@ button span { font-weight: 600; } +@media (max-width: 980px) { + .user-bulk-toolbar { + grid-template-columns: 1fr; + } + + .user-bulk-actions { + justify-content: flex-start; + } + + .user-detail-layout { + grid-template-columns: 1fr; + } + + .user-detail-actions .ghost-button { + width: auto; + } +} + .label-row { display: flex; justify-content: space-between; @@ -2106,3 +2259,1639 @@ button span { color: var(--ink-muted); font-size: 15px; } + +/* -------------------------------------------------------------------------- */ +/* Professional UI Refresh (graphite / silver / black + subtle blue accents) */ +/* -------------------------------------------------------------------------- */ + +:root { + --ink: #10151d; + --ink-muted: #5b6472; + --paper: #eaedf1; + --paper-strong: #f8fafc; + --accent: #3f78d7; + --accent-2: #5ea0ff; + --accent-3: #8fa7c8; + --border: rgba(16, 21, 29, 0.1); + --shadow: rgba(16, 21, 29, 0.14); + --glow: 0 0 0 transparent; + --input-bg: rgba(16, 21, 29, 0.03); + --error-bg: rgba(185, 28, 28, 0.08); + --error-ink: #7f1d1d; +} + +[data-theme='dark'] { + --ink: #edf1f7; + --ink-muted: #98a2b3; + --paper: #090c10; + --paper-strong: #11151b; + --accent: #4b7fdb; + --accent-2: #66a3ff; + --accent-3: #93a6c4; + --border: rgba(255, 255, 255, 0.07); + --shadow: rgba(0, 0, 0, 0.45); + --glow: 0 0 0 transparent; + --input-bg: rgba(255, 255, 255, 0.035); + --error-bg: rgba(248, 113, 113, 0.12); + --error-ink: #fecaca; +} + +body { + font-family: "Manrope", "Segoe UI", sans-serif; + background: + radial-gradient(800px 380px at 10% -8%, rgba(102, 163, 255, 0.09), transparent 60%), + radial-gradient(700px 340px at 88% 0%, rgba(149, 176, 214, 0.07), transparent 58%), + linear-gradient(180deg, #090b0f 0%, #07090d 100%); + letter-spacing: 0.005em; +} + +[data-theme='light'] body { + background: + radial-gradient(700px 320px at 10% -10%, rgba(102, 163, 255, 0.12), transparent 60%), + linear-gradient(180deg, #f2f4f7 0%, #e9edf3 100%); +} + +body::before { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + opacity: 0.08; + background-image: + linear-gradient(rgba(255, 255, 255, 0.045) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.045) 1px, transparent 1px); + background-size: 24px 24px; + z-index: 0; +} + +.page { + position: relative; + z-index: 1; + max-width: 1200px; + gap: 24px; +} + +.header { + padding: 18px 20px; + border-radius: 18px; + border: 1px solid var(--border); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.025), rgba(255, 255, 255, 0.01)), + rgba(13, 17, 23, 0.7); + backdrop-filter: blur(14px); + box-shadow: 0 12px 28px rgba(0, 0, 0, 0.18); +} + +[data-theme='light'] .header { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.82), rgba(255, 255, 255, 0.7)), + rgba(248, 250, 252, 0.9); +} + +.brand { + font-size: 30px; + letter-spacing: 0.06em; + font-weight: 800; +} + +.tagline { + font-size: 15px; + color: var(--ink-muted); +} + +h1 { + font-size: 34px; + font-weight: 800; + letter-spacing: -0.02em; +} + +h2 { + font-size: 21px; + font-weight: 700; + letter-spacing: -0.01em; +} + +h3 { + font-size: 17px; + font-weight: 700; +} + +.lede { + color: var(--ink-muted); + font-size: 16px; +} + +.header-actions a, +.header-actions .header-link { + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + border-radius: 12px; + padding: 8px 14px; + box-shadow: none; + backdrop-filter: none; +} + +.header-actions a:hover, +.header-actions .header-link:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(102, 163, 255, 0.25); +} + +.header-actions .header-cta { + background: linear-gradient(180deg, rgba(78, 133, 224, 0.95), rgba(61, 112, 196, 0.95)); + color: #f7fbff; + border: 1px solid rgba(102, 163, 255, 0.45); + box-shadow: 0 8px 18px rgba(62, 109, 190, 0.22); +} + +.avatar-button { + width: 42px; + height: 42px; + border-radius: 12px; + border: 1px solid rgba(102, 163, 255, 0.22); + background: linear-gradient(180deg, rgba(78, 133, 224, 0.16), rgba(255, 255, 255, 0.02)); + box-shadow: none; +} + +.theme-toggle { + width: 40px; + height: 40px; + border-radius: 12px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + box-shadow: none; +} + +.theme-toggle:hover, +.avatar-button:hover { + border-color: rgba(102, 163, 255, 0.28); + background: rgba(255, 255, 255, 0.05); +} + +.signed-in-dropdown { + border-radius: 14px; + background: rgba(12, 15, 20, 0.96); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 18px 30px rgba(0, 0, 0, 0.35); +} + +[data-theme='light'] .signed-in-dropdown { + background: rgba(250, 252, 255, 0.98); +} + +.signed-in-actions a, +.signed-in-signout { + border-radius: 10px; + background: rgba(255, 255, 255, 0.03); +} + +.signed-in-actions a:hover, +.signed-in-signout:hover { + background: rgba(255, 255, 255, 0.07); +} + +.card { + border-radius: 18px; + border: 1px solid var(--border); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.018), rgba(255, 255, 255, 0.008)), + rgba(17, 21, 27, 0.84); + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.2); + padding: 26px; + animation: none; +} + +[data-theme='light'] .card { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.88), rgba(255, 255, 255, 0.78)), + rgba(248, 250, 252, 0.95); + box-shadow: 0 12px 24px rgba(15, 23, 42, 0.08); +} + +input, +select, +textarea { + border-radius: 12px; + border: 1px solid var(--border); + background: rgba(255, 255, 255, 0.02); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.03); +} + +input:focus, +select:focus, +textarea:focus { + outline: none; + border-color: rgba(102, 163, 255, 0.42); + box-shadow: + 0 0 0 3px rgba(102, 163, 255, 0.12), + inset 0 1px 0 rgba(255, 255, 255, 0.04); +} + +button { + border-radius: 12px; + background: linear-gradient(180deg, rgba(74, 123, 210, 0.96), rgba(61, 103, 176, 0.96)); + border: 1px solid rgba(102, 163, 255, 0.35); + box-shadow: 0 8px 16px rgba(62, 104, 179, 0.2); + font-weight: 600; + letter-spacing: 0.01em; +} + +button:hover:not(:disabled) { + filter: brightness(1.03); + transform: translateY(-1px); +} + +button:disabled { + opacity: 0.65; + cursor: not-allowed; +} + +.ghost-button, +.details-toggle button, +.recent-grid button, +.admin-pagination button, +.system-test { + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); + box-shadow: none; + color: var(--ink); +} + +.ghost-button:hover:not(:disabled), +.details-toggle button:hover:not(:disabled), +.recent-grid button:hover:not(:disabled), +.system-test:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.055); + border-color: rgba(102, 163, 255, 0.24); +} + +.danger-button { + background: linear-gradient(180deg, #c93f3f, #9f3030); + border-color: rgba(255, 143, 143, 0.25); + box-shadow: 0 8px 16px rgba(156, 46, 46, 0.22); +} + +.site-banner, +.status-banner, +.state, +.action-message, +.sync-progress, +.system-status, +.pipeline-map, +.summary-card, +.status-box, +.timeline-card, +.connection-item, +.stat-card, +.how-card, +.how-step-card, +.schedule-card, +.log-viewer, +.cache-row, +.filters-compact, +.settings-nav, +.user-grid-card, +.user-detail-card, +.user-detail-controls, +.user-bulk-toolbar, +.system-item { + background: rgba(255, 255, 255, 0.025); + border-color: var(--border); + box-shadow: none; +} + +.status-box, +.summary-card, +.timeline-card, +.system-status, +.pipeline-map { + border-radius: 16px; +} + +.timeline::before { + background: linear-gradient(180deg, rgba(102, 163, 255, 0.5), rgba(102, 163, 255, 0.08)); +} + +.timeline-marker, +.timeline-item.is-active .timeline-marker { + background: var(--accent-2); + box-shadow: 0 0 0 4px rgba(102, 163, 255, 0.14); +} + +.admin-shell { + gap: 20px; + grid-template-columns: minmax(220px, 250px) minmax(0, 1fr); +} + +.admin-sidebar { + padding: 14px; + gap: 14px; + border-radius: 16px; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.025), rgba(255, 255, 255, 0.008)), + rgba(15, 18, 24, 0.85); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18); +} + +[data-theme='light'] .admin-sidebar { + background: rgba(255, 255, 255, 0.78); +} + +.admin-sidebar-title, +.admin-nav-title, +.settings-title, +.eyebrow { + color: #8e98a9; + letter-spacing: 0.12em; +} + +.admin-nav-links a { + border-radius: 10px; + padding: 10px 12px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid transparent; +} + +.admin-nav-links a:hover { + border-color: rgba(102, 163, 255, 0.18); + background: rgba(255, 255, 255, 0.04); +} + +.admin-nav-links a.is-active { + background: + linear-gradient(180deg, rgba(102, 163, 255, 0.14), rgba(102, 163, 255, 0.08)); + border-color: rgba(102, 163, 255, 0.28); + box-shadow: inset 0 0 0 1px rgba(102, 163, 255, 0.06); +} + +.admin-table-row, +.cache-row, +.system-item, +.pipeline-step, +.step-fix-list li, +.how-steps li { + border-radius: 12px; +} + +.admin-table-row { + background: rgba(255, 255, 255, 0.02); +} + +.admin-table-row:hover { + transform: none; + box-shadow: none; + border: 1px solid rgba(102, 163, 255, 0.15); +} + +.pipeline-step.is-active { + border-color: rgba(102, 163, 255, 0.34); + background: rgba(102, 163, 255, 0.1); +} + +.pipeline-step.is-active .pipeline-dot, +.pipeline-step.is-complete .pipeline-dot, +.system-up .system-dot { + box-shadow: 0 0 0 3px rgba(102, 163, 255, 0.12); +} + +.pipeline-dot, +.system-dot { + background: #6f7a8a; + box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.03); +} + +.system-up .system-dot { + background: #7fb0ff; +} + +.system-down .system-dot { + background: #f87171; + box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.12); +} + +.system-degraded .system-dot, +.system-not_configured .system-dot { + background: #d4b36b; + box-shadow: 0 0 0 3px rgba(212, 179, 107, 0.12); +} + +.system-pill, +.user-grid-pill, +.signed-in-build, +.site-version { + border-radius: 999px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--border); +} + +.user-grid-pill { + font-weight: 600; +} + +.user-grid-pill.is-blocked { + background: rgba(248, 113, 113, 0.12); + border-color: rgba(248, 113, 113, 0.25); +} + +.user-grid-card, +.user-detail-card { + transition: border-color 0.16s ease, background 0.16s ease; +} + +.user-grid-card:hover { + transform: none; + background: rgba(255, 255, 255, 0.035); + border-color: rgba(102, 163, 255, 0.2); +} + +.user-detail-chip, +.user-detail-stat { + background: rgba(255, 255, 255, 0.02); + border-color: var(--border); +} + +.user-detail-controls { + border-radius: 14px; +} + +.toggle { + font-size: 13px; + color: var(--ink); +} + +.toggle input[type='checkbox'] { + accent-color: var(--accent); +} + +.search, +.filters, +.find-controls, +.admin-form, +.profile-grid, +.profile-section { + gap: 14px; +} + +.recent-poster, +.request-poster, +.brand-preview { + border-radius: 12px; + box-shadow: none; +} + +.how-step-card::before { + opacity: 0.12; +} + +.how-title { + color: #9fb4d4; +} + +.step-badge { + background: rgba(255, 255, 255, 0.04); +} + +.how-callout { + border-left-color: rgba(102, 163, 255, 0.45); + background: rgba(255, 255, 255, 0.025); +} + +.modal-card { + border-radius: 16px; + border: 1px solid var(--border); + background: rgba(17, 21, 27, 0.96); + box-shadow: 0 18px 36px rgba(0, 0, 0, 0.35); +} + +[data-theme='light'] .modal-card { + background: rgba(249, 251, 255, 0.98); +} + +.site-banner { + border-radius: 14px; + padding: 14px 16px; +} + +.site-banner--info { + background: rgba(102, 163, 255, 0.1); + border-color: rgba(102, 163, 255, 0.22); +} + +.site-banner--warning { + background: rgba(212, 179, 107, 0.12); + border-color: rgba(212, 179, 107, 0.22); +} + +.site-banner--error { + background: rgba(248, 113, 113, 0.11); + border-color: rgba(248, 113, 113, 0.22); +} + +.site-banner--maintenance { + background: rgba(113, 128, 150, 0.12); + border-color: rgba(143, 160, 185, 0.22); +} + +.progress { + background: rgba(255, 255, 255, 0.03); + border-radius: 999px; +} + +.progress-fill { + background: linear-gradient(90deg, #4c7fdc, #6da8ff); +} + +.spinner { + border: 4px solid rgba(255, 255, 255, 0.08); + border-top-color: #6da8ff; + box-shadow: none; +} + +.brand-logo--header { + width: 86px; + height: 86px; +} + +@media (max-width: 720px) { + .header { + padding: 14px; + border-radius: 16px; + } + + .card { + padding: 20px; + border-radius: 16px; + } + + .header-actions { + gap: 8px; + } + + .header-actions a, + .header-actions .header-link { + border-radius: 10px; + } +} + +/* Release 1.1 UI Refresh: Professional control-panel theme */ +:root { + --ink: #111318; + --ink-muted: #5f6776; + --paper: #eef1f6; + --paper-strong: #ffffff; + --accent: #4e8ef7; + --accent-2: #77abff; + --accent-3: #9dbdff; + --border: rgba(17, 19, 24, 0.1); + --shadow: rgba(17, 19, 24, 0.16); + --glow: 0 0 0 1px rgba(78, 142, 247, 0.08), 0 14px 30px rgba(16, 20, 28, 0.08); + --input-bg: rgba(17, 19, 24, 0.035); + --input-ink: var(--ink); + --error-bg: rgba(225, 81, 81, 0.12); + --error-ink: #6f1f1f; +} + +[data-theme='dark'] { + --ink: #eef1f7; + --ink-muted: #9aa3b2; + --paper: #0a0d12; + --paper-strong: #12161d; + --accent: #5d9cff; + --accent-2: #87b5ff; + --accent-3: #a5c4ff; + --border: rgba(255, 255, 255, 0.08); + --shadow: rgba(0, 0, 0, 0.55); + --glow: 0 0 0 1px rgba(93, 156, 255, 0.12), 0 18px 42px rgba(0, 0, 0, 0.38); + --input-bg: rgba(255, 255, 255, 0.035); + --input-ink: var(--ink); + --error-bg: rgba(248, 113, 113, 0.14); + --error-ink: #ffd4d4; +} + +body { + font-family: "Manrope", "Segoe UI", sans-serif; + background: + radial-gradient(circle at 12% -8%, rgba(93, 156, 255, 0.11), transparent 42%), + radial-gradient(circle at 88% 0%, rgba(150, 160, 180, 0.08), transparent 35%), + linear-gradient(180deg, #06080d 0%, #080b10 36%, #06080c 100%); + color: var(--ink); +} + +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + background: + linear-gradient(rgba(255, 255, 255, 0.018) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.015) 1px, transparent 1px); + background-size: 28px 28px; + opacity: 0.35; + z-index: -1; +} + +[data-theme='light'] body { + background: + radial-gradient(circle at 15% -10%, rgba(93, 156, 255, 0.1), transparent 38%), + radial-gradient(circle at 90% 0%, rgba(70, 80, 95, 0.06), transparent 30%), + linear-gradient(180deg, #f4f6fb 0%, #eceff5 46%, #e8ecf2 100%); +} + +[data-theme='light'] body::before { + background: + linear-gradient(rgba(17, 19, 24, 0.028) 1px, transparent 1px), + linear-gradient(90deg, rgba(17, 19, 24, 0.02) 1px, transparent 1px); + opacity: 0.45; +} + +.page { + max-width: 1240px; + gap: 28px; +} + +.brand { + font-size: 2.15rem; + letter-spacing: 0.04em; + font-weight: 800; +} + +.tagline { + color: var(--ink-muted); + font-size: 0.98rem; + letter-spacing: 0.01em; +} + +.brand-logo { + filter: saturate(0.92) contrast(1.02); +} + +.brand-logo--header { + width: 74px; + height: 74px; +} + +.header { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.028), rgba(255, 255, 255, 0.012)); + border: 1px solid var(--border); + border-radius: 20px; + padding: 18px 20px; + box-shadow: 0 18px 38px rgba(0, 0, 0, 0.22); + backdrop-filter: blur(14px); +} + +[data-theme='light'] .header { + background: rgba(255, 255, 255, 0.7); + box-shadow: 0 14px 30px rgba(17, 19, 24, 0.08); +} + +.header-actions { + gap: 10px; +} + +.header-actions a, +.header-actions .header-link { + min-height: 36px; + padding: 8px 14px; + border-radius: 11px; + background: rgba(255, 255, 255, 0.025); + border: 1px solid rgba(255, 255, 255, 0.07); + color: var(--ink); + font-weight: 600; + box-shadow: none; +} + +[data-theme='light'] .header-actions a, +[data-theme='light'] .header-actions .header-link { + background: rgba(255, 255, 255, 0.8); + border-color: rgba(17, 19, 24, 0.08); +} + +.header-actions a:hover, +.header-actions .header-link:hover { + border-color: rgba(93, 156, 255, 0.26); + background: rgba(93, 156, 255, 0.08); +} + +.header-actions .header-cta { + background: linear-gradient(180deg, rgba(93, 156, 255, 0.28), rgba(93, 156, 255, 0.18)); + color: #ecf2ff; + border: 1px solid rgba(93, 156, 255, 0.34); + box-shadow: 0 10px 22px rgba(11, 24, 48, 0.22); +} + +[data-theme='light'] .header-actions .header-cta { + color: #102542; + background: linear-gradient(180deg, rgba(93, 156, 255, 0.16), rgba(93, 156, 255, 0.1)); + box-shadow: 0 8px 18px rgba(93, 156, 255, 0.12); +} + +.theme-toggle, +.avatar-button { + width: 44px; + height: 44px; + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.02)); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18); +} + +[data-theme='light'] .theme-toggle, +[data-theme='light'] .avatar-button { + border-color: rgba(17, 19, 24, 0.08); + background: rgba(255, 255, 255, 0.8); + box-shadow: 0 10px 18px rgba(17, 19, 24, 0.08); +} + +.avatar-button { + border-radius: 50%; + background: + radial-gradient(circle at 35% 20%, rgba(93, 156, 255, 0.22), transparent 58%), + linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02)); + font-weight: 700; + box-shadow: 0 10px 22px rgba(0, 0, 0, 0.18); +} + +.avatar-button:hover, +.theme-toggle:hover { + border-color: rgba(93, 156, 255, 0.24); + box-shadow: 0 0 0 1px rgba(93, 156, 255, 0.12), 0 14px 24px rgba(0, 0, 0, 0.2); +} + +.signed-in-dropdown { + border-radius: 14px; + background: rgba(10, 13, 19, 0.96); + border: 1px solid rgba(255, 255, 255, 0.08); + box-shadow: 0 18px 36px rgba(0, 0, 0, 0.34); + backdrop-filter: blur(14px); +} + +[data-theme='light'] .signed-in-dropdown { + background: rgba(255, 255, 255, 0.95); + border-color: rgba(17, 19, 24, 0.08); + box-shadow: 0 18px 36px rgba(17, 19, 24, 0.11); +} + +.signed-in-actions a, +.signed-in-signout { + background: rgba(255, 255, 255, 0.03); + border-color: rgba(255, 255, 255, 0.07); + border-radius: 10px; + font-weight: 600; +} + +[data-theme='light'] .signed-in-actions a, +[data-theme='light'] .signed-in-signout { + background: rgba(17, 19, 24, 0.03); + border-color: rgba(17, 19, 24, 0.07); +} + +.signed-in-actions a:hover, +.signed-in-signout:hover { + background: rgba(93, 156, 255, 0.08); + border-color: rgba(93, 156, 255, 0.22); +} + +.card, +.admin-card, +.status-box, +.summary-card, +.user-card, +.timeline-card, +.modal-card, +.auth-card, +.site-banner, +.system-status, +.user-detail-card, +.user-grid-card, +.profile-section, +.schedule-card, +.cache-row, +.system-item, +.maintenance-grid > *, +.history-grid ul, +.history-grid li { + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.028), rgba(255, 255, 255, 0.014)); + border-color: rgba(255, 255, 255, 0.075); +} + +[data-theme='light'] .card, +[data-theme='light'] .admin-card, +[data-theme='light'] .status-box, +[data-theme='light'] .summary-card, +[data-theme='light'] .user-card, +[data-theme='light'] .timeline-card, +[data-theme='light'] .modal-card, +[data-theme='light'] .auth-card, +[data-theme='light'] .site-banner, +[data-theme='light'] .system-status, +[data-theme='light'] .user-detail-card, +[data-theme='light'] .user-grid-card, +[data-theme='light'] .profile-section, +[data-theme='light'] .schedule-card, +[data-theme='light'] .cache-row, +[data-theme='light'] .system-item { + background: rgba(255, 255, 255, 0.8); + border-color: rgba(17, 19, 24, 0.08); +} + +.card, +.admin-card { + border-radius: 22px; + box-shadow: 0 20px 44px rgba(0, 0, 0, 0.28); +} + +[data-theme='light'] .card, +[data-theme='light'] .admin-card { + box-shadow: 0 14px 28px rgba(17, 19, 24, 0.07); +} + +.lede, +.section-subtitle, +.sync-note, +.user-meta, +.signed-in-header, +.admin-nav-title, +.system-test-message { + color: var(--ink-muted); +} + +.admin-shell { + gap: 22px; +} + +.admin-shell-nav { + position: sticky; + top: 20px; + align-self: start; +} + +.admin-sidebar { + border-radius: 18px; + border: 1px solid rgba(255, 255, 255, 0.07); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.012)); + box-shadow: 0 18px 34px rgba(0, 0, 0, 0.2); +} + +[data-theme='light'] .admin-sidebar { + border-color: rgba(17, 19, 24, 0.08); + background: rgba(255, 255, 255, 0.82); + box-shadow: 0 12px 22px rgba(17, 19, 24, 0.06); +} + +.admin-sidebar-title { + font-weight: 800; + letter-spacing: 0.12em; + color: #aeb7c6; +} + +[data-theme='light'] .admin-sidebar-title { + color: #667085; +} + +.admin-nav-title { + letter-spacing: 0.1em; + font-weight: 700; + color: #8e97a8; +} + +.admin-nav-links a { + border-radius: 12px; + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.04); + font-weight: 600; +} + +[data-theme='light'] .admin-nav-links a { + background: rgba(17, 19, 24, 0.02); + border-color: rgba(17, 19, 24, 0.05); +} + +.admin-nav-links a:hover { + background: rgba(93, 156, 255, 0.07); + border-color: rgba(93, 156, 255, 0.22); +} + +.admin-nav-links a.is-active { + color: #eef4ff; + background: linear-gradient(180deg, rgba(93, 156, 255, 0.2), rgba(93, 156, 255, 0.1)); + border-color: rgba(93, 156, 255, 0.3); + box-shadow: inset 0 0 0 1px rgba(93, 156, 255, 0.08); +} + +[data-theme='light'] .admin-nav-links a.is-active { + color: #0f2342; +} + +.admin-header h1, +.section-header h2, +.request-header h1, +.auth-card h1, +.profile-section h2, +.status-box h2 { + letter-spacing: -0.02em; + font-weight: 800; +} + +.admin-form, +.admin-section, +.profile-section, +.status-box, +.summary-card, +.timeline-card, +.user-detail-card, +.user-grid-card { + border-radius: 16px; +} + +input, +select, +textarea { + border-radius: 10px; + border: 1px solid rgba(255, 255, 255, 0.08); + background: rgba(255, 255, 255, 0.03); + color: var(--ink); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02); +} + +[data-theme='light'] input, +[data-theme='light'] select, +[data-theme='light'] textarea { + border-color: rgba(17, 19, 24, 0.1); + background: rgba(17, 19, 24, 0.025); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4); +} + +input:focus, +select:focus, +textarea:focus { + border-color: rgba(93, 156, 255, 0.34); + box-shadow: 0 0 0 3px rgba(93, 156, 255, 0.12); + outline: none; +} + +button { + border-radius: 11px; + border: 1px solid rgba(93, 156, 255, 0.28); + background: linear-gradient(180deg, rgba(93, 156, 255, 0.2), rgba(93, 156, 255, 0.12)); + color: #eef4ff; + box-shadow: 0 8px 18px rgba(10, 20, 38, 0.18); + font-weight: 700; +} + +[data-theme='light'] button { + color: #11233f; + box-shadow: 0 6px 14px rgba(93, 156, 255, 0.08); +} + +button:hover:not(:disabled) { + transform: translateY(-1px); + border-color: rgba(93, 156, 255, 0.42); + background: linear-gradient(180deg, rgba(93, 156, 255, 0.26), rgba(93, 156, 255, 0.15)); +} + +button:disabled { + opacity: 0.6; + box-shadow: none; +} + +.ghost-button, +.details-toggle button, +.recent-grid button, +.admin-pagination button, +.system-test, +.header-actions a, +.header-actions .header-link { + box-shadow: none; +} + +.ghost-button, +.details-toggle button, +.recent-grid button, +.admin-pagination button, +.system-test { + color: var(--ink); + background: rgba(255, 255, 255, 0.025); + border: 1px solid rgba(255, 255, 255, 0.08); +} + +[data-theme='light'] .ghost-button, +[data-theme='light'] .details-toggle button, +[data-theme='light'] .recent-grid button, +[data-theme='light'] .admin-pagination button, +[data-theme='light'] .system-test { + background: rgba(17, 19, 24, 0.025); + border-color: rgba(17, 19, 24, 0.08); +} + +.ghost-button:hover:not(:disabled), +.details-toggle button:hover:not(:disabled), +.recent-grid button:hover:not(:disabled), +.admin-pagination button:hover:not(:disabled), +.system-test:hover:not(:disabled) { + background: rgba(93, 156, 255, 0.08); + border-color: rgba(93, 156, 255, 0.24); + color: var(--ink); +} + +.danger-button { + border-color: rgba(244, 114, 114, 0.28); + background: linear-gradient(180deg, rgba(244, 114, 114, 0.2), rgba(244, 114, 114, 0.11)); + color: #ffeaea; +} + +[data-theme='light'] .danger-button { + color: #5c1717; +} + +.admin-table { + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.07); + overflow: hidden; + background: rgba(255, 255, 255, 0.015); +} + +[data-theme='light'] .admin-table { + border-color: rgba(17, 19, 24, 0.08); + background: rgba(255, 255, 255, 0.7); +} + +.admin-table-head { + background: rgba(255, 255, 255, 0.02); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +[data-theme='light'] .admin-table-head { + background: rgba(17, 19, 24, 0.02); + border-bottom-color: rgba(17, 19, 24, 0.06); +} + +.admin-table-row { + border-top-color: rgba(255, 255, 255, 0.05); +} + +[data-theme='light'] .admin-table-row { + border-top-color: rgba(17, 19, 24, 0.05); +} + +.admin-table-row:hover { + background: rgba(93, 156, 255, 0.04); + border-color: rgba(93, 156, 255, 0.18); +} + +.user-grid-card:hover, +.recent-card:hover, +.summary-card:hover, +.timeline-card:hover { + border-color: rgba(93, 156, 255, 0.2); + background: rgba(255, 255, 255, 0.04); +} + +[data-theme='light'] .user-grid-card:hover, +[data-theme='light'] .recent-card:hover, +[data-theme='light'] .summary-card:hover, +[data-theme='light'] .timeline-card:hover { + background: rgba(255, 255, 255, 0.92); +} + +.recent-card, +.user-grid-card, +.summary-card, +.timeline-card, +.system-item, +.cache-row, +.user-detail-controls, +.user-detail-stat, +.user-detail-chip { + border-radius: 14px; + border: 1px solid rgba(255, 255, 255, 0.07); + box-shadow: none; +} + +[data-theme='light'] .recent-card, +[data-theme='light'] .user-grid-card, +[data-theme='light'] .summary-card, +[data-theme='light'] .timeline-card, +[data-theme='light'] .system-item, +[data-theme='light'] .cache-row, +[data-theme='light'] .user-detail-controls, +[data-theme='light'] .user-detail-stat, +[data-theme='light'] .user-detail-chip { + border-color: rgba(17, 19, 24, 0.08); +} + +.recent-title, +.user-detail-name, +.user-grid-header strong, +.timeline-title, +.system-name { + font-weight: 700; + letter-spacing: -0.01em; +} + +.system-pill, +.user-grid-pill, +.user-detail-chip, +.site-version, +.signed-in-build { + background: rgba(255, 255, 255, 0.02); + border-color: rgba(255, 255, 255, 0.08); + border-radius: 999px; +} + +[data-theme='light'] .system-pill, +[data-theme='light'] .user-grid-pill, +[data-theme='light'] .user-detail-chip, +[data-theme='light'] .site-version, +[data-theme='light'] .signed-in-build { + background: rgba(17, 19, 24, 0.02); + border-color: rgba(17, 19, 24, 0.08); +} + +.system-pill-up { + background: rgba(93, 156, 255, 0.14); + border-color: rgba(93, 156, 255, 0.24); + color: #d7e8ff; +} + +[data-theme='light'] .system-pill-up { + color: #163963; +} + +.system-pill-down, +.user-grid-pill.is-blocked { + background: rgba(244, 114, 114, 0.14); + border-color: rgba(244, 114, 114, 0.24); +} + +.system-pill-degraded, +.user-grid-pill.is-disabled { + background: rgba(208, 166, 92, 0.14); + border-color: rgba(208, 166, 92, 0.22); +} + +.system-dot { + box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.03); +} + +.system-up .system-dot { + background: #7fb1ff; + box-shadow: 0 0 0 3px rgba(93, 156, 255, 0.14); +} + +.system-down .system-dot { + background: #f87171; + box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.14); +} + +.system-degraded .system-dot, +.system-not_configured .system-dot { + background: #d7ac61; + box-shadow: 0 0 0 3px rgba(215, 172, 97, 0.14); +} + +.site-banner { + border-radius: 14px; + border-width: 1px; + background: rgba(255, 255, 255, 0.03); +} + +.site-banner--info { + background: rgba(93, 156, 255, 0.08); + border-color: rgba(93, 156, 255, 0.18); +} + +.site-banner--warning { + background: rgba(208, 166, 92, 0.1); + border-color: rgba(208, 166, 92, 0.22); +} + +.site-banner--error { + background: rgba(244, 114, 114, 0.1); + border-color: rgba(244, 114, 114, 0.22); +} + +.site-banner--maintenance { + background: rgba(148, 163, 184, 0.08); + border-color: rgba(148, 163, 184, 0.18); +} + +.request-poster, +.recent-poster, +.brand-preview { + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.06); + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.16); +} + +[data-theme='light'] .request-poster, +[data-theme='light'] .recent-poster, +[data-theme='light'] .brand-preview { + border-color: rgba(17, 19, 24, 0.08); + box-shadow: 0 8px 18px rgba(17, 19, 24, 0.08); +} + +.progress { + background: rgba(255, 255, 255, 0.035); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 999px; +} + +[data-theme='light'] .progress { + background: rgba(17, 19, 24, 0.03); + border-color: rgba(17, 19, 24, 0.05); +} + +.progress-fill { + background: linear-gradient(90deg, #5d9cff, #87b5ff); +} + +.spinner { + border-color: rgba(255, 255, 255, 0.08); + border-top-color: #87b5ff; +} + +.toggle { + gap: 8px; + padding: 6px 8px; + border-radius: 10px; + background: rgba(255, 255, 255, 0.018); + border: 1px solid rgba(255, 255, 255, 0.05); +} + +[data-theme='light'] .toggle { + background: rgba(17, 19, 24, 0.02); + border-color: rgba(17, 19, 24, 0.05); +} + +.toggle input[type='checkbox'] { + accent-color: #5d9cff; +} + +.user-bulk-toolbar { + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 14px; + padding: 12px 14px; +} + +[data-theme='light'] .user-bulk-toolbar { + background: rgba(17, 19, 24, 0.02); + border-color: rgba(17, 19, 24, 0.06); +} + +.user-detail-layout { + align-items: start; + gap: 16px; +} + +.user-detail-controls { + background: rgba(255, 255, 255, 0.02); + border: 1px solid rgba(255, 255, 255, 0.07); +} + +[data-theme='light'] .user-detail-controls { + background: rgba(17, 19, 24, 0.02); + border-color: rgba(17, 19, 24, 0.07); +} + +.error-banner, +.status-banner { + border-radius: 12px; + border: 1px solid rgba(244, 114, 114, 0.2); + background: rgba(244, 114, 114, 0.1); + color: var(--error-ink); +} + +[data-theme='dark'] .error-banner, +[data-theme='dark'] .status-banner { + color: #ffd9d9; +} + +.auth-card { + max-width: 520px; + margin-inline: auto; +} + +.auth-form label, +.admin-grid label { + font-weight: 600; + color: var(--ink); +} + +@media (max-width: 980px) { + .page { + padding: 28px 18px 64px; + } + + .header { + padding: 14px; + border-radius: 16px; + } + + .brand-logo--header { + width: 60px; + height: 60px; + } +} + +/* Enterprise polish pass */ +[data-theme='dark'] { + --accent: #6f95c6; + --accent-2: #8aa9d1; + --accent-3: #b2c5de; +} + +body { + background: + radial-gradient(circle at 12% -8%, rgba(111, 149, 198, 0.08), transparent 40%), + linear-gradient(180deg, #06070a 0%, #07090d 55%, #06080a 100%); +} + +body::before { + opacity: 0.2; + background-size: 32px 32px; +} + +.page { + max-width: 1280px; + gap: 24px; +} + +.header { + border-radius: 14px; + padding: 14px 16px; + backdrop-filter: blur(8px); + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.24); +} + +.brand { + font-size: 2rem; + letter-spacing: 0.03em; +} + +.tagline { + font-size: 0.95rem; +} + +.header-actions a, +.header-actions .header-link { + border-radius: 8px; + min-height: 34px; + padding: 7px 12px; + font-weight: 600; +} + +.header-actions .header-cta { + background: rgba(111, 149, 198, 0.14); + border-color: rgba(111, 149, 198, 0.28); + color: #edf3fb; +} + +[data-theme='light'] .header-actions .header-cta { + color: #19324f; +} + +.theme-toggle { + border-radius: 10px; +} + +.avatar-button { + width: 42px; + height: 42px; + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.16); +} + +.signed-in-dropdown { + border-radius: 12px; +} + +.signed-in-actions a, +.signed-in-signout { + border-radius: 8px; +} + +.card, +.admin-card { + border-radius: 16px; + box-shadow: 0 14px 28px rgba(0, 0, 0, 0.24); +} + +.admin-shell { + gap: 18px; +} + +.admin-sidebar { + border-radius: 14px; + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.18); +} + +.admin-sidebar-title { + letter-spacing: 0.14em; + font-size: 0.78rem; +} + +.admin-nav-title { + letter-spacing: 0.12em; + font-size: 0.72rem; +} + +.admin-nav-links a { + border-radius: 8px; + padding: 10px 12px; +} + +.admin-nav-links a.is-active { + background: rgba(111, 149, 198, 0.14); + color: #edf3fb; + border-color: rgba(111, 149, 198, 0.26); +} + +[data-theme='light'] .admin-nav-links a.is-active { + color: #183252; +} + +.admin-header { + gap: 14px; + padding-bottom: 6px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +[data-theme='light'] .admin-header { + border-bottom-color: rgba(17, 19, 24, 0.06); +} + +.admin-header h1, +.section-header h2, +.request-header h1 { + font-weight: 700; +} + +.admin-form, +.admin-section, +.profile-section, +.status-box, +.summary-card, +.timeline-card, +.user-detail-card, +.user-grid-card, +.system-status { + border-radius: 12px; +} + +input, +select, +textarea { + border-radius: 8px; + min-height: 40px; + background: rgba(255, 255, 255, 0.022); +} + +textarea { + min-height: 92px; +} + +[data-theme='light'] input, +[data-theme='light'] select, +[data-theme='light'] textarea { + background: rgba(17, 19, 24, 0.02); +} + +button { + border-radius: 8px; + min-height: 36px; + padding: 8px 12px; + background: rgba(111, 149, 198, 0.15); + border-color: rgba(111, 149, 198, 0.28); + color: #eef2f7; +} + +[data-theme='light'] button { + color: #16304d; +} + +button:hover:not(:disabled) { + background: rgba(111, 149, 198, 0.22); + border-color: rgba(111, 149, 198, 0.36); + transform: none; +} + +.ghost-button, +.details-toggle button, +.recent-grid button, +.admin-pagination button, +.system-test { + background: rgba(255, 255, 255, 0.018); + border-radius: 8px; +} + +[data-theme='light'] .ghost-button, +[data-theme='light'] .details-toggle button, +[data-theme='light'] .recent-grid button, +[data-theme='light'] .admin-pagination button, +[data-theme='light'] .system-test { + background: rgba(17, 19, 24, 0.018); +} + +.danger-button { + background: rgba(214, 93, 93, 0.14); + border-color: rgba(214, 93, 93, 0.28); +} + +.admin-table { + border-radius: 12px; +} + +.admin-table-head { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.admin-table-row, +.cache-row { + border-radius: 8px; +} + +.admin-table-row:hover { + background: rgba(111, 149, 198, 0.05); + border-color: rgba(111, 149, 198, 0.16); +} + +.user-grid { + gap: 12px; +} + +.user-grid-card { + border-radius: 10px; +} + +.user-grid-meta, +.user-meta, +.user-detail-helper, +.user-bulk-summary span, +.system-test-message, +.timeline-sublist, +.request-meta, +.recent-meta { + color: #98a1af; +} + +.user-detail-chip, +.site-version, +.signed-in-build, +.system-pill, +.user-grid-pill { + border-radius: 999px; + font-size: 0.75rem; +} + +.user-detail-controls, +.user-detail-stat, +.user-detail-chip, +.system-item, +.recent-card, +.timeline-card, +.summary-card { + border-radius: 10px; +} + +.user-detail-grid { + gap: 10px; +} + +.user-detail-stat { + padding: 12px; +} + +.site-banner { + border-radius: 10px; + padding: 12px 14px; +} + +.toggle { + border-radius: 8px; + padding: 4px 6px; +} + +.progress { + height: 10px; +} + +.spinner { + border-width: 3px; +} + +@media (max-width: 980px) { + .header { + border-radius: 12px; + padding: 12px; + } + + .card, + .admin-card { + border-radius: 14px; + } +} diff --git a/frontend/app/users/[id]/page.tsx b/frontend/app/users/[id]/page.tsx index 6ea180f..4733280 100644 --- a/frontend/app/users/[id]/page.tsx +++ b/frontend/app/users/[id]/page.tsx @@ -182,82 +182,92 @@ export default function UserDetailPage() { ) : ( <>
-
-
- {user.username} -
- Jellyseerr ID: {user.jellyseerr_user_id ?? user.id ?? 'Unknown'} - Role: {user.role} - Login type: {user.auth_provider || 'local'} - Last login: {formatDateTime(user.last_login_at)} +
+
+
+ {user.username} + + {user.is_blocked ? 'Blocked' : 'Active'} + +
+
+ + Jellyseerr ID: {user.jellyseerr_user_id ?? user.id ?? 'Unknown'} + + Role: {user.role} + Login type: {user.auth_provider || 'local'} + Last login: {formatDateTime(user.last_login_at)}
-
- - - +
+
User controls
+
+ + + +
+ {user.role === 'admin' && ( +
+ Admins always have auto search/download access. +
+ )}
- {user.role === 'admin' && ( -
- Admins always have auto search/download access. -
- )}
-
+
Total {stats?.total ?? 0}
-
+
Ready {stats?.ready ?? 0}
-
+
Pending {stats?.pending ?? 0}
-
+
Approved {stats?.approved ?? 0}
-
+
Working {stats?.working ?? 0}
-
+
Partial {stats?.partial ?? 0}
-
+
Declined {stats?.declined ?? 0}
-
+
In progress {stats?.in_progress ?? 0}
-
+
Last request {formatDateTime(stats?.last_request_at)}
diff --git a/frontend/app/users/page.tsx b/frontend/app/users/page.tsx index 934df40..82afa94 100644 --- a/frontend/app/users/page.tsx +++ b/frontend/app/users/page.tsx @@ -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(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
Loading users...
} + const nonAdminUsers = users.filter((user) => user.role !== 'admin') + const autoSearchEnabledCount = nonAdminUsers.filter((user) => user.autoSearchEnabled !== false).length + return ( {error &&
{error}
} {jellyseerrSyncStatus &&
{jellyseerrSyncStatus}
} +
+
+ Auto search/download + + {autoSearchEnabledCount} of {nonAdminUsers.length} non-admin users enabled + +
+
+ + +
+
{users.length === 0 ? (
No users found yet.
) : ( @@ -260,6 +318,11 @@ export default function UsersPage() { {user.isBlocked ? 'Blocked' : 'Active'}
+
+ + Auto search {user.autoSearchEnabled === false ? 'Off' : 'On'} + +
Total