refactor(ts): StatusPage → TypeScript (server-health dashboard, loose response shape)
This commit is contained in:
parent
18d0c0bdb9
commit
7a33bd1d56
|
|
@ -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 { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Activity, AlertCircle, ArrowUpCircle, BarChart3, Bell, CalendarClock,
|
Activity, AlertCircle, ArrowUpCircle, BarChart3, Bell, CalendarClock,
|
||||||
|
|
@ -7,35 +7,68 @@ import {
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '@/api';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { Switch } from '@/components/ui/switch';
|
import { Switch } from '@/components/ui/switch';
|
||||||
import { MarkdownText } from '@/components/MarkdownText';
|
import { MarkdownText } from '@/components/MarkdownText';
|
||||||
|
|
||||||
|
type Dict = Record<string, unknown>;
|
||||||
|
|
||||||
|
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 ──────────────────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function formatMemory(runtime) {
|
function formatMemory(runtime: Dict): string | null {
|
||||||
if (runtime.memory_used_bytes ?? runtime.memory?.used ?? runtime.memory) {
|
const memUsed = runtime.memory_used_bytes ?? (runtime.memory as { used?: unknown } | undefined)?.used ?? runtime.memory;
|
||||||
return fmtBytes(runtime.memory_used_bytes ?? runtime.memory?.used ?? runtime.memory);
|
if (memUsed) return fmtBytes(memUsed as number);
|
||||||
}
|
|
||||||
if (runtime.memory_mb) return `${runtime.memory_mb} MB`;
|
if (runtime.memory_mb) return `${runtime.memory_mb} MB`;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatBytesMaybe(value) {
|
function formatBytesMaybe(value: unknown): string | null {
|
||||||
return value === undefined || value === null ? null : fmtBytes(value);
|
return value === undefined || value === null ? null : fmtBytes(value as number);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDateTime(value) {
|
function formatDateTime(value: unknown): string | null {
|
||||||
if (!value) return null;
|
if (!value) return null;
|
||||||
const date = new Date(value);
|
const date = new Date(value as string | number);
|
||||||
if (Number.isNaN(date.getTime())) return value;
|
if (Number.isNaN(date.getTime())) return String(value);
|
||||||
return date.toLocaleString();
|
return date.toLocaleString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Design primitives ────────────────────────────────────────────────────────
|
// ─── Design primitives ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function SectionLabel({ children }) {
|
function SectionLabel({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-3 mb-3 mt-8 first:mt-0">
|
<div className="flex items-center gap-3 mb-3 mt-8 first:mt-0">
|
||||||
<span className="text-[11px] font-semibold uppercase tracking-widest text-muted-foreground/50 whitespace-nowrap">
|
<span className="text-[11px] font-semibold uppercase tracking-widest text-muted-foreground/50 whitespace-nowrap">
|
||||||
|
|
@ -46,24 +79,24 @@ function SectionLabel({ children }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatRow({ label, value, last }) {
|
function StatRow({ label, value, last }: { label: ReactNode; value?: unknown; last?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex items-center justify-between py-1.5 gap-3', !last && 'border-b border-border/40')}>
|
<div className={cn('flex items-center justify-between py-1.5 gap-3', !last && 'border-b border-border/40')}>
|
||||||
<span className="text-xs text-muted-foreground shrink-0">{label}</span>
|
<span className="text-xs text-muted-foreground shrink-0">{label}</span>
|
||||||
<span className="text-xs font-medium tabular-nums text-right truncate max-w-[60%]">
|
<span className="text-xs font-medium tabular-nums text-right truncate max-w-[60%]">
|
||||||
{value ?? <span className="text-muted-foreground/50">—</span>}
|
{value == null ? <span className="text-muted-foreground/50">—</span> : (value as ReactNode)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusPill({ tone = 'muted', children }) {
|
function StatusPill({ tone = 'muted', children }: { tone?: string; children: ReactNode }) {
|
||||||
const cls = {
|
const cls = ({
|
||||||
good: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/25',
|
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',
|
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',
|
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',
|
muted: 'bg-muted/60 text-muted-foreground border-border',
|
||||||
}[tone] ?? 'bg-muted/60 text-muted-foreground border-border';
|
} as Record<string, string>)[tone] ?? 'bg-muted/60 text-muted-foreground border-border';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
|
|
@ -75,13 +108,20 @@ function StatusPill({ tone = 'muted', children }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatusCard({ title, icon: Icon, status, tone = 'muted', children, className }) {
|
function StatusCard({ title, icon: Icon, status, tone = 'muted', children, className }: {
|
||||||
const accentCls = {
|
title: ReactNode;
|
||||||
|
icon?: ComponentType<{ className?: string }>;
|
||||||
|
status?: ReactNode;
|
||||||
|
tone?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const accentCls = ({
|
||||||
good: 'border-t-emerald-500/60',
|
good: 'border-t-emerald-500/60',
|
||||||
warn: 'border-t-amber-400/70',
|
warn: 'border-t-amber-400/70',
|
||||||
bad: 'border-t-red-500/60',
|
bad: 'border-t-red-500/60',
|
||||||
muted: 'border-t-border/60',
|
muted: 'border-t-border/60',
|
||||||
}[tone] ?? 'border-t-border/60';
|
} as Record<string, string>)[tone] ?? 'border-t-border/60';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
|
|
@ -129,7 +169,13 @@ function SkeletonCard() {
|
||||||
|
|
||||||
const CATEGORY_ORDER = ['Added', 'Changed', 'Fixed', 'Removed', 'Deprecated', 'Security'];
|
const CATEGORY_ORDER = ['Added', 'Changed', 'Fixed', 'Removed', 'Deprecated', 'Security'];
|
||||||
|
|
||||||
function UpdateCard({ update, onCheckNow, checking, enabled = true, onToggle }) {
|
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 hasUpdate = !!update.has_update;
|
||||||
const isKnown = update.up_to_date !== null && update.up_to_date !== undefined;
|
const isKnown = update.up_to_date !== null && update.up_to_date !== undefined;
|
||||||
const hasError = !!update.error;
|
const hasError = !!update.error;
|
||||||
|
|
@ -194,20 +240,22 @@ function UpdateCard({ update, onCheckNow, checking, enabled = true, onToggle })
|
||||||
|
|
||||||
// ─── Release Notes Card ───────────────────────────────────────────────────────
|
// ─── Release Notes Card ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
function ReleaseNotesCard({ version, historyMeta }) {
|
function ReleaseNotesCard({ version, historyMeta }: { version: VersionData | null; historyMeta: HistoryMeta | null }) {
|
||||||
if (!version) return <SkeletonCard />;
|
if (!version) return <SkeletonCard />;
|
||||||
|
|
||||||
const grouped = CATEGORY_ORDER.reduce((acc, cat) => {
|
const grouped = CATEGORY_ORDER.reduce<Record<string, Note[]>>((acc, cat) => {
|
||||||
const notes = version.notes?.filter(n => n.category === cat) ?? [];
|
const notes = version.notes?.filter(n => n.category === cat) ?? [];
|
||||||
if (notes.length) acc[cat] = notes;
|
if (notes.length) acc[cat] = notes;
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
version.notes?.forEach(n => {
|
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 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 (
|
return (
|
||||||
<StatusCard title="Release Notes" icon={ScrollText} status={`v${version.version}`} tone="good">
|
<StatusCard title="Release Notes" icon={ScrollText} status={`v${version.version}`} tone="good">
|
||||||
|
|
@ -231,11 +279,11 @@ function ReleaseNotesCard({ version, historyMeta }) {
|
||||||
// ─── StatusPage ───────────────────────────────────────────────────────────────
|
// ─── StatusPage ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export default function StatusPage() {
|
export default function StatusPage() {
|
||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState<Dict | null>(null);
|
||||||
const [version, setVersion] = useState(null);
|
const [version, setVersion] = useState<VersionData | null>(null);
|
||||||
const [historyMeta, setHistoryMeta] = useState(null);
|
const [historyMeta, setHistoryMeta] = useState<HistoryMeta | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [updateData, setUpdateData] = useState(null);
|
const [updateData, setUpdateData] = useState<UpdateData | null>(null);
|
||||||
const [updateChecking, setUpdateChecking] = useState(false);
|
const [updateChecking, setUpdateChecking] = useState(false);
|
||||||
const [updateEnabled, setUpdateEnabled] = useState(true);
|
const [updateEnabled, setUpdateEnabled] = useState(true);
|
||||||
|
|
||||||
|
|
@ -244,20 +292,23 @@ export default function StatusPage() {
|
||||||
setVersion(null);
|
setVersion(null);
|
||||||
setHistoryMeta(null);
|
setHistoryMeta(null);
|
||||||
try {
|
try {
|
||||||
const [statusData, versionData] = await Promise.all([api.status(), api.version()]);
|
const [statusData, versionData] = await Promise.all([
|
||||||
|
api.status() as Promise<Dict>,
|
||||||
|
api.version() as Promise<VersionData>,
|
||||||
|
]);
|
||||||
setData(statusData);
|
setData(statusData);
|
||||||
setUpdateData(statusData?.update ?? null);
|
setUpdateData((statusData?.update ?? null) as UpdateData | null);
|
||||||
setVersion(versionData);
|
setVersion(versionData);
|
||||||
try {
|
try {
|
||||||
const historyData = await api.releaseHistory();
|
const historyData = await api.releaseHistory() as HistoryMeta;
|
||||||
setHistoryMeta({ version: historyData.version, updated_at: historyData.updated_at });
|
setHistoryMeta({ version: historyData.version, updated_at: historyData.updated_at });
|
||||||
} catch { setHistoryMeta(null); }
|
} catch { setHistoryMeta(null); }
|
||||||
try {
|
try {
|
||||||
const s = await api.getUpdateCheckSetting();
|
const s = await api.getUpdateCheckSetting() as { enabled?: boolean };
|
||||||
setUpdateEnabled(s.enabled !== false);
|
setUpdateEnabled(s.enabled !== false);
|
||||||
} catch { /* keep default */ }
|
} catch { /* keep default */ }
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || 'Failed to load status.');
|
toast.error(errMessage(err, 'Failed to load status.'));
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
@ -268,38 +319,38 @@ export default function StatusPage() {
|
||||||
const handleCheckNow = useCallback(async () => {
|
const handleCheckNow = useCallback(async () => {
|
||||||
setUpdateChecking(true);
|
setUpdateChecking(true);
|
||||||
try {
|
try {
|
||||||
setUpdateData(await api.checkForUpdates());
|
setUpdateData(await api.checkForUpdates() as UpdateData);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error(err.message || 'Update check failed');
|
toast.error(errMessage(err, 'Update check failed'));
|
||||||
} finally {
|
} finally {
|
||||||
setUpdateChecking(false);
|
setUpdateChecking(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleToggleUpdateCheck = useCallback(async (enabled) => {
|
const handleToggleUpdateCheck = useCallback(async (enabled: boolean) => {
|
||||||
setUpdateEnabled(enabled); // optimistic
|
setUpdateEnabled(enabled); // optimistic
|
||||||
try {
|
try {
|
||||||
await api.setUpdateCheckSetting(enabled);
|
await api.setUpdateCheckSetting(enabled);
|
||||||
if (enabled) handleCheckNow();
|
if (enabled) handleCheckNow();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setUpdateEnabled(!enabled); // revert
|
setUpdateEnabled(!enabled); // revert
|
||||||
toast.error(err.message || 'Failed to update setting');
|
toast.error(errMessage(err, 'Failed to update setting'));
|
||||||
}
|
}
|
||||||
}, [handleCheckNow]);
|
}, [handleCheckNow]);
|
||||||
|
|
||||||
// Normalize the nested response shape
|
// Normalize the nested response shape
|
||||||
const app = data?.application ?? data?.app ?? {};
|
const app = (data?.application ?? data?.app ?? {}) as Dict;
|
||||||
const rt = data?.runtime ?? {};
|
const rt = (data?.runtime ?? {}) as Dict;
|
||||||
const db = data?.database ?? data?.db ?? {};
|
const db = (data?.database ?? data?.db ?? {}) as Dict;
|
||||||
const stats = data?.statistics ?? data?.stats ?? {};
|
const stats = (data?.statistics ?? data?.stats ?? {}) as Dict;
|
||||||
const worker = data?.worker ?? data?.jobs ?? {};
|
const worker = (data?.worker ?? data?.jobs ?? {}) as Dict;
|
||||||
const notifications = data?.notifications ?? data?.email ?? {};
|
const notifications = (data?.notifications ?? data?.email ?? {}) as Dict;
|
||||||
const backups = data?.backups ?? data?.backup ?? {};
|
const backups = (data?.backups ?? data?.backup ?? {}) as Dict;
|
||||||
const server = data?.server ?? data?.clock ?? {};
|
const server = (data?.server ?? data?.clock ?? {}) as Dict;
|
||||||
const tracker = data?.tracker ?? data?.tracker_health ?? {};
|
const tracker = (data?.tracker ?? data?.tracker_health ?? {}) as Dict;
|
||||||
const cleanup = data?.cleanup ?? {};
|
const cleanup = (data?.cleanup ?? {}) as Dict;
|
||||||
const bankSync = data?.bank_sync ?? data?.simplefin ?? {};
|
const bankSync = (data?.bank_sync ?? data?.simplefin ?? {}) as Dict;
|
||||||
const errors = data?.errors ?? data?.recent_errors ?? [];
|
const errors = (data?.errors ?? data?.recent_errors ?? []) as ErrItem[];
|
||||||
|
|
||||||
const dbOk = db.connected ?? (db.status ? db.status === 'connected' : true);
|
const dbOk = db.connected ?? (db.status ? db.status === 'connected' : true);
|
||||||
const workerOk = !worker.enabled ? null : worker.last_error ? false : true;
|
const workerOk = !worker.enabled ? null : worker.last_error ? false : true;
|
||||||
|
|
@ -320,17 +371,24 @@ export default function StatusPage() {
|
||||||
const bankSyncStatus = !bankSyncEnabled ? 'Disabled'
|
const bankSyncStatus = !bankSyncEnabled ? 'Disabled'
|
||||||
: bankSync.running ? 'Syncing'
|
: bankSync.running ? 'Syncing'
|
||||||
: bankSync.last_error ? 'Error'
|
: bankSync.last_error ? 'Error'
|
||||||
: (bankSync.source_count ?? 0) > 0 ? 'Connected'
|
: Number(bankSync.source_count ?? 0) > 0 ? 'Connected'
|
||||||
: 'No Sources';
|
: 'No Sources';
|
||||||
const bankSyncTone = !bankSyncEnabled ? 'muted'
|
const bankSyncTone = !bankSyncEnabled ? 'muted'
|
||||||
: bankSync.last_error ? 'warn'
|
: bankSync.last_error ? 'warn'
|
||||||
: (bankSync.source_count ?? 0) > 0 ? 'good'
|
: Number(bankSync.source_count ?? 0) > 0 ? 'good'
|
||||||
: 'warn';
|
: 'warn';
|
||||||
|
|
||||||
const utcOffset = server.utc_offset != null
|
const off = server.utc_offset;
|
||||||
? `UTC${server.utc_offset >= 0 ? '+' : ''}${server.utc_offset}`
|
const utcOffset = off != null
|
||||||
|
? `UTC${(off as number) >= 0 ? '+' : ''}${off}`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const lastResult = cleanup.last_result as {
|
||||||
|
import_sessions?: { pruned?: number };
|
||||||
|
temp_export_files?: { removed?: number };
|
||||||
|
backup_partials?: { removed?: number };
|
||||||
|
} | undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
||||||
|
|
@ -367,8 +425,8 @@ export default function StatusPage() {
|
||||||
{data.ok ? 'All systems operational' : 'One or more systems need attention'}
|
{data.ok ? 'All systems operational' : 'One or more systems need attention'}
|
||||||
</span>
|
</span>
|
||||||
<div className="ml-auto hidden sm:flex items-center gap-4 text-xs text-muted-foreground tabular-nums">
|
<div className="ml-auto hidden sm:flex items-center gap-4 text-xs text-muted-foreground tabular-nums">
|
||||||
{(app.version ?? data?.version) && <span>v{app.version ?? data?.version}</span>}
|
{(app.version ?? data?.version) ? <span>v{String(app.version ?? data?.version)}</span> : null}
|
||||||
<span>{fmtUptime(app.uptime_seconds ?? data?.uptime_seconds ?? 0)} uptime</span>
|
<span>{fmtUptime(Number(app.uptime_seconds ?? data?.uptime_seconds ?? 0))} uptime</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -401,7 +459,7 @@ export default function StatusPage() {
|
||||||
>
|
>
|
||||||
<StatRow label="Version" value={(app.version ?? data?.version) ? `v${app.version ?? data?.version}` : null} />
|
<StatRow label="Version" value={(app.version ?? data?.version) ? `v${app.version ?? data?.version}` : null} />
|
||||||
<StatRow label="Environment" value={app.environment ?? app.env ?? data?.environment ?? data?.env} />
|
<StatRow label="Environment" value={app.environment ?? app.env ?? data?.environment ?? data?.env} />
|
||||||
<StatRow label="Uptime" value={fmtUptime(app.uptime_seconds ?? app.uptime ?? data?.uptime_seconds ?? 0)} last />
|
<StatRow label="Uptime" value={fmtUptime(Number(app.uptime_seconds ?? app.uptime ?? data?.uptime_seconds ?? 0))} last />
|
||||||
</StatusCard>
|
</StatusCard>
|
||||||
|
|
||||||
<StatusCard title="Database" icon={Database}
|
<StatusCard title="Database" icon={Database}
|
||||||
|
|
@ -484,9 +542,9 @@ export default function StatusPage() {
|
||||||
tone={cleanup.last_run_at ? 'good' : 'muted'}
|
tone={cleanup.last_run_at ? 'good' : 'muted'}
|
||||||
>
|
>
|
||||||
<StatRow label="Last Run" value={formatDateTime(cleanup.last_run_at)} />
|
<StatRow label="Last Run" value={formatDateTime(cleanup.last_run_at)} />
|
||||||
<StatRow label="Import Sessions" value={cleanup.last_result?.import_sessions?.pruned != null ? `${cleanup.last_result.import_sessions.pruned} pruned` : null} />
|
<StatRow label="Import Sessions" value={lastResult?.import_sessions?.pruned != null ? `${lastResult.import_sessions.pruned} pruned` : null} />
|
||||||
<StatRow label="Temp Files" value={cleanup.last_result?.temp_export_files?.removed != null ? `${cleanup.last_result.temp_export_files.removed} removed` : null} />
|
<StatRow label="Temp Files" value={lastResult?.temp_export_files?.removed != null ? `${lastResult.temp_export_files.removed} removed` : null} />
|
||||||
<StatRow label="Backup Partials" value={cleanup.last_result?.backup_partials?.removed != null ? `${cleanup.last_result.backup_partials.removed} removed` : null} last />
|
<StatRow label="Backup Partials" value={lastResult?.backup_partials?.removed != null ? `${lastResult.backup_partials.removed} removed` : null} last />
|
||||||
</StatusCard>
|
</StatusCard>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
Loading…
Reference in New Issue