Tidy beta landing page and qBittorrent status
Magent CI/CD / verify (push) Successful in 11m0s
Magent CI/CD / deploy-prod (push) Has been skipped
Magent CI/CD / deploy-beta (push) Successful in 15s

This commit is contained in:
2026-06-21 12:06:38 +12:00
parent e6b4f99ea7
commit e163920e21
5 changed files with 217 additions and 82 deletions
+101
View File
@@ -598,6 +598,86 @@ button:disabled,
gap: 16px;
}
.system-status-dropdown {
display: block;
margin-bottom: 16px;
}
.system-status-dropdown summary {
list-style: none;
}
.system-status-dropdown summary::-webkit-details-marker {
display: none;
}
.system-summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 14px 16px;
cursor: pointer;
border: 1px solid var(--ops-line-soft);
border-radius: var(--ops-radius);
background: rgba(255, 255, 255, 0.032);
}
.system-summary-copy {
display: grid;
gap: 4px;
min-width: 0;
}
.system-summary-copy strong {
color: var(--ops-text);
font-size: 1rem;
}
.system-summary-copy span:last-child {
color: var(--ops-muted);
font-size: 0.86rem;
}
.system-summary-actions {
display: inline-flex;
align-items: center;
gap: 10px;
flex: 0 0 auto;
}
.system-dropdown-cue {
min-width: 52px;
color: var(--ops-cyan);
font-family: "JetBrains Mono", Consolas, monospace;
font-size: 0.72rem;
font-weight: 700;
text-align: right;
text-transform: uppercase;
}
.system-status-dropdown[open] .system-dropdown-cue::before {
content: "Close";
font-size: 0.72rem;
}
.system-status-dropdown[open] .system-dropdown-cue {
font-size: 0;
}
.system-status-dropdown .system-list {
margin-top: 10px;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
}
.system-status-dropdown .system-item {
grid-template-columns: auto minmax(0, 1fr) auto;
}
.system-status-dropdown .system-actions {
flex-wrap: nowrap;
}
.system-header {
display: flex;
align-items: center;
@@ -658,6 +738,26 @@ button:disabled,
font-size: 0.78rem;
}
.find-panel .find-header {
display: grid;
gap: 6px;
}
.find-panel .find-header h1 {
margin: 0;
font-size: clamp(1.12rem, 1.5vw, 1.38rem);
line-height: 1.12;
}
.find-panel h2 {
font-size: 1.22rem;
}
.find-panel .lede {
font-size: 0.9rem;
line-height: 1.45;
}
.system-actions {
display: inline-flex;
align-items: center;
@@ -679,6 +779,7 @@ button:disabled,
font-family: "JetBrains Mono", Consolas, monospace;
font-size: 0.7rem;
font-weight: 700;
white-space: nowrap;
text-transform: uppercase;
}
+71 -64
View File
@@ -367,6 +367,29 @@ export default function HomePage() {
const serviceAttentionCount = serviceItems.filter((service) =>
['down', 'degraded', 'not_configured'].includes(service.status)
).length
const serviceOverall = servicesStatus?.overall ?? 'unknown'
const serviceStatusLabel = servicesLoading
? 'Checking services...'
: servicesError
? 'Status not available yet'
: serviceOverall === 'up'
? 'Services are up and running'
: serviceOverall === 'down'
? 'Something is down'
: 'Some services need attention'
const serviceSummary = servicesError
? 'Unable to load service status'
: serviceItems.length === 0
? 'No services reported yet'
: serviceAttentionCount > 0
? `${serviceAttentionCount} of ${serviceItems.length} need attention`
: `${serviceUpCount} of ${serviceItems.length} online`
const orderedServices = ['Seerr', 'Sonarr', 'Radarr', 'Prowlarr', 'qBittorrent', 'Jellyfin'].map(
(name) => {
const item = serviceItems.find((entry) => entry.name === name)
return { name, status: item?.status ?? 'unknown', message: item?.message }
}
)
const activeRecentCount = recent.filter((item) => {
const label = String(item.statusLabel ?? '').toLowerCase()
return !label.includes('ready') && !label.includes('available') && !label.includes('declined')
@@ -400,74 +423,58 @@ export default function HomePage() {
</section>
<div className="layout-grid">
<section className="recent centerpiece">
<div className="system-status">
<div className="system-header">
<h2>System status</h2>
<span
className={`system-pill system-pill-${servicesStatus?.overall ?? 'unknown'}`}
>
{servicesLoading
? 'Checking services...'
: servicesError
? 'Status not available yet'
: servicesStatus?.overall === 'up'
? 'Services are up and running'
: servicesStatus?.overall === 'down'
? 'Something is down'
: 'Some services need attention'}
<details className="system-status system-status-dropdown">
<summary className="system-summary">
<span className="system-summary-copy">
<span className="section-kicker">System status</span>
<strong>{serviceSummary}</strong>
<span>{serviceStatusLabel}</span>
</span>
</div>
<span className="system-summary-actions">
<span className={`system-pill system-pill-${serviceOverall}`}>
{servicesLoading ? 'Checking' : serviceOverall.replaceAll('_', ' ')}
</span>
<span className="system-dropdown-cue" aria-hidden="true">Open</span>
</span>
</summary>
<div className="system-list">
{(() => {
const order = [
'Seerr',
'Sonarr',
'Radarr',
'Prowlarr',
'qBittorrent',
'Jellyfin',
]
const items = servicesStatus?.services ?? []
return order.map((name) => {
const item = items.find((entry) => entry.name === name)
const status = item?.status ?? 'unknown'
const testing = serviceTesting[name] ?? false
return (
<div key={name} className={`system-item system-${status}`}>
<span className="system-dot" />
<div className="system-meta">
<span className="system-name">{name}</span>
{serviceTestResults[name] && (
<span className="system-test-message">{serviceTestResults[name]}</span>
)}
</div>
<div className="system-actions">
<span className="system-state">
{status === 'up'
? 'Up'
: status === 'down'
? 'Down'
: status === 'degraded'
? 'Needs attention'
: status === 'not_configured'
? 'Not configured'
: 'Unknown'}
</span>
<button
type="button"
className="system-test"
onClick={() => void testService(name)}
disabled={testing}
>
{testing ? 'Testing...' : 'Test'}
</button>
</div>
{orderedServices.map(({ name, status, message }) => {
const testing = serviceTesting[name] ?? false
return (
<div key={name} className={`system-item system-${status}`}>
<span className="system-dot" />
<div className="system-meta">
<span className="system-name">{name}</span>
<span className="system-test-message">
{serviceTestResults[name] ?? message ?? 'No recent detail'}
</span>
</div>
)
})
})()}
<div className="system-actions">
<span className="system-state">
{status === 'up'
? 'Up'
: status === 'down'
? 'Down'
: status === 'degraded'
? 'Needs attention'
: status === 'not_configured'
? 'Not configured'
: 'Unknown'}
</span>
<button
type="button"
className="system-test"
onClick={() => void testService(name)}
disabled={testing}
>
{testing ? 'Testing...' : 'Test'}
</button>
</div>
</div>
)
})}
</div>
</div>
</details>
<div className="recent-header">
<h2>{role === 'admin' ? 'All requests' : 'My recent requests'}</h2>
{authReady && (
+28 -17
View File
@@ -38,41 +38,52 @@ export default function HeaderActions() {
return null
}
const roleItems =
role === null
? []
: role === 'admin'
? [
{
href: '/admin',
label: 'Config',
match: (path: string) => path.startsWith('/admin'),
},
]
: [
{
href: '/profile',
label: 'Profile',
match: (path: string) => path.startsWith('/profile') && !path.startsWith('/profile/invites'),
},
{
href: '/profile/invites',
label: 'Invites',
match: (path: string) => path.startsWith('/profile/invites'),
},
]
const items = [
{ href: '/', label: 'Health', icon: '01', match: (path: string) => path === '/' },
{ href: '/', label: 'Health', match: (path: string) => path === '/' },
{
href: '/portal/requests',
label: 'Requests',
icon: '02',
match: (path: string) => path === '/portal/requests' || path.startsWith('/requests/'),
},
{
href: '/portal/issues',
label: 'Issues',
icon: '03',
match: (path: string) => path === '/portal/issues' || path === '/admin/issues',
},
{
href: role === 'admin' ? '/users' : '/profile',
label: role === 'admin' ? 'Users' : 'Profile',
icon: '04',
match: (path: string) => path.startsWith('/users') || path.startsWith('/profile'),
},
{
href: role === 'admin' ? '/admin' : '/profile/invites',
label: role === 'admin' ? 'Config' : 'Invites',
icon: '05',
match: (path: string) => path.startsWith('/admin') || path.startsWith('/profile/invites'),
},
...roleItems,
]
return (
<nav className="header-actions" aria-label="Primary">
{items.map((item) => {
{items.map((item, index) => {
const active = item.match(pathname)
return (
<a key={item.href} href={item.href} className={active ? 'is-active' : undefined}>
<span aria-hidden="true">{item.icon}</span>
<span aria-hidden="true">{String(index + 1).padStart(2, '0')}</span>
{item.label}
</a>
)