Move fleet health into admin settings and tidy landing page
This commit is contained in:
+112
-25
@@ -57,6 +57,9 @@ export default function AdminLandingPage() {
|
||||
const [portalOverview, setPortalOverview] = useState<PortalOverview | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [serviceTesting, setServiceTesting] = useState<Record<string, boolean>>({})
|
||||
const [serviceTestResults, setServiceTestResults] = useState<Record<string, string>>({})
|
||||
const [serviceCheckedAt, setServiceCheckedAt] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!getToken()) {
|
||||
@@ -95,6 +98,7 @@ export default function AdminLandingPage() {
|
||||
const data = await serviceResponse.json()
|
||||
setServiceOverall(data?.overall ?? 'unknown')
|
||||
setServices(Array.isArray(data?.services) ? data.services : [])
|
||||
setServiceCheckedAt(new Date().toISOString())
|
||||
}
|
||||
|
||||
if (recentResponse.ok) {
|
||||
@@ -115,8 +119,58 @@ export default function AdminLandingPage() {
|
||||
}
|
||||
|
||||
void load()
|
||||
|
||||
const refreshTimer = window.setInterval(async () => {
|
||||
try {
|
||||
const response = await authFetch(`${getApiBase()}/status/services`)
|
||||
if (!response.ok) return
|
||||
const data = await response.json()
|
||||
setServiceOverall(data?.overall ?? 'unknown')
|
||||
setServices(Array.isArray(data?.services) ? data.services : [])
|
||||
setServiceCheckedAt(new Date().toISOString())
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
}, 30_000)
|
||||
|
||||
return () => window.clearInterval(refreshTimer)
|
||||
}, [router])
|
||||
|
||||
const testService = async (service: ServiceState) => {
|
||||
const slug = service.name.toLowerCase().replace(/[^a-z0-9]/g, '')
|
||||
setServiceTesting((current) => ({ ...current, [service.name]: true }))
|
||||
setServiceTestResults((current) => {
|
||||
const next = { ...current }
|
||||
delete next[service.name]
|
||||
return next
|
||||
})
|
||||
try {
|
||||
const response = await authFetch(`${getApiBase()}/status/services/${slug}/test`, {
|
||||
method: 'POST',
|
||||
})
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
throw new Error(text || `Service test failed: ${response.status}`)
|
||||
}
|
||||
const result = await response.json()
|
||||
setServices((current) => current.map((item) =>
|
||||
item.name === service.name
|
||||
? { ...item, status: result?.status ?? item.status, message: result?.message ?? item.message }
|
||||
: item
|
||||
))
|
||||
setServiceTestResults((current) => ({
|
||||
...current,
|
||||
[service.name]: result?.message || (result?.status === 'up' ? 'Connection test passed.' : 'Connection test completed.'),
|
||||
}))
|
||||
setServiceCheckedAt(new Date().toISOString())
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
setServiceTestResults((current) => ({ ...current, [service.name]: 'Connection test failed.' }))
|
||||
} finally {
|
||||
setServiceTesting((current) => ({ ...current, [service.name]: false }))
|
||||
}
|
||||
}
|
||||
|
||||
const serviceCounts = useMemo(() => {
|
||||
const up = services.filter((service) => service.status === 'up').length
|
||||
const down = services.filter((service) => service.status === 'down').length
|
||||
@@ -132,27 +186,14 @@ export default function AdminLandingPage() {
|
||||
const rail = (
|
||||
<div className="admin-rail-stack">
|
||||
<div className="admin-rail-card">
|
||||
<span className="admin-rail-eyebrow">Service ecosystem</span>
|
||||
<div className="service-ecosystem">
|
||||
{services.length === 0 ? (
|
||||
<div className="status-banner">Service status is not available yet.</div>
|
||||
) : (
|
||||
services.map((service) => (
|
||||
<a
|
||||
key={service.name}
|
||||
className="service-row"
|
||||
href={`/admin/${service.name.toLowerCase().replace(/[^a-z0-9]/g, '')}`}
|
||||
>
|
||||
<span className={`system-dot system-dot-${service.status}`} />
|
||||
<span>
|
||||
<strong>{service.name}</strong>
|
||||
<small>{service.message ?? 'No message reported'}</small>
|
||||
</span>
|
||||
<span className={`small-pill system-pill-${service.status}`}>{service.status}</span>
|
||||
</a>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
<span className="admin-rail-eyebrow">Fleet summary</span>
|
||||
<h2>{serviceCounts.up} of {serviceCounts.total || 0} online</h2>
|
||||
<p>
|
||||
{serviceCounts.down + serviceCounts.degraded > 0
|
||||
? `${serviceCounts.down + serviceCounts.degraded} configured service${serviceCounts.down + serviceCounts.degraded === 1 ? '' : 's'} need attention.`
|
||||
: 'No configured service is currently reporting a fault.'}
|
||||
</p>
|
||||
<a className="admin-rail-action" href="/admin/diagnostics">Open full diagnostics</a>
|
||||
</div>
|
||||
<div className="admin-rail-card">
|
||||
<span className="admin-rail-eyebrow">Quick actions</span>
|
||||
@@ -168,12 +209,12 @@ export default function AdminLandingPage() {
|
||||
|
||||
return (
|
||||
<AdminShell
|
||||
title="Operations Center"
|
||||
subtitle="Live Magent controls, request movement, issue intake, and service health."
|
||||
title="Admin overview"
|
||||
subtitle="Service health, request movement, issue intake, and the controls that keep Magent running."
|
||||
rail={rail}
|
||||
actions={
|
||||
<button type="button" onClick={() => router.push('/')}>
|
||||
View health
|
||||
<button type="button" onClick={() => router.push('/admin/diagnostics')}>
|
||||
Run diagnostics
|
||||
</button>
|
||||
}
|
||||
>
|
||||
@@ -205,6 +246,52 @@ export default function AdminLandingPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="admin-zone fleet-status-panel">
|
||||
<div className="section-header fleet-status-header">
|
||||
<div>
|
||||
<span className="section-kicker">Fleet service mesh</span>
|
||||
<h2>System status</h2>
|
||||
<p className="section-subtitle">
|
||||
Admin-only connectivity status for the services used by Magent.
|
||||
{serviceCheckedAt ? ` Last checked ${formatDateTime(serviceCheckedAt)}.` : ''}
|
||||
</p>
|
||||
</div>
|
||||
<span className={`small-pill system-pill-${serviceOverall}`}>
|
||||
{serviceOverall.replaceAll('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
{services.length === 0 ? (
|
||||
<div className="status-banner">Service status is not available yet.</div>
|
||||
) : (
|
||||
<div className="fleet-service-grid">
|
||||
{services.map((service) => {
|
||||
const slug = service.name.toLowerCase().replace(/[^a-z0-9]/g, '')
|
||||
const testing = Boolean(serviceTesting[service.name])
|
||||
return (
|
||||
<article className={`fleet-service-card system-${service.status}`} key={service.name}>
|
||||
<div className="fleet-service-title">
|
||||
<span className="system-dot" aria-hidden="true" />
|
||||
<div>
|
||||
<h3>{service.name}</h3>
|
||||
<span className={`small-pill system-pill-${service.status}`}>
|
||||
{service.status.replaceAll('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<p>{serviceTestResults[service.name] ?? service.message ?? 'No recent detail was returned.'}</p>
|
||||
<div className="fleet-service-actions">
|
||||
<a href={`/admin/${slug}`}>Configure</a>
|
||||
<button type="button" className="ghost-button" disabled={testing} onClick={() => void testService(service)}>
|
||||
{testing ? 'Testing...' : 'Test connection'}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="admin-zone">
|
||||
<div className="section-header">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user