diff --git a/client/pages/StatusPage.jsx b/client/pages/StatusPage.tsx similarity index 80% rename from client/pages/StatusPage.jsx rename to client/pages/StatusPage.tsx index 178239c..375cae9 100644 --- a/client/pages/StatusPage.jsx +++ b/client/pages/StatusPage.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, type ComponentType, type ReactNode } from 'react'; import { Link } from 'react-router-dom'; import { Activity, AlertCircle, ArrowUpCircle, BarChart3, Bell, CalendarClock, @@ -7,35 +7,68 @@ import { } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; -import { cn, fmtUptime, fmtBytes } from '@/lib/utils'; +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) { - if (runtime.memory_used_bytes ?? runtime.memory?.used ?? runtime.memory) { - return fmtBytes(runtime.memory_used_bytes ?? runtime.memory?.used ?? runtime.memory); - } +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) { - return value === undefined || value === null ? null : fmtBytes(value); +function formatBytesMaybe(value: unknown): string | null { + return value === undefined || value === null ? null : fmtBytes(value as number); } -function formatDateTime(value) { +function formatDateTime(value: unknown): string | null { if (!value) return null; - const date = new Date(value); - if (Number.isNaN(date.getTime())) return value; + const date = new Date(value as string | number); + if (Number.isNaN(date.getTime())) return String(value); return date.toLocaleString(); } // ─── Design primitives ──────────────────────────────────────────────────────── -function SectionLabel({ children }) { +function SectionLabel({ children }: { children: ReactNode }) { return (
@@ -46,24 +79,24 @@ function SectionLabel({ children }) { ); } -function StatRow({ label, value, last }) { +function StatRow({ label, value, last }: { label: ReactNode; value?: unknown; last?: boolean }) { return (
{label} - {value ?? } + {value == null ? : (value as ReactNode)}
); } -function StatusPill({ tone = 'muted', children }) { - const cls = { +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', - }[tone] ?? 'bg-muted/60 text-muted-foreground border-border'; + } as Record)[tone] ?? 'bg-muted/60 text-muted-foreground border-border'; return ( ; + 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', - }[tone] ?? 'border-t-border/60'; + } as Record)[tone] ?? 'border-t-border/60'; return (
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; @@ -194,20 +240,22 @@ function UpdateCard({ update, onCheckNow, checking, enabled = true, onToggle }) // ─── Release Notes Card ─────────────────────────────────────────────────────── -function ReleaseNotesCard({ version, historyMeta }) { +function ReleaseNotesCard({ version, historyMeta }: { version: VersionData | null; historyMeta: HistoryMeta | null }) { if (!version) return ; - const grouped = CATEGORY_ORDER.reduce((acc, cat) => { + 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 cat = n.category ?? ''; + if (!grouped[cat]) grouped[cat] = [...(grouped[cat] ?? []), n]; }); const categories = Object.keys(grouped); - const preview = categories.length ? grouped[categories[0]]?.[0]?.text : null; + const first = categories[0]; + const preview = first ? grouped[first]?.[0]?.text : null; return ( @@ -231,11 +279,11 @@ function ReleaseNotesCard({ version, historyMeta }) { // ─── StatusPage ─────────────────────────────────────────────────────────────── export default function StatusPage() { - const [data, setData] = useState(null); - const [version, setVersion] = useState(null); - const [historyMeta, setHistoryMeta] = useState(null); + 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 [updateData, setUpdateData] = useState(null); const [updateChecking, setUpdateChecking] = useState(false); const [updateEnabled, setUpdateEnabled] = useState(true); @@ -244,20 +292,23 @@ export default function StatusPage() { setVersion(null); setHistoryMeta(null); try { - const [statusData, versionData] = await Promise.all([api.status(), api.version()]); + const [statusData, versionData] = await Promise.all([ + api.status() as Promise, + api.version() as Promise, + ]); setData(statusData); - setUpdateData(statusData?.update ?? null); + setUpdateData((statusData?.update ?? null) as UpdateData | null); setVersion(versionData); try { - const historyData = await api.releaseHistory(); + 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(); + const s = await api.getUpdateCheckSetting() as { enabled?: boolean }; setUpdateEnabled(s.enabled !== false); } catch { /* keep default */ } } catch (err) { - toast.error(err.message || 'Failed to load status.'); + toast.error(errMessage(err, 'Failed to load status.')); } finally { setLoading(false); } @@ -268,38 +319,38 @@ export default function StatusPage() { const handleCheckNow = useCallback(async () => { setUpdateChecking(true); try { - setUpdateData(await api.checkForUpdates()); + setUpdateData(await api.checkForUpdates() as UpdateData); } catch (err) { - toast.error(err.message || 'Update check failed'); + toast.error(errMessage(err, 'Update check failed')); } finally { setUpdateChecking(false); } }, []); - const handleToggleUpdateCheck = useCallback(async (enabled) => { + const handleToggleUpdateCheck = useCallback(async (enabled: boolean) => { setUpdateEnabled(enabled); // optimistic try { await api.setUpdateCheckSetting(enabled); if (enabled) handleCheckNow(); } catch (err) { setUpdateEnabled(!enabled); // revert - toast.error(err.message || 'Failed to update setting'); + toast.error(errMessage(err, 'Failed to update setting')); } }, [handleCheckNow]); // 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 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; @@ -320,17 +371,24 @@ export default function StatusPage() { const bankSyncStatus = !bankSyncEnabled ? 'Disabled' : bankSync.running ? 'Syncing' : bankSync.last_error ? 'Error' - : (bankSync.source_count ?? 0) > 0 ? 'Connected' + : Number(bankSync.source_count ?? 0) > 0 ? 'Connected' : 'No Sources'; const bankSyncTone = !bankSyncEnabled ? 'muted' : bankSync.last_error ? 'warn' - : (bankSync.source_count ?? 0) > 0 ? 'good' + : Number(bankSync.source_count ?? 0) > 0 ? 'good' : 'warn'; - const utcOffset = server.utc_offset != null - ? `UTC${server.utc_offset >= 0 ? '+' : ''}${server.utc_offset}` + 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 (
@@ -367,8 +425,8 @@ export default function StatusPage() { {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 + {(app.version ?? data?.version) ? v{String(app.version ?? data?.version)} : null} + {fmtUptime(Number(app.uptime_seconds ?? data?.uptime_seconds ?? 0))} uptime
)} @@ -401,7 +459,7 @@ export default function StatusPage() { > - +
- - - + + +