Rename Uptime tab to States; add RAM/disk progress bars per service

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-14 09:04:23 +00:00
parent f36fd83196
commit 43a54e8b4c

View file

@ -25,10 +25,36 @@ function relativeTime(iso: string | null | undefined): string | null {
return new Date(iso).toLocaleDateString(undefined, { day: 'numeric', month: 'short' }) return new Date(iso).toLocaleDateString(undefined, { day: 'numeric', month: 'short' })
} }
interface StatRange { total: number; used: number }
interface HealthStatus { interface HealthStatus {
name: string name: string
up: boolean up: boolean
ms: number | null ms: number | null
ram: StatRange | null
disk: StatRange | null
}
function formatBytes(b: number): string {
if (b >= 1_073_741_824) return `${(b / 1_073_741_824).toFixed(1)} GB`
if (b >= 1_048_576) return `${(b / 1_048_576).toFixed(0)} MB`
return `${(b / 1024).toFixed(0)} KB`
}
function StatBar({ label, stat }: { label: string; stat: StatRange }) {
const pct = Math.min(100, Math.round((stat.used / stat.total) * 100))
const color = pct > 85 ? 'var(--danger)' : pct > 70 ? '#d97706' : '#16a34a'
return (
<div style={{ marginTop: '0.45rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.68rem', color: 'var(--text-mid)', marginBottom: '0.2rem' }}>
<span>{label}</span>
<span>{pct}% · {formatBytes(stat.used)} / {formatBytes(stat.total)}</span>
</div>
<div style={{ height: '3px', background: 'var(--card-border)', borderRadius: '2px' }}>
<div style={{ width: `${pct}%`, height: '100%', background: color, borderRadius: '2px', transition: 'width 0.3s' }} />
</div>
</div>
)
} }
interface DeployEntry { interface DeployEntry {
@ -42,7 +68,7 @@ interface DeployEntry {
} }
export function AdminMonitor({ user }: { user: User }) { export function AdminMonitor({ user }: { user: User }) {
const [tab, setTab] = useState<'updates' | 'uptime' | 'deploys'>('updates') const [tab, setTab] = useState<'updates' | 'states' | 'deploys'>('updates')
const [statuses, setStatuses] = useState<AppStatus[]>([]) const [statuses, setStatuses] = useState<AppStatus[]>([])
const [deploys, setDeploys] = useState<DeployEntry[]>([]) const [deploys, setDeploys] = useState<DeployEntry[]>([])
const [health, setHealth] = useState<HealthStatus[]>([]) const [health, setHealth] = useState<HealthStatus[]>([])
@ -60,7 +86,7 @@ export function AdminMonitor({ user }: { user: User }) {
} }
} }
async function fetchHealth() { async function fetchStates() {
setLoading(true) setLoading(true)
try { try {
const res = await fetch('/deploy/health-status', { credentials: 'include' }) const res = await fetch('/deploy/health-status', { credentials: 'include' })
@ -96,9 +122,9 @@ export function AdminMonitor({ user }: { user: User }) {
useEffect(() => { useEffect(() => {
if (tab === 'updates') fetchStatus() if (tab === 'updates') fetchStatus()
if (tab === 'deploys') fetchDeploys() if (tab === 'deploys') fetchDeploys()
if (tab === 'uptime') { if (tab === 'states') {
fetchHealth() fetchStates()
const id = setInterval(fetchHealth, 30_000) const id = setInterval(fetchStates, 30_000)
return () => clearInterval(id) return () => clearInterval(id)
} }
}, [tab]) }, [tab])
@ -107,7 +133,7 @@ export function AdminMonitor({ user }: { user: User }) {
const tabs = [ const tabs = [
{ key: 'updates' as const, label: 'Updates', icon: <ArrowUpCircle size={14} strokeWidth={1.75} /> }, { key: 'updates' as const, label: 'Updates', icon: <ArrowUpCircle size={14} strokeWidth={1.75} /> },
{ key: 'uptime' as const, label: 'Uptime', icon: <Activity size={14} strokeWidth={1.75} /> }, { key: 'states' as const, label: 'States', icon: <Activity size={14} strokeWidth={1.75} /> },
{ key: 'deploys' as const, label: 'Deploy Log', icon: <ScrollText size={14} strokeWidth={1.75} /> }, { key: 'deploys' as const, label: 'Deploy Log', icon: <ScrollText size={14} strokeWidth={1.75} /> },
] ]
@ -225,12 +251,12 @@ export function AdminMonitor({ user }: { user: User }) {
</div> </div>
)} )}
{/* Uptime tab */} {/* States tab */}
{tab === 'uptime' && ( {tab === 'states' && (
<div style={{ flex: 1, overflowY: 'auto', padding: '1.5rem 2rem' }}> <div style={{ flex: 1, overflowY: 'auto', padding: '1.5rem 2rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.25rem' }}> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1.25rem' }}>
<h2 style={{ fontSize: '0.95rem', fontWeight: 600, color: 'var(--text-dark)' }}>Service Health</h2> <h2 style={{ fontSize: '0.95rem', fontWeight: 600, color: 'var(--text-dark)' }}>Service States</h2>
<button onClick={fetchHealth} disabled={loading} style={{ <button onClick={fetchStates} disabled={loading} style={{
background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: '6px', background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: '6px',
color: 'var(--text-mid)', padding: '0.35rem 0.9rem', fontSize: '0.8rem', color: 'var(--text-mid)', padding: '0.35rem 0.9rem', fontSize: '0.8rem',
display: 'flex', alignItems: 'center', gap: '0.4rem', display: 'flex', alignItems: 'center', gap: '0.4rem',
@ -242,26 +268,38 @@ export function AdminMonitor({ user }: { user: User }) {
{health.length === 0 && !loading && ( {health.length === 0 && !loading && (
<p style={{ color: 'var(--text-mid)' }}>Checking services</p> <p style={{ color: 'var(--text-mid)' }}>Checking services</p>
)} )}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}> <div style={{ display: 'flex', flexDirection: 'column', gap: '0.6rem' }}>
{health.map(h => ( {health.map(h => (
<div key={h.name} style={{ <div key={h.name} style={{
background: 'var(--card-bg)', border: '1px solid var(--card-border)', background: 'var(--card-bg)', border: '1px solid var(--card-border)',
borderLeft: `3px solid ${h.up ? '#16a34a' : 'var(--danger)'}`, borderLeft: `3px solid ${h.up ? '#16a34a' : 'var(--danger)'}`,
borderRadius: 'var(--radius)', padding: '0.7rem 1rem', borderRadius: 'var(--radius)', padding: '0.85rem 1rem',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
boxShadow: 'var(--shadow-sm)', boxShadow: 'var(--shadow-sm)',
}}> }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}> <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<span style={{ <div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
width: '8px', height: '8px', borderRadius: '50%', flexShrink: 0, <span style={{
background: h.up ? '#16a34a' : 'var(--danger)', width: '8px', height: '8px', borderRadius: '50%', flexShrink: 0,
boxShadow: h.up ? '0 0 5px #16a34a66' : undefined, background: h.up ? '#16a34a' : 'var(--danger)',
}} /> boxShadow: h.up ? '0 0 5px #16a34a66' : undefined,
<span style={{ fontWeight: 500, fontSize: '0.88rem', color: 'var(--text-dark)' }}>{h.name}</span> }} />
<span style={{ fontWeight: 600, fontSize: '0.88rem', color: 'var(--text-dark)' }}>{h.name}</span>
</div>
<span style={{ fontSize: '0.72rem', fontFamily: 'monospace', color: h.up ? '#16a34a' : 'var(--text-mid)' }}>
{h.up ? `${h.ms}ms` : 'unreachable'}
</span>
</div> </div>
<span style={{ fontSize: '0.75rem', fontFamily: 'monospace', color: h.up ? '#16a34a' : 'var(--text-mid)' }}> {(h.ram || h.disk) && (
{h.up ? `${h.ms}ms` : 'unreachable'} <div style={{ marginTop: '0.5rem' }}>
</span> {h.ram && <StatBar label="RAM" stat={h.ram} />}
{h.disk && <StatBar label="Disk" stat={h.disk} />}
</div>
)}
{h.up && !h.ram && !h.disk && (
<div style={{ marginTop: '0.4rem', fontSize: '0.68rem', color: 'var(--text-mid)' }}>
resource stats unavailable
</div>
)}
</div> </div>
))} ))}
</div> </div>