import { useState, useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom';
import { RefreshCw } from 'lucide-react';
import { toast } from 'sonner';
import { api } from '@/api';
import { cn, fmtUptime, fmtBytes } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { MarkdownText } from '@/components/MarkdownText';
// ─── Skeleton Card ────────────────────────────────────────────────────────────
function SkeletonCard() {
return (
{[1, 2, 3].map((i) => (
))}
);
}
// ─── Stat Row ─────────────────────────────────────────────────────────────────
function StatRow({ label, value, last }) {
return (
{label}
{value ?? '—'}
);
}
function formatMemory(runtime) {
if (runtime.memory_used_bytes ?? runtime.memory?.used ?? runtime.memory) {
return fmtBytes(runtime.memory_used_bytes ?? runtime.memory?.used ?? runtime.memory);
}
if (runtime.memory_mb) return `${runtime.memory_mb} MB`;
return null;
}
function formatBytesMaybe(value) {
return value === undefined || value === null ? null : fmtBytes(value);
}
function formatDateTime(value) {
if (!value) return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return date.toLocaleString();
}
function StatusPill({ tone = 'muted', children }) {
const cls = {
good: 'bg-emerald-500/10 text-emerald-500 border-emerald-500/25',
warn: 'bg-amber-500/10 text-amber-500 border-amber-500/25',
bad: 'bg-red-500/10 text-red-500 border-red-500/25',
muted: 'bg-muted text-muted-foreground border-border',
}[tone];
return (
{children}
);
}
function StatusCard({ title, status, tone = 'muted', children }) {
return (
{title}
{status && {status}}
{children}
);
}
const CATEGORY_ORDER = ['Added', 'Changed', 'Fixed', 'Removed', 'Deprecated', 'Security'];
// ─── Release Notes Section ────────────────────────────────────────────────────
function ReleaseNotesSection({ version, historyMeta }) {
if (!version) {
return (
);
}
// Group notes by category, preserving CATEGORY_ORDER priority
const grouped = CATEGORY_ORDER.reduce((acc, cat) => {
const notes = version.notes?.filter((n) => n.category === cat) ?? [];
if (notes.length) acc[cat] = notes;
return acc;
}, {});
// Catch any categories not in CATEGORY_ORDER
version.notes?.forEach((n) => {
if (!grouped[n.category]) {
grouped[n.category] = [...(grouped[n.category] ?? []), n];
}
});
const categories = Object.keys(grouped);
const preview = categories.length ? grouped[categories[0]]?.[0]?.text : null;
return (
);
}
// ─── StatusPage ───────────────────────────────────────────────────────────────
export default function StatusPage() {
const [data, setData] = useState(null);
const [version, setVersion] = useState(null);
const [historyMeta, setHistoryMeta] = useState(null);
const [loading, setLoading] = useState(true);
const load = useCallback(async () => {
setLoading(true);
setVersion(null);
setHistoryMeta(null);
try {
const [statusData, versionData] = await Promise.all([
api.status(),
api.version(),
]);
setData(statusData);
setVersion(versionData);
try {
const historyData = await api.releaseHistory();
setHistoryMeta({
version: historyData.version,
updated_at: historyData.updated_at,
});
} catch {
setHistoryMeta(null);
}
} catch (err) {
toast.error(err.message || 'Failed to load status.');
} finally {
setLoading(false);
}
}, []);
useEffect(() => { load(); }, [load]);
// Flatten nested status shape gracefully
const app = data?.application ?? data?.app ?? {};
const rt = data?.runtime ?? {};
const db = data?.database ?? data?.db ?? {};
const stats = data?.statistics ?? data?.stats ?? {};
const worker = data?.worker ?? data?.jobs ?? {};
const notifications = data?.notifications ?? data?.email ?? {};
const backups = data?.backups ?? data?.backup ?? {};
const server = data?.server ?? data?.clock ?? {};
const tracker = data?.tracker ?? data?.tracker_health ?? {};
const cleanup = data?.cleanup ?? {};
const errors = data?.errors ?? data?.recent_errors ?? [];
const dbOk = db.connected ?? (db.status === 'connected') ?? true;
const workerOk = worker.running ?? worker.enabled ?? null;
const notificationsConfigured = notifications.configured ?? notifications.smtp_configured ?? null;
const backupsEnabled = backups.enabled ?? null;
const recentErrors = Array.isArray(errors) ? errors : [];
return (
{/* Page header — flat on background */}
Server Status
{data ? 'All systems operational' : 'Loading…'}
{/* 2x2 grid of stat cards */}
{loading ? (
) : (
{/* Application */}
{/* Runtime */}
{/* Database */}
File path
{db.file ?? db.path ?? db.filename ?? '—'}
{/* Statistics */}
)}
{!loading && (
<>
Operations
{recentErrors.length ? (
recentErrors.slice(0, 4).map((err, index) => (
{err.source ?? err.type ?? 'Application'}
{err.message ?? String(err)}
))
) : (
<>
>
)}
>
)}
{/* Release Notes */}
);
}