import { useEffect, useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { api, type AlertCategory, type AlertItem, type AlertSeverity, type AlertsReport, type CurrentUser } from "../api/client"; import { useSortable } from "../hooks/useSortable"; import SortableTh from "../components/SortableTh"; import { usePagination } from "../hooks/usePagination"; import Pagination from "../components/Pagination"; import { downloadCsv } from "../utils/csv"; import { formatDateTime } from "../utils/date"; import { formatAgo } from "../utils/duration"; import { readableError } from "../utils/errors"; const SEVERITY: Record = { critical: { label: "Critical", badge: "bg-red-lt text-red", rank: 0 }, warning: { label: "Warning", badge: "bg-yellow-lt text-yellow", rank: 1 }, info: { label: "Info", badge: "bg-blue-lt text-blue", rank: 2 }, }; const CATEGORY_LABELS: Record = { offline: "Server down", disk: "Disk & storage", backup: "Backups", updates: "Updates", expiry: "Expiry", automation: "Automation", integration: "Integration failing", monitoring: "Monitoring", tickets: "Tickets", }; type Row = AlertItem & { rank: number }; export default function Alerts(_props: { user: CurrentUser }) { const [report, setReport] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [severity, setSeverity] = useState<"all" | AlertSeverity>("all"); const [category, setCategory] = useState<"all" | AlertCategory>("all"); const [search, setSearch] = useState(""); const [showSilenced, setShowSilenced] = useState(true); function load(refresh = false) { setLoading(true); return api.alerts .list(refresh) .then((res) => { setReport(res); setError(null); }) .catch((err) => setError(readableError(err))) .finally(() => setLoading(false)); } useEffect(() => { void load(); }, []); const rows: Row[] = useMemo(() => { if (!report) return []; const q = search.trim().toLowerCase(); return report.alerts .filter((a) => (severity === "all" || a.severity === severity) && (category === "all" || a.category === category) && (showSilenced || !a.silenced)) .filter((a) => !q || [a.message, a.source, CATEGORY_LABELS[a.category]].some((v) => v.toLowerCase().includes(q))) .map((a) => ({ ...a, rank: SEVERITY[a.severity].rank })); }, [report, severity, category, search, showSilenced]); // No initial sort: the server already puts active problems first, worst first. const { sorted, sortKey, sortDir, requestSort } = useSortable(rows); const { pageItems, page, setPage, pageCount, totalCount } = usePagination(sorted); const presentCategories = useMemo(() => [...new Set((report?.alerts ?? []).map((a) => a.category))], [report]); function exportCsv() { downloadCsv( "alerts.csv", ["Severity", "Category", "Source", "Alert", "Silenced"], (sorted ?? []).map((a) => [SEVERITY[a.severity].label, CATEGORY_LABELS[a.category], a.source, a.message, a.silenced ? "yes" : "no"]), ); } const counts = report?.counts; const nothingFound = report !== null && report.alerts.length === 0; const cards: { label: string; value: number | undefined; color: string }[] = [ { label: "Critical", value: counts?.critical, color: "text-red" }, { label: "Warnings", value: counts?.warning, color: "text-yellow" }, { label: "Info", value: counts?.info, color: "text-blue" }, { label: "Silenced (maintenance)", value: counts?.silenced, color: "text-secondary" }, ]; return ( <>

Alerts

What's wrong right now across your servers and integrations — full disks, servers that stopped reporting, failed backups, updates waiting, things about to expire. It runs the same checks that send notifications, whether or not those notifications are switched on. {report && ( <> {" "} Checked {formatAgo(report.generatedAt)} {report.cached ? " (a recent result, reused)" : ""}. )}
{error &&
{error}
} {!report && !error && (
Checking everything — this can take a few seconds…
)} {report && ( <>
{cards.map((c) => (
{c.label}
{c.value ?? "—"}
))}
{report.couldntCheck.length > 0 && (
Couldn't check everything — what's missing below isn't necessarily fine
    {report.couldntCheck.map((c) => (
  • {c.name} — {c.error} {c.affects.length > 0 && (affects: {c.affects.join(", ")})}
  • ))}
)} {report.notes.map((n) => (
{n}
))} {nothingFound ? (
{report.couldntCheck.length === 0 ? ( <>
All clear
Nothing needs attention right now.
) : (
No problems found in what could be checked — see the warning above for what couldn't.
)}
) : (
setSearch(e.target.value)} /> {(counts?.silenced ?? 0) > 0 && ( )}
label="Severity" sortKeyName="rank" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Kind" sortKeyName="category" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Source" sortKeyName="source" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> label="Alert" sortKeyName="message" activeKey={sortKey} direction={sortDir} onSort={requestSort} /> {(pageItems ?? []).map((a) => ( ))} {(sorted ?? []).length === 0 && ( )}
{SEVERITY[a.severity].label} {CATEGORY_LABELS[a.category]} {a.link ? {a.source} : a.source} {a.message} {a.silenced && ( silenced )}
Nothing matches these filters.
)} )} ); }