import React, { useState, useEffect, useCallback } from 'react';
import { Link } from 'react-router-dom';
import {
Activity, AlertCircle, ArrowUpCircle, BarChart3, Bell, CalendarClock,
CheckCircle2, Clock, Cpu, Database, Globe, HardDrive, Landmark,
RefreshCw, ScrollText, Wrench,
} 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';
// ─── Helpers ──────────────────────────────────────────────────────────────────
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();
}
// ─── Design primitives ────────────────────────────────────────────────────────
function SectionLabel({ children }) {
return (
);
}
function StatRow({ label, value, last }) {
return (
{label}
{value ?? —}
);
}
function StatusPill({ tone = 'muted', children }) {
const cls = {
good: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/25',
warn: 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-400/30',
bad: 'bg-red-500/10 text-red-600 dark:text-red-400 border-red-500/25',
muted: 'bg-muted/60 text-muted-foreground border-border',
}[tone] ?? 'bg-muted/60 text-muted-foreground border-border';
return (
{children}
);
}
function StatusCard({ title, icon: Icon, status, tone = 'muted', children, className }) {
const accentCls = {
good: 'border-t-emerald-500/60',
warn: 'border-t-amber-400/70',
bad: 'border-t-red-500/60',
muted: 'border-t-border/60',
}[tone] ?? 'border-t-border/60';
return (
{Icon && }
{title}
{status != null &&
{status}}
{children}
);
}
function SkeletonCard() {
return (
{[1, 2, 3].map(i => (
))}
);
}
// ─── Update Card ──────────────────────────────────────────────────────────────
const CATEGORY_ORDER = ['Added', 'Changed', 'Fixed', 'Removed', 'Deprecated', 'Security'];
function UpdateCard({ update, onCheckNow, checking }) {
const hasUpdate = !!update.has_update;
const isKnown = update.up_to_date !== null && update.up_to_date !== undefined;
const hasError = !!update.error;
const tone = hasUpdate ? 'warn' : isKnown && !hasError ? 'good' : 'muted';
const status = hasUpdate ? 'Update Available'
: hasError ? 'Check Failed'
: isKnown ? 'Up to Date'
: 'Unknown';
const StatusIcon = hasUpdate ? ArrowUpCircle : hasError ? AlertCircle : CheckCircle2;
const iconCls = hasUpdate ? 'text-amber-500' : hasError ? 'text-red-500' : isKnown ? 'text-emerald-500' : 'text-muted-foreground';
return (
{hasUpdate ? `v${update.latest_version} is available`
: isKnown && !hasError ? 'Running the latest version'
: hasError ? 'Could not reach update server'
: 'Update status unknown'}
Latest
{update.latest_version ? (
update.latest_release_url ? (
v{update.latest_version} ↗
) : (
v{update.latest_version}
)
) : (
—
)}
{update.error && (
{update.error}
)}
);
}
// ─── Release Notes Card ───────────────────────────────────────────────────────
function ReleaseNotesCard({ version, historyMeta }) {
if (!version) return ;
const grouped = CATEGORY_ORDER.reduce((acc, cat) => {
const notes = version.notes?.filter(n => n.category === cat) ?? [];
if (notes.length) acc[cat] = notes;
return acc;
}, {});
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 [updateData, setUpdateData] = useState(null);
const [updateChecking, setUpdateChecking] = useState(false);
const load = useCallback(async () => {
setLoading(true);
setVersion(null);
setHistoryMeta(null);
try {
const [statusData, versionData] = await Promise.all([api.status(), api.version()]);
setData(statusData);
setUpdateData(statusData?.update ?? null);
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]);
const handleCheckNow = useCallback(async () => {
setUpdateChecking(true);
try {
setUpdateData(await api.checkForUpdates());
} catch (err) {
toast.error(err.message || 'Update check failed');
} finally {
setUpdateChecking(false);
}
}, []);
// Normalize the nested response shape
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 bankSync = data?.bank_sync ?? data?.simplefin ?? {};
const errors = data?.errors ?? data?.recent_errors ?? [];
const dbOk = db.connected ?? (db.status === 'connected') ?? true;
const workerOk = !worker.enabled ? null : worker.last_error ? false : true;
const notificationsConfigured = notifications.configured ?? notifications.smtp_configured ?? null;
const backupsEnabled = backups.enabled ?? null;
const backupsScheduled = backups.scheduled_enabled ?? null;
const backupsStatus = backupsEnabled === null ? 'Pending'
: !backupsEnabled ? 'Disabled'
: backupsScheduled ? 'Scheduled'
: 'Manual Only';
const backupsTone = backupsEnabled === null ? 'muted'
: !backupsEnabled ? 'warn'
: backupsScheduled ? 'good'
: 'warn';
const recentErrors = Array.isArray(errors) ? errors : [];
const bankSyncEnabled = bankSync.enabled ?? false;
const bankSyncStatus = !bankSyncEnabled ? 'Disabled'
: bankSync.running ? 'Syncing'
: bankSync.last_error ? 'Error'
: (bankSync.source_count ?? 0) > 0 ? 'Connected'
: 'No Sources';
const bankSyncTone = !bankSyncEnabled ? 'muted'
: bankSync.last_error ? 'warn'
: (bankSync.source_count ?? 0) > 0 ? 'good'
: 'warn';
const utcOffset = server.utc_offset != null
? `UTC${server.utc_offset >= 0 ? '+' : ''}${server.utc_offset}`
: null;
return (
{/* ── Page header ──────────────────────────────────────────────── */}
Server Status
Health and operational status
{/* ── Health banner ────────────────────────────────────────────── */}
{!loading && data && (
{data.ok ? 'All systems operational' : 'One or more systems need attention'}
{(app.version ?? data?.version) && v{app.version ?? data?.version}}
{fmtUptime(app.uptime_seconds ?? data?.uptime_seconds ?? 0)} uptime
)}
{/* ── Loading skeleton ─────────────────────────────────────────── */}
{loading ? (
<>
{[3, 5].map((count, si) => (
{Array.from({ length: count }).map((_, i) => )}
))}
>
) : (
<>
{/* ── Infrastructure ─────────────────────────────────────────── */}
Infrastructure
{dbOk
?
: }
{/* ── Services ───────────────────────────────────────────────── */}
Services
{/* ── App Health ─────────────────────────────────────────────── */}
App Health
{/* ── Software ───────────────────────────────────────────────── */}
Software
{updateData && (
)}
{/* ── Errors ─────────────────────────────────────────────────── */}
Errors
{recentErrors.length ? (
recentErrors.slice(0, 5).map((err, i) => (
{err.source ?? err.type ?? 'Application'}
{err.timestamp && (
{formatDateTime(err.timestamp)}
)}
{err.message ?? String(err)}
))
) : (
No recent errors recorded.
)}
>
)}
);
}