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' })
}
interface StatRange { total: number; used: number }
interface HealthStatus {
name: string
up: boolean
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 {
@ -42,7 +68,7 @@ interface DeployEntry {
}
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 [deploys, setDeploys] = useState<DeployEntry[]>([])
const [health, setHealth] = useState<HealthStatus[]>([])
@ -60,7 +86,7 @@ export function AdminMonitor({ user }: { user: User }) {
}
}
async function fetchHealth() {
async function fetchStates() {
setLoading(true)
try {
const res = await fetch('/deploy/health-status', { credentials: 'include' })
@ -96,9 +122,9 @@ export function AdminMonitor({ user }: { user: User }) {
useEffect(() => {
if (tab === 'updates') fetchStatus()
if (tab === 'deploys') fetchDeploys()
if (tab === 'uptime') {
fetchHealth()
const id = setInterval(fetchHealth, 30_000)
if (tab === 'states') {
fetchStates()
const id = setInterval(fetchStates, 30_000)
return () => clearInterval(id)
}
}, [tab])
@ -107,7 +133,7 @@ export function AdminMonitor({ user }: { user: User }) {
const tabs = [
{ 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} /> },
]
@ -225,12 +251,12 @@ export function AdminMonitor({ user }: { user: User }) {
</div>
)}
{/* Uptime tab */}
{tab === 'uptime' && (
{/* States tab */}
{tab === 'states' && (
<div style={{ flex: 1, overflowY: 'auto', padding: '1.5rem 2rem' }}>
<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>
<button onClick={fetchHealth} disabled={loading} style={{
<h2 style={{ fontSize: '0.95rem', fontWeight: 600, color: 'var(--text-dark)' }}>Service States</h2>
<button onClick={fetchStates} disabled={loading} style={{
background: 'var(--card-bg)', border: '1px solid var(--card-border)', borderRadius: '6px',
color: 'var(--text-mid)', padding: '0.35rem 0.9rem', fontSize: '0.8rem',
display: 'flex', alignItems: 'center', gap: '0.4rem',
@ -242,27 +268,39 @@ export function AdminMonitor({ user }: { user: User }) {
{health.length === 0 && !loading && (
<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 => (
<div key={h.name} style={{
background: 'var(--card-bg)', border: '1px solid var(--card-border)',
borderLeft: `3px solid ${h.up ? '#16a34a' : 'var(--danger)'}`,
borderRadius: 'var(--radius)', padding: '0.7rem 1rem',
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
borderRadius: 'var(--radius)', padding: '0.85rem 1rem',
boxShadow: 'var(--shadow-sm)',
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.6rem' }}>
<span style={{
width: '8px', height: '8px', borderRadius: '50%', flexShrink: 0,
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.75rem', fontFamily: 'monospace', color: h.up ? '#16a34a' : 'var(--text-mid)' }}>
<span style={{ fontSize: '0.72rem', fontFamily: 'monospace', color: h.up ? '#16a34a' : 'var(--text-mid)' }}>
{h.up ? `${h.ms}ms` : 'unreachable'}
</span>
</div>
{(h.ram || h.disk) && (
<div style={{ marginTop: '0.5rem' }}>
{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>