import React, { useState, useEffect, useCallback, type ComponentType, type ReactNode } 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, errMessage } from '@/lib/utils'; import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; import { MarkdownText } from '@/components/MarkdownText'; type Dict = Record; interface UpdateData { has_update?: boolean; up_to_date?: boolean | null; error?: string; latest_version?: string; current_version?: string; latest_release_url?: string; last_checked_at?: string; } interface Note { category?: string; text?: string; } interface VersionData { version?: string; notes?: Note[]; } interface HistoryMeta { version?: string; updated_at?: string; } interface ErrItem { timestamp?: string; message?: string; source?: string; type?: string; } // ─── Helpers ────────────────────────────────────────────────────────────────── function formatMemory(runtime: Dict): string | null { const memUsed = runtime.memory_used_bytes ?? (runtime.memory as { used?: unknown } | undefined)?.used ?? runtime.memory; if (memUsed) return fmtBytes(memUsed as number); if (runtime.memory_mb) return `${runtime.memory_mb} MB`; return null; } function formatBytesMaybe(value: unknown): string | null { return value === undefined || value === null ? null : fmtBytes(value as number); } function formatDateTime(value: unknown): string | null { if (!value) return null; const date = new Date(value as string | number); if (Number.isNaN(date.getTime())) return String(value); return date.toLocaleString(); } // ─── Design primitives ──────────────────────────────────────────────────────── function SectionLabel({ children }: { children: ReactNode }) { return (
{children}
); } function StatRow({ label, value, last }: { label: ReactNode; value?: unknown; last?: boolean }) { return (
{label} {value == null ? : (value as ReactNode)}
); } function StatusPill({ tone = 'muted', children }: { tone?: string; children: ReactNode }) { 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', } as Record)[tone] ?? 'bg-muted/60 text-muted-foreground border-border'; return ( {children} ); } function StatusCard({ title, icon: Icon, status, tone = 'muted', children, className }: { title: ReactNode; icon?: ComponentType<{ className?: string }>; status?: ReactNode; tone?: string; children: ReactNode; className?: string; }) { 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', } as Record)[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, enabled = true, onToggle }: { update: UpdateData; onCheckNow: () => void; checking: boolean; enabled?: boolean; onToggle: (enabled: boolean) => void; }) { 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}

)}
Automatic version check External request; off = no phone-home
); } // ─── Release Notes Card ─────────────────────────────────────────────────────── function ReleaseNotesCard({ version, historyMeta }: { version: VersionData | null; historyMeta: HistoryMeta | null }) { 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 => { const cat = n.category ?? ''; if (!grouped[cat]) grouped[cat] = [...(grouped[cat] ?? []), n]; }); const categories = Object.keys(grouped); const first = categories[0]; const preview = first ? grouped[first]?.[0]?.text : null; return (

Preview

); } // ─── 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 [updateEnabled, setUpdateEnabled] = useState(true); const load = useCallback(async () => { setLoading(true); setVersion(null); setHistoryMeta(null); try { const [statusData, versionData] = await Promise.all([ api.status() as Promise, api.version() as Promise, ]); setData(statusData); setUpdateData((statusData?.update ?? null) as UpdateData | null); setVersion(versionData); try { const historyData = await api.releaseHistory() as HistoryMeta; setHistoryMeta({ version: historyData.version, updated_at: historyData.updated_at }); } catch { setHistoryMeta(null); } try { const s = await api.getUpdateCheckSetting() as { enabled?: boolean }; setUpdateEnabled(s.enabled !== false); } catch { /* keep default */ } } catch (err) { toast.error(errMessage(err, 'Failed to load status.')); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const handleCheckNow = useCallback(async () => { setUpdateChecking(true); try { setUpdateData(await api.checkForUpdates() as UpdateData); } catch (err) { toast.error(errMessage(err, 'Update check failed')); } finally { setUpdateChecking(false); } }, []); const handleToggleUpdateCheck = useCallback(async (enabled: boolean) => { setUpdateEnabled(enabled); // optimistic try { await api.setUpdateCheckSetting(enabled); if (enabled) handleCheckNow(); } catch (err) { setUpdateEnabled(!enabled); // revert toast.error(errMessage(err, 'Failed to update setting')); } }, [handleCheckNow]); // Normalize the nested response shape const app = (data?.application ?? data?.app ?? {}) as Dict; const rt = (data?.runtime ?? {}) as Dict; const db = (data?.database ?? data?.db ?? {}) as Dict; const stats = (data?.statistics ?? data?.stats ?? {}) as Dict; const worker = (data?.worker ?? data?.jobs ?? {}) as Dict; const notifications = (data?.notifications ?? data?.email ?? {}) as Dict; const backups = (data?.backups ?? data?.backup ?? {}) as Dict; const server = (data?.server ?? data?.clock ?? {}) as Dict; const tracker = (data?.tracker ?? data?.tracker_health ?? {}) as Dict; const cleanup = (data?.cleanup ?? {}) as Dict; const bankSync = (data?.bank_sync ?? data?.simplefin ?? {}) as Dict; const errors = (data?.errors ?? data?.recent_errors ?? []) as ErrItem[]; const dbOk = db.connected ?? (db.status ? 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' : Number(bankSync.source_count ?? 0) > 0 ? 'Connected' : 'No Sources'; const bankSyncTone = !bankSyncEnabled ? 'muted' : bankSync.last_error ? 'warn' : Number(bankSync.source_count ?? 0) > 0 ? 'good' : 'warn'; const off = server.utc_offset; const utcOffset = off != null ? `UTC${(off as number) >= 0 ? '+' : ''}${off}` : null; const lastResult = cleanup.last_result as { import_sessions?: { pruned?: number }; temp_export_files?: { removed?: number }; backup_partials?: { removed?: number }; } | undefined; 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{String(app.version ?? data?.version)} : null} {fmtUptime(Number(app.uptime_seconds ?? data?.uptime_seconds ?? 0))} uptime
)} {/* ── Loading skeleton ─────────────────────────────────────────── */} {loading ? ( <> {[3, 5].map((count, si) => (
0 && 'mt-8')}>
{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.

)}
)}
); }