From e6b4f99ea796977fddf1cd5fa55a1807b3520c86 Mon Sep 17 00:00:00 2001 From: Rephl3x Date: Sun, 21 Jun 2026 11:41:38 +1200 Subject: [PATCH] Redesign beta Magent UI --- frontend/app/admin/SettingsPage.tsx | 76 +- frontend/app/admin/issues/page.tsx | 5 + frontend/app/admin/page.tsx | 246 ++++- frontend/app/layout.tsx | 6 +- frontend/app/login/page.tsx | 27 +- frontend/app/ops-redesign.css | 1388 +++++++++++++++++++++++++++ frontend/app/page.tsx | 34 + frontend/app/requests/[id]/page.tsx | 40 +- frontend/app/ui/AdminShell.tsx | 1 + frontend/app/ui/AdminSidebar.tsx | 10 + frontend/app/ui/BrandingLogo.tsx | 40 +- frontend/app/ui/HeaderActions.tsx | 58 +- 12 files changed, 1891 insertions(+), 40 deletions(-) create mode 100644 frontend/app/admin/issues/page.tsx create mode 100644 frontend/app/ops-redesign.css diff --git a/frontend/app/admin/SettingsPage.tsx b/frontend/app/admin/SettingsPage.tsx index 0e0673e..992e222 100644 --- a/frontend/app/admin/SettingsPage.tsx +++ b/frontend/app/admin/SettingsPage.tsx @@ -19,6 +19,12 @@ type ServiceOptions = { qualityProfiles: { id: number; name: string; label: string }[] } +type ServiceStatus = { + name: string + status: string + message?: string +} + const SECTION_LABELS: Record = { magent: 'Magent', general: 'General', @@ -414,6 +420,8 @@ export default function SettingsPage({ section }: SettingsPageProps) { const [maintenanceStatus, setMaintenanceStatus] = useState(null) const [maintenanceBusy, setMaintenanceBusy] = useState(false) const [liveStreamConnected, setLiveStreamConnected] = useState(false) + const [serviceStatuses, setServiceStatuses] = useState([]) + const [serviceStatusCheckedAt, setServiceStatusCheckedAt] = useState(null) const requestsSyncRef = useRef(null) const artworkPrefetchRef = useRef(null) const computeProgressPercent = ( @@ -543,6 +551,21 @@ export default function SettingsPage({ section }: SettingsPageProps) { } }, []) + const loadServiceStatuses = useCallback(async () => { + try { + const baseUrl = getApiBase() + const response = await authFetch(`${baseUrl}/status/services`) + if (!response.ok) { + return + } + const data = await response.json() + setServiceStatuses(Array.isArray(data?.services) ? data.services : []) + setServiceStatusCheckedAt(new Date().toISOString()) + } catch (err) { + console.error(err) + } + }, []) + useEffect(() => { const load = async () => { if (!getToken()) { @@ -550,7 +573,7 @@ export default function SettingsPage({ section }: SettingsPageProps) { return } try { - await loadSettings() + await Promise.all([loadSettings(), loadServiceStatuses()]) if (section === 'cache' || section === 'artwork') { await loadArtworkPrefetchStatus() await loadArtworkSummary() @@ -570,7 +593,7 @@ export default function SettingsPage({ section }: SettingsPageProps) { if (section === 'radarr') { void loadOptions('radarr') } - }, [loadArtworkPrefetchStatus, loadArtworkSummary, loadOptions, loadSettings, router, section]) + }, [loadArtworkPrefetchStatus, loadArtworkSummary, loadOptions, loadServiceStatuses, loadSettings, router, section]) const groupedSettings = useMemo(() => { const groups: Record = {} @@ -583,6 +606,22 @@ export default function SettingsPage({ section }: SettingsPageProps) { }, [settings]) const settingsSection = SETTINGS_SECTION_MAP[section] ?? null + const statusNamesBySection: Record = { + seerr: ['Seerr', 'Jellyseerr', 'Jellyseer'], + jellyseerr: ['Seerr', 'Jellyseerr', 'Jellyseer'], + jellyfin: ['Jellyfin'], + sonarr: ['Sonarr'], + radarr: ['Radarr'], + prowlarr: ['Prowlarr'], + qbittorrent: ['qBittorrent', 'Qbittorrent'], + } + const statusNames = statusNamesBySection[section] ?? statusNamesBySection[settingsSection ?? ''] ?? [] + const currentServiceStatus = serviceStatuses.find((service) => + statusNames.some((name) => name.toLowerCase() === service.name.toLowerCase()) + ) + const currentServiceConfigured = currentServiceStatus + ? currentServiceStatus.status !== 'not_configured' + : null const isMagentGroupedSection = section === 'magent' || section === 'general' || section === 'notifications' const isSiteGroupedSection = section === 'site' const visibleSections = settingsSection ? [settingsSection] : [] @@ -1681,6 +1720,39 @@ export default function SettingsPage({ section }: SettingsPageProps) { } > {status &&
{status}
} + {currentServiceStatus ? ( +
+
+
+
+
+ Status + {currentServiceStatus.status.replaceAll('_', ' ')} +
+
+ Configuration + {currentServiceConfigured ? 'Configured' : 'Not configured'} +
+
+ Last checked + + {serviceStatusCheckedAt ? new Date(serviceStatusCheckedAt).toLocaleString() : 'Not checked yet'} + +
+ +
+
+ ) : null} {settingsSections.length > 0 ? (
{settingsSections diff --git a/frontend/app/admin/issues/page.tsx b/frontend/app/admin/issues/page.tsx new file mode 100644 index 0000000..e626fa4 --- /dev/null +++ b/frontend/app/admin/issues/page.tsx @@ -0,0 +1,5 @@ +import PortalClient from '../../portal/PortalClient' + +export default function AdminIssuesPage() { + return +} diff --git a/frontend/app/admin/page.tsx b/frontend/app/admin/page.tsx index 30f1888..932d298 100644 --- a/frontend/app/admin/page.tsx +++ b/frontend/app/admin/page.tsx @@ -1,24 +1,258 @@ '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 + by_status?: Record + } + 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([]) + const [serviceOverall, setServiceOverall] = useState('unknown') + const [recent, setRecent] = useState([]) + const [portalOverview, setPortalOverview] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(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 = ( +
+
+ Service ecosystem +
+ {services.length === 0 ? ( +
Service status is not available yet.
+ ) : ( + services.map((service) => ( + + + + {service.name} + {service.message ?? 'No message reported'} + + {service.status} + + )) + )} +
+
+ +
+ ) return ( router.push('/')}> - Back to requests + View health } > -
-
- Pick a section from the left. Each page explains what it does and how it helps. + {loading ?
Loading operations dashboard...
: null} + {error ?
{error}
: null} + +
+
+ Services online + + {serviceCounts.up}/{serviceCounts.total || 0} + +

{serviceOverall === 'up' ? 'All configured services are responding.' : 'Some services need review.'}

+
+
+ Recent requests + {recent.length} +

Loaded from the live request cache.

+
+
+ Open issue items + {issueCount} +

{commentCount} portal comments recorded.

+
+
+ Portal requests + {requestItemCount} +

Tracked in the dedicated request workflow.

+
+
+ +
+
+
+

Recent activity

+

Live request cache entries, newest first.

+
+
+ {recent.length === 0 ? ( +
No recent requests were returned.
+ ) : ( +
+
+ Request + Status + User + Created +
+ {recent.map((row) => ( + + ))} +
+ )} +
+ +
+
+
+

Attention states

+

Service states that affect request processing.

+
+
+
+ {serviceCounts.down} down + {serviceCounts.degraded} degraded + {serviceCounts.notConfigured} not configured
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index fdb99a9..c0b36a8 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -1,8 +1,8 @@ import './globals.css' +import './ops-redesign.css' import type { ReactNode } from 'react' import HeaderActions from './ui/HeaderActions' import HeaderIdentity from './ui/HeaderIdentity' -import ThemeToggle from './ui/ThemeToggle' import BrandingFavicon from './ui/BrandingFavicon' import BrandingLogo from './ui/BrandingLogo' import SiteStatus from './ui/SiteStatus' @@ -24,12 +24,12 @@ export default function RootLayout({ children }: { children: ReactNode }) {
Magent
-
Find and fix media requests fast.
+
GrizzlyFlix media operations
- + Beta
diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx index 7cae285..071e5b4 100644 --- a/frontend/app/login/page.tsx +++ b/frontend/app/login/page.tsx @@ -108,10 +108,17 @@ export default function LoginPage() { })() return ( -
- -

Sign in

-

{loginHelpText}

+
+
+
+ +
+
+ Secure access +

Magent operational gateway

+

{loginHelpText}

+
+
{ if (!primaryMode) { @@ -121,23 +128,25 @@ export default function LoginPage() { } void submit(event, primaryMode) }} - className="auth-form" + className="auth-form auth-panel" > {error &&
{error}
} @@ -171,6 +180,10 @@ export default function LoginPage() { {!loginOptions.showJellyfinLogin && !loginOptions.showLocalLogin ? (
Login is currently disabled. Contact an administrator.
) : null} +
+
) diff --git a/frontend/app/ops-redesign.css b/frontend/app/ops-redesign.css new file mode 100644 index 0000000..3c72d1b --- /dev/null +++ b/frontend/app/ops-redesign.css @@ -0,0 +1,1388 @@ +@import url('https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap'); + +:root, +[data-theme='dark'], +[data-theme='light'] { + color-scheme: dark; + --ops-bg: #070d1c; + --ops-bg-2: #0b1326; + --ops-panel: #10182b; + --ops-panel-2: #151e33; + --ops-panel-3: #1c263d; + --ops-line: #334057; + --ops-line-soft: rgba(176, 190, 226, 0.16); + --ops-text: #eff4ff; + --ops-muted: #aeb7ce; + --ops-faint: #737d96; + --ops-primary: #5a50f0; + --ops-primary-2: #c6c1ff; + --ops-cyan: #7ed7ff; + --ops-cyan-2: #0ea5e9; + --ops-coral: #ffb08a; + --ops-green: #85efac; + --ops-red: #ff8d8d; + --ops-warn: #ffd082; + --ops-radius-sm: 4px; + --ops-radius: 6px; + --ops-radius-lg: 8px; + --ink: var(--ops-text); + --ink-muted: var(--ops-muted); + --paper: var(--ops-bg); + --paper-strong: var(--ops-panel); + --accent: var(--ops-coral); + --accent-2: var(--ops-primary); + --accent-3: var(--ops-cyan); + --border: var(--ops-line-soft); + --shadow: transparent; + --glow: 0 0 0 1px rgba(126, 215, 255, 0.16); + --input-bg: rgba(255, 255, 255, 0.035); + --input-ink: var(--ops-text); + --line: var(--ops-line-soft); + --panel: var(--ops-panel); + --panel-soft: rgba(255, 255, 255, 0.035); + --text: var(--ops-text); + --muted: var(--ops-muted); + --error-bg: rgba(122, 36, 53, 0.44); + --error-ink: #ffd6d6; +} + +* { + letter-spacing: 0 !important; +} + +html { + background: var(--ops-bg); +} + +body { + min-height: 100vh; + background: + radial-gradient(circle at 18% 0%, rgba(90, 80, 240, 0.2), transparent 32rem), + radial-gradient(circle at 88% 12%, rgba(14, 165, 233, 0.13), transparent 28rem), + linear-gradient(180deg, #081025 0%, #060b18 62%, #050914 100%); + color: var(--ops-text); + font-family: "Geist", "Segoe UI", Arial, sans-serif; + font-size: 15px; + line-height: 1.5; +} + +a { + color: inherit; +} + +.page { + width: min(100%, 1480px); + max-width: none; + margin: 0 auto; + padding: 0 20px 92px; + gap: 18px; +} + +.header { + position: sticky; + top: 0; + z-index: 100; + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + grid-template-rows: auto auto; + gap: 0; + align-items: center; + margin: 0 -20px; + padding: 14px 20px 12px; + border-bottom: 1px solid var(--ops-line); + background: rgba(8, 14, 31, 0.88); + backdrop-filter: blur(18px); +} + +.header-left, +.header-right { + min-width: 0; +} + +.brand-link { + gap: 12px; + min-width: 0; +} + +.brand-stack { + gap: 0; + min-width: 0; +} + +.brand { + color: var(--ops-primary-2); + font-size: clamp(1.25rem, 2.5vw, 1.75rem); + font-weight: 800; + text-transform: uppercase; + text-shadow: 0 0 18px rgba(198, 193, 255, 0.18); +} + +.tagline { + color: var(--ops-muted); + font-size: 0.78rem; + font-family: "JetBrains Mono", Consolas, monospace; +} + +.brand-logo--header { + width: 34px; + height: 34px; + object-fit: contain; + border-radius: var(--ops-radius); +} + +.branding-logo-shell { + position: relative; + display: grid; + place-items: center; + overflow: hidden; + flex: 0 0 auto; + border: 1px solid rgba(126, 215, 255, 0.22); + background: rgba(17, 26, 51, 0.92); +} + +.branding-logo-shell img, +.branding-logo-shell svg { + grid-area: 1 / 1; + width: 100%; + height: 100%; + object-fit: contain; +} + +.branding-logo-shell img { + opacity: 0; +} + +.branding-logo-shell img.is-loaded { + opacity: 1; +} + +.header-right { + display: inline-flex; + align-items: center; + gap: 10px; +} + +.beta-chip { + display: inline-flex; + align-items: center; + min-height: 32px; + padding: 6px 14px; + border: 1px solid rgba(126, 215, 255, 0.36); + border-radius: var(--ops-radius-lg); + background: rgba(14, 165, 233, 0.16); + color: var(--ops-cyan); + font-family: "JetBrains Mono", Consolas, monospace; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; +} + +.header-nav { + grid-column: 1 / -1; + margin-top: 12px; +} + +.header-actions { + display: flex; + justify-content: center; + gap: 8px; + width: 100%; +} + +.header-actions a { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + min-height: 38px; + padding: 8px 12px; + border-radius: var(--ops-radius); + border: 1px solid transparent; + background: transparent; + color: var(--ops-muted); + font-family: "JetBrains Mono", Consolas, monospace; + font-size: 0.78rem; + text-decoration: none; + box-shadow: none; +} + +.header-actions a span { + color: var(--ops-cyan); + font-size: 0.68rem; +} + +.header-actions a:hover, +.header-actions a.is-active { + border-color: rgba(126, 215, 255, 0.22); + background: rgba(14, 165, 233, 0.16); + color: var(--ops-text); +} + +.avatar-button { + width: 38px; + height: 38px; + border-radius: var(--ops-radius); + border: 1px solid var(--ops-line-soft); + background: var(--ops-panel-3); + color: var(--ops-text); + box-shadow: none; +} + +.signed-in-dropdown { + border-radius: var(--ops-radius-lg); + border: 1px solid var(--ops-line); + background: rgba(13, 20, 39, 0.98); + box-shadow: 0 22px 60px rgba(0, 0, 0, 0.42); +} + +.signed-in-header, +.signed-in-build { + color: var(--ops-muted); + font-family: "JetBrains Mono", Consolas, monospace; +} + +.signed-in-actions a, +.signed-in-signout { + border-radius: var(--ops-radius); + border-color: var(--ops-line-soft); + background: rgba(255, 255, 255, 0.035); +} + +.site-banner { + border-radius: var(--ops-radius); + border: 1px solid rgba(255, 208, 130, 0.28); + background: rgba(103, 75, 25, 0.38); + color: #ffe4b3; + font-family: "JetBrains Mono", Consolas, monospace; + font-size: 0.82rem; +} + +.card, +.admin-card, +.admin-panel, +.main-panel, +.summary-card, +.portal-overview-card, +.status-box, +.timeline-card, +.modal-card, +.admin-zone, +.admin-rail-card, +.users-summary-card, +.stat-card { + border-radius: var(--ops-radius-lg) !important; + border: 1px solid var(--ops-line) !important; + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.012)), + var(--ops-panel) !important; + box-shadow: none !important; +} + +.card { + width: 100%; + padding: 24px; + gap: 20px; +} + +h1, +h2, +h3 { + color: var(--ops-text); + font-weight: 700; + line-height: 1.1; +} + +h1 { + font-size: clamp(2rem, 5vw, 4rem); +} + +h2 { + font-size: clamp(1.25rem, 2vw, 1.75rem); +} + +h3 { + font-size: 1rem; +} + +.lede, +.section-subtitle, +.meta, +.helper, +.recent-meta, +.user-directory-subtext, +.users-summary-meta { + color: var(--ops-muted) !important; +} + +.section-kicker, +.admin-sidebar-title, +.admin-nav-title, +.stat-label, +.users-summary-label, +.user-bulk-label, +.settings-inline-field span, +.recent-filter span, +.label-row, +.portal-toolbar label span, +.admin-rail-eyebrow { + color: var(--ops-muted); + font-family: "JetBrains Mono", Consolas, monospace; + font-size: 0.76rem; + font-weight: 700; + text-transform: uppercase; +} + +input, +select, +textarea { + width: 100%; + min-height: 44px; + border-radius: var(--ops-radius) !important; + border: 1px solid var(--ops-line) !important; + background: rgba(8, 13, 28, 0.78) !important; + color: var(--ops-text) !important; + font-family: "Geist", "Segoe UI", Arial, sans-serif; + font-size: 0.96rem; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.025); +} + +input:focus, +select:focus, +textarea:focus { + outline: 2px solid rgba(126, 215, 255, 0.24); + border-color: rgba(126, 215, 255, 0.6) !important; +} + +input::placeholder, +textarea::placeholder { + color: #616b84; +} + +button, +.ghost-button, +.settings-action-button, +.header-link { + min-height: 40px; + border-radius: var(--ops-radius) !important; + border: 1px solid rgba(126, 215, 255, 0.18); + background: var(--ops-primary); + color: #f7f7ff; + font-family: "Geist", "Segoe UI", Arial, sans-serif; + font-size: 0.92rem; + font-weight: 700; + box-shadow: none !important; +} + +button:hover, +.ghost-button:hover { + filter: brightness(1.08); +} + +button:disabled, +.ghost-button:disabled { + cursor: not-allowed; + opacity: 0.58; +} + +.ghost-button, +.details-toggle button, +.admin-toolbar-actions button, +.portal-workspace-switch button:not(.is-active), +.settings-action-button.ghost-button { + background: rgba(255, 255, 255, 0.035) !important; + color: var(--ops-text) !important; +} + +.error-banner, +.status-banner, +.action-message { + border-radius: var(--ops-radius); + border: 1px solid var(--ops-line-soft); + background: rgba(255, 255, 255, 0.035); + color: var(--ops-muted); +} + +.error-banner { + border-color: rgba(255, 141, 141, 0.35); + background: rgba(122, 36, 53, 0.35); + color: var(--ops-red); +} + +.loading-center { + min-height: 180px; + place-items: center; +} + +.spinner { + border-color: rgba(126, 215, 255, 0.18); + border-top-color: var(--ops-cyan); +} + +.loading-text { + color: var(--ops-muted); + font-family: "JetBrains Mono", Consolas, monospace; +} + +.auth-screen { + display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(360px, 520px); + gap: 32px; + align-items: center; + min-height: calc(100vh - 210px); + padding: 36px 0; +} + +.auth-hero { + display: grid; + gap: 28px; + min-height: 520px; + align-content: center; + padding: 42px; + border: 1px solid var(--ops-line); + border-radius: var(--ops-radius-lg); + background: + linear-gradient(90deg, rgba(8, 13, 28, 0.94), rgba(8, 13, 28, 0.72)), + repeating-linear-gradient(90deg, rgba(126, 215, 255, 0.04) 0 1px, transparent 1px 56px), + linear-gradient(140deg, rgba(90, 80, 240, 0.16), transparent 55%); +} + +.auth-mark { + width: 86px; + height: 86px; + display: grid; + place-items: center; + border: 1px solid rgba(198, 193, 255, 0.22); + border-radius: var(--ops-radius-lg); + background: rgba(255, 255, 255, 0.035); +} + +.auth-mark .brand-logo--login { + width: 64px; + height: 64px; + margin: 0; +} + +.auth-title-block { + display: grid; + gap: 14px; + max-width: 620px; +} + +.auth-title-block h1 { + text-transform: capitalize; +} + +.auth-title-block p { + color: var(--ops-muted); + font-size: 1.15rem; + max-width: 34rem; +} + +.auth-panel { + padding: 28px; + border: 1px solid var(--ops-line); + border-radius: var(--ops-radius-lg); + background: + radial-gradient(circle at top right, rgba(126, 215, 255, 0.08), transparent 20rem), + var(--ops-panel); +} + +.auth-form label, +.admin-form label, +.compact-form label, +.profile-section label, +.filter, +.users-page-toolbar-group, +.user-directory-search label { + display: grid; + gap: 8px; + color: var(--ops-muted); + font-family: "JetBrains Mono", Consolas, monospace; + font-size: 0.76rem; + font-weight: 700; + text-align: left; + text-transform: uppercase; +} + +.auth-footnote { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--ops-cyan); + font-family: "JetBrains Mono", Consolas, monospace; + font-size: 0.78rem; +} + +.live-dot, +.system-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--ops-cyan); + box-shadow: 0 0 14px rgba(126, 215, 255, 0.64); +} + +.system-dot-up, +.system-up .system-dot { + background: var(--ops-green); +} + +.system-dot-down, +.system-down .system-dot { + background: var(--ops-red); +} + +.system-dot-degraded, +.system-degraded .system-dot { + background: var(--ops-warn); +} + +.system-dot-not_configured, +.system-not_configured .system-dot, +.system-disabled .system-dot, +.system-unknown .system-dot { + background: var(--ops-faint); + box-shadow: none; +} + +.ops-metric-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 14px; +} + +.ops-metric-card { + display: grid; + gap: 8px; + min-height: 150px; + padding: 18px; + border: 1px solid var(--ops-line); + border-radius: var(--ops-radius-lg); + background: + linear-gradient(145deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0.012)), + var(--ops-panel); +} + +.ops-metric-card strong { + color: var(--ops-primary-2); + font-size: clamp(2rem, 4vw, 3.25rem); + line-height: 1; +} + +.ops-metric-card p { + margin: 0; + color: var(--ops-muted); +} + +.layout-grid { + grid-template-columns: minmax(0, 1.35fr) minmax(340px, 0.65fr); + align-items: start; +} + +.find-panel, +.recent.centerpiece { + border: 1px solid var(--ops-line); + border-radius: var(--ops-radius-lg); + background: rgba(255, 255, 255, 0.025); + padding: 18px; +} + +.system-status, +.recent-header, +.find-header, +.section-header, +.user-directory-panel-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; +} + +.system-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.system-list, +.service-ecosystem, +.recent-grid, +.portal-item-list, +.portal-comment-list, +.quick-action-grid { + display: grid; + gap: 10px; +} + +.system-item, +.service-row, +.recent-card, +.admin-table-row, +.user-directory-row, +.portal-item-row, +.portal-discovery-item, +.connection-item { + border: 1px solid var(--ops-line-soft) !important; + border-radius: var(--ops-radius) !important; + background: rgba(255, 255, 255, 0.032) !important; + color: var(--ops-text) !important; + box-shadow: none !important; +} + +.system-item, +.service-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + padding: 12px; + text-decoration: none; +} + +.system-meta, +.service-row span:nth-child(2) { + display: grid; + gap: 2px; + min-width: 0; +} + +.system-name, +.service-row strong { + color: var(--ops-text); +} + +.system-test-message, +.service-row small { + color: var(--ops-muted); + font-size: 0.78rem; +} + +.system-actions { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.system-state, +.small-pill, +.user-grid-pill { + display: inline-flex; + align-items: center; + gap: 6px; + min-height: 24px; + padding: 4px 8px; + border-radius: var(--ops-radius); + border: 1px solid rgba(126, 215, 255, 0.22); + background: rgba(14, 165, 233, 0.14); + color: var(--ops-cyan); + font-family: "JetBrains Mono", Consolas, monospace; + font-size: 0.7rem; + font-weight: 700; + text-transform: uppercase; +} + +.system-pill-up, +.system-pill-online, +.small-pill.is-positive { + border-color: rgba(133, 239, 172, 0.28); + background: rgba(34, 197, 94, 0.14); + color: var(--ops-green); +} + +.system-pill-down, +.system-pill-failed, +.user-grid-pill.is-blocked { + border-color: rgba(255, 141, 141, 0.34); + background: rgba(239, 68, 68, 0.16); + color: var(--ops-red); +} + +.system-pill-degraded, +.system-pill-warning { + border-color: rgba(255, 208, 130, 0.34); + background: rgba(245, 158, 11, 0.14); + color: var(--ops-warn); +} + +.small-pill.is-muted, +.user-grid-pill.is-disabled, +.system-pill-not_configured, +.system-pill-unknown, +.system-pill-idle { + border-color: rgba(174, 183, 206, 0.2); + background: rgba(174, 183, 206, 0.08); + color: var(--ops-muted); +} + +.recent-card { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + text-align: left; +} + +.recent-poster, +.request-poster { + border-radius: var(--ops-radius) !important; + border: 1px solid var(--ops-line); +} + +.search, +.portal-discovery-form { + grid-template-columns: minmax(0, 1fr) auto; +} + +.filters-compact, +.portal-toolbar, +.admin-toolbar, +.users-page-toolbar, +.user-directory-search-panel, +.user-directory-bulk-panel { + border-radius: var(--ops-radius-lg); + border: 1px solid var(--ops-line); + background: rgba(255, 255, 255, 0.024); +} + +.pill-group button { + background: rgba(14, 165, 233, 0.12); + color: var(--ops-cyan); + border-color: rgba(126, 215, 255, 0.2); +} + +.admin-shell { + display: grid; + grid-template-columns: minmax(210px, 250px) minmax(0, 1fr); + grid-template-areas: "nav main"; + gap: 18px; + align-items: start; +} + +.admin-shell.admin-shell--with-rail { + grid-template-columns: minmax(210px, 250px) minmax(0, 1fr) minmax(300px, 380px); + grid-template-areas: "nav main rail"; +} + +.admin-shell-nav { + grid-area: nav; + position: sticky; + top: 116px; +} + +.admin-card { + grid-area: main; +} + +.admin-shell-rail { + grid-area: rail; + position: sticky; + top: 116px; +} + +.admin-sidebar { + gap: 18px; + padding: 14px; + border-radius: var(--ops-radius-lg); + border: 1px solid var(--ops-line); + background: rgba(255, 255, 255, 0.026); +} + +.admin-nav-links a { + display: flex; + align-items: center; + min-height: 36px; + border-radius: var(--ops-radius); + background: transparent; + color: var(--ops-muted); +} + +.admin-nav-links a:hover, +.admin-nav-links a.is-active { + border-color: rgba(126, 215, 255, 0.22); + background: rgba(14, 165, 233, 0.13); + color: var(--ops-text); +} + +.admin-header { + padding-bottom: 18px; + border-bottom: 1px solid var(--ops-line-soft); +} + +.admin-header h1 { + margin-top: 8px; +} + +.admin-zone { + padding: 18px; +} + +.admin-zone-stack { + display: grid; + gap: 16px; +} + +.admin-table { + overflow-x: auto; +} + +.admin-table-head, +.admin-table-row, +.user-directory-header, +.user-directory-row, +.cache-row { + grid-template-columns: minmax(260px, 2fr) minmax(140px, 1fr) minmax(150px, 1fr) minmax(170px, 1fr); +} + +.admin-table-head, +.user-directory-header, +.cache-head { + color: var(--ops-muted); + font-family: "JetBrains Mono", Consolas, monospace; + font-size: 0.72rem; + font-weight: 700; + text-transform: uppercase; +} + +.admin-table-row, +.user-directory-row { + min-width: 0; + padding: 12px 14px; +} + +.admin-table-head { + min-width: 0; +} + +.admin-pagination { + color: var(--ops-muted); +} + +.dashboard-activity-table .admin-table-row span:first-child { + font-weight: 700; +} + +.dashboard-activity-table .admin-table-head, +.dashboard-activity-table .admin-table-row { + grid-template-columns: minmax(150px, 1.45fr) minmax(120px, 0.9fr) minmax(72px, 0.55fr) minmax(135px, 0.9fr); +} + +.dashboard-activity-table .admin-table-row span, +.dashboard-activity-table .admin-table-head span { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.quick-action-grid { + grid-template-columns: 1fr; +} + +.quick-action-grid a { + padding: 10px 12px; + border: 1px solid var(--ops-line-soft); + border-radius: var(--ops-radius); + background: rgba(255, 255, 255, 0.032); + color: var(--ops-text); + text-decoration: none; +} + +.ops-status-strip { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 10px; +} + +.ops-status-strip span { + padding: 12px; + border: 1px solid var(--ops-line-soft); + border-radius: var(--ops-radius); + color: var(--ops-muted); + font-family: "JetBrains Mono", Consolas, monospace; + text-transform: uppercase; +} + +.portal-page { + gap: 18px; +} + +.portal-page > .user-directory-panel-header:first-child { + min-height: 190px; + padding: 28px; + border: 1px solid var(--ops-line); + border-radius: var(--ops-radius-lg); + background: + linear-gradient(135deg, rgba(90, 80, 240, 0.2), transparent 42%), + var(--ops-panel); +} + +.portal-workspace-switch { + display: flex; + gap: 8px; +} + +.portal-workspace-switch button.is-active { + background: rgba(14, 165, 233, 0.18); + color: var(--ops-cyan); +} + +.portal-overview-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.portal-overview-card { + min-height: 110px; + padding: 16px; +} + +.portal-overview-card strong { + color: var(--ops-primary-2); + font-size: 2rem; +} + +.portal-workspace { + grid-template-columns: minmax(320px, 420px) minmax(0, 1fr); + gap: 14px; +} + +.portal-item-row { + width: 100%; + justify-content: stretch; +} + +.portal-item-row.is-active { + border-color: rgba(126, 215, 255, 0.62) !important; + background: rgba(14, 165, 233, 0.12) !important; +} + +.portal-item-row-title strong, +.portal-comment-card strong { + color: var(--ops-text); +} + +.portal-item-row p, +.portal-item-row-meta, +.portal-comment-card header { + color: var(--ops-muted); +} + +.portal-form-grid, +.settings-grid, +.compact-form { + gap: 14px; +} + +.portal-comments-block { + border-top-color: var(--ops-line); +} + +.request-detail-page { + gap: 22px; +} + +.request-header { + align-items: center; + padding-bottom: 18px; + border-bottom: 1px solid var(--ops-line-soft); +} + +.request-header-main { + align-items: center; +} + +.status-box { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.status-text { + color: var(--ops-primary-2); + font-size: clamp(1.5rem, 3vw, 2.8rem); +} + +.pipeline-map, +.actions, +.history, +.request-error-state { + display: grid; + gap: 14px; + padding: 18px; + border: 1px solid var(--ops-line); + border-radius: var(--ops-radius-lg); + background: rgba(255, 255, 255, 0.024); +} + +.pipeline-steps { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 10px; +} + +.pipeline-step { + display: grid; + gap: 8px; + justify-items: center; + padding: 12px 8px; + border: 1px solid var(--ops-line-soft); + border-radius: var(--ops-radius); + color: var(--ops-muted); + text-align: center; +} + +.pipeline-step.is-active { + border-color: rgba(126, 215, 255, 0.48); + background: rgba(14, 165, 233, 0.14); + color: var(--ops-cyan); +} + +.pipeline-step.is-complete { + color: var(--ops-green); +} + +.pipeline-dot, +.timeline-marker { + background: var(--ops-cyan); +} + +.timeline::before { + background: linear-gradient(180deg, var(--ops-cyan), transparent); +} + +.timeline-card pre, +.log-viewer { + border: 1px solid var(--ops-line-soft); + border-radius: var(--ops-radius); + background: #050914; + color: #d9e4ff; +} + +.timeline-title { + color: var(--ops-muted); + font-family: "JetBrains Mono", Consolas, monospace; +} + +.timeline-sublist li { + border-bottom: 1px solid var(--ops-line-soft); + padding-bottom: 8px; +} + +.request-error-state { + min-height: 340px; + align-content: center; + justify-items: start; +} + +.request-error-state h1 { + max-width: 680px; +} + +.request-error-state p { + max-width: 640px; + color: var(--ops-muted); +} + +.request-error-actions { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.settings-nav, +.settings-links, +.settings-section-actions, +.user-bulk-actions, +.users-page-toolbar-actions, +.admin-toolbar-actions { + gap: 10px; +} + +.settings-links a { + border-radius: var(--ops-radius); + border: 1px solid var(--ops-line-soft); + background: rgba(255, 255, 255, 0.028); +} + +.settings-links a.is-active { + border-color: rgba(126, 215, 255, 0.38); + background: rgba(14, 165, 233, 0.13); + color: var(--ops-cyan); +} + +.settings-section-actions { + padding-top: 14px; + border-top: 1px solid var(--ops-line-soft); +} + +.service-status-panel { + display: grid; + grid-template-columns: minmax(0, 0.82fr) minmax(520px, 1fr); + gap: 18px; + align-items: center; +} + +.service-status-summary { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 14px; + align-items: center; +} + +.service-status-summary .system-dot { + width: 14px; + height: 14px; +} + +.service-status-grid { + display: grid; + grid-template-columns: minmax(88px, 0.65fr) minmax(130px, 0.85fr) minmax(190px, 1.15fr) minmax(132px, auto); + gap: 10px; + align-items: stretch; +} + +.service-status-grid > div { + display: grid; + gap: 4px; + min-width: 0; + padding: 12px; + border: 1px solid var(--ops-line-soft); + border-radius: var(--ops-radius); + background: rgba(255, 255, 255, 0.03); +} + +.service-status-grid span { + color: var(--ops-muted); + font-family: "JetBrains Mono", Consolas, monospace; + font-size: 0.72rem; + text-transform: uppercase; +} + +.service-status-grid strong { + overflow-wrap: normal; + color: var(--ops-text); + text-transform: capitalize; + white-space: nowrap; +} + +.settings-inline-field { + min-width: min(100%, 280px); +} + +.field-span-full { + grid-column: 1 / -1; +} + +.users-page-toolbar-grid, +.users-summary-grid, +.users-page-overview-grid { + gap: 12px; +} + +.user-directory-list { + display: grid; + gap: 8px; + overflow-x: auto; +} + +.user-directory-row { + text-decoration: none; +} + +.user-directory-row-chevron { + color: var(--ops-cyan); + font-family: "JetBrains Mono", Consolas, monospace; + font-size: 0.72rem; + text-transform: uppercase; +} + +.modal-backdrop { + background: rgba(3, 7, 18, 0.72); + backdrop-filter: blur(10px); +} + +@media (max-width: 1180px) { + .ops-metric-grid, + .portal-overview-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .layout-grid, + .admin-shell, + .admin-shell.admin-shell--with-rail, + .portal-workspace { + grid-template-columns: 1fr; + grid-template-areas: + "nav" + "main" + "rail"; + } + + .admin-shell-nav, + .admin-shell-rail { + position: static; + } + + .admin-sidebar { + display: flex; + overflow-x: auto; + } + + .service-status-panel, + .service-status-grid { + grid-template-columns: 1fr; + } + + .service-status-grid strong { + white-space: normal; + overflow-wrap: anywhere; + } + + .admin-nav-group { + min-width: 190px; + } + + .side-panel { + position: static; + } +} + +@media (max-width: 780px) { + body { + font-size: 14px; + } + + .page { + padding: 0 14px 90px; + } + + .header { + grid-template-columns: minmax(0, 1fr) auto !important; + margin: 0 -14px; + padding: 12px 14px; + backdrop-filter: none; + } + + .tagline { + display: none; + } + + .brand-logo--header { + width: 28px; + height: 28px; + } + + .header-nav { + grid-column: auto !important; + grid-row: auto !important; + position: fixed !important; + left: 0; + right: 0; + top: auto !important; + bottom: 0; + z-index: 1000; + margin: 0; + padding: 8px 10px; + border-top: 1px solid var(--ops-line); + background: rgba(8, 14, 31, 0.96); + backdrop-filter: blur(18px); + } + + .header-actions { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 6px; + } + + .header-actions a { + display: grid; + gap: 3px; + min-height: 52px; + padding: 6px 4px; + font-size: 0.67rem; + } + + .beta-chip { + min-height: 28px; + padding: 4px 9px; + } + + .header-right { + grid-column: 2 / 3 !important; + grid-row: 1 / 2 !important; + width: auto !important; + justify-content: flex-end !important; + } + + .card, + .admin-card { + padding: 16px; + } + + .auth-screen { + grid-template-columns: 1fr; + gap: 16px; + min-height: auto; + padding: 18px 0; + } + + .auth-hero { + min-height: auto; + padding: 24px; + } + + .auth-mark { + width: 64px; + height: 64px; + } + + .auth-mark .brand-logo--login { + width: 44px; + height: 44px; + } + + .ops-metric-grid, + .portal-overview-grid, + .status-box, + .history-grid, + .summary, + .ops-status-strip, + .pipeline-steps { + grid-template-columns: 1fr; + } + + .search, + .portal-discovery-form, + .portal-toolbar, + .portal-form-grid, + .settings-grid { + grid-template-columns: 1fr; + } + + .portal-field-span-2, + .field-span-full { + grid-column: auto; + } + + .system-status, + .recent-header, + .find-header, + .section-header, + .user-directory-panel-header, + .admin-header, + .admin-toolbar, + .request-header { + flex-direction: column; + align-items: stretch; + } + + .service-status-panel, + .service-status-grid { + grid-template-columns: 1fr; + } + + .admin-table-head, + .admin-table-row, + .user-directory-header, + .user-directory-row, + .cache-row { + min-width: 0; + grid-template-columns: 1fr; + } +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index ea1117e..f5ec606 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -362,8 +362,42 @@ export default function HomePage() { return date.toLocaleString() } + const serviceItems = servicesStatus?.services ?? [] + const serviceUpCount = serviceItems.filter((service) => service.status === 'up').length + const serviceAttentionCount = serviceItems.filter((service) => + ['down', 'degraded', 'not_configured'].includes(service.status) + ).length + const activeRecentCount = recent.filter((item) => { + const label = String(item.statusLabel ?? '').toLowerCase() + return !label.includes('ready') && !label.includes('available') && !label.includes('declined') + }).length + return (
+
+
+ Service mesh + + {serviceUpCount}/{serviceItems.length || 0} + +

{servicesLoading ? 'Checking services now.' : 'Configured services online.'}

+
+
+ Attention + {serviceAttentionCount} +

Services reporting down, degraded, or not configured.

+
+
+ Loaded requests + {recent.length} +

Returned by the live request cache.

+
+
+ Active queue + {activeRecentCount} +

Loaded requests still moving through the pipeline.

+
+
diff --git a/frontend/app/requests/[id]/page.tsx b/frontend/app/requests/[id]/page.tsx index cfa41d6..cf1b66c 100644 --- a/frontend/app/requests/[id]/page.tsx +++ b/frontend/app/requests/[id]/page.tsx @@ -374,7 +374,7 @@ export default function RequestTimelinePage() { if (loading) { return ( -
+