diff --git a/drb-frontend/app/alerts/page.tsx b/drb-frontend/app/alerts/page.tsx index 2a4e597..1df97b8 100644 --- a/drb-frontend/app/alerts/page.tsx +++ b/drb-frontend/app/alerts/page.tsx @@ -5,6 +5,8 @@ import { useAuth } from "@/components/AuthProvider"; import { useAlerts } from "@/lib/useAlerts"; import { MachineOutputNotice } from "@/components/ui/MachineOutputNotice"; import { c2api } from "@/lib/c2api"; +import { errorMessage } from "@/lib/c2error"; +import { ErrorBanner } from "@/components/ui/EmptyState"; import type { AlertRule } from "@/lib/types"; function fmtTime(iso: string) { @@ -20,6 +22,10 @@ function RulesTab({ isAdmin }: { isAdmin: boolean }) { const [webhook, setWebhook] = useState(""); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); + // Load and toggle/delete failures (server-26#284) — separate from the create + // form's own `error`, and it keeps the list from claiming "no rules" when + // the fetch actually failed. + const [listError, setListError] = useState(null); async function load() { if (loaded) return; @@ -27,8 +33,9 @@ function RulesTab({ isAdmin }: { isAdmin: boolean }) { const data = await c2api.getAlertRules() as AlertRule[]; setRules(data); setLoaded(true); + setListError(null); } catch (e) { - console.error(e); + setListError(`Couldn't load alert rules: ${errorMessage(e)}`); } } @@ -50,8 +57,8 @@ function RulesTab({ isAdmin }: { isAdmin: boolean }) { const created = await c2api.createAlertRule(body) as AlertRule; setRules((prev) => [created, ...prev]); setName(""); setKeywords(""); setTgIds(""); setWebhook(""); - } catch { - setError("Failed to create rule."); + } catch (e) { + setError(`Failed to create rule: ${errorMessage(e)}`); } finally { setSaving(false); } @@ -61,14 +68,17 @@ function RulesTab({ isAdmin }: { isAdmin: boolean }) { try { await c2api.updateAlertRule(rule.rule_id, { enabled: !rule.enabled }); setRules((prev) => prev.map((r) => r.rule_id === rule.rule_id ? { ...r, enabled: !r.enabled } : r)); - } catch (e) { console.error(e); } + setListError(null); + } catch (e) { setListError(`Couldn't update "${rule.name}": ${errorMessage(e)}`); } } async function handleDelete(id: string) { + if (!window.confirm("Delete this alert rule?")) return; try { await c2api.deleteAlertRule(id); setRules((prev) => prev.filter((r) => r.rule_id !== id)); - } catch (e) { console.error(e); } + setListError(null); + } catch (e) { setListError(`Couldn't delete the rule: ${errorMessage(e)}`); } } return ( @@ -120,8 +130,12 @@ function RulesTab({ isAdmin }: { isAdmin: boolean }) { )} + {listError && } +
- {rules.length === 0 ? ( + {!loaded ? ( +

{listError ? "Rules unavailable." : "Loading…"}

+ ) : rules.length === 0 ? (

No alert rules configured.

) : ( @@ -188,11 +202,13 @@ export default function AlertsPage() { const { isAdmin } = useAuth(); const { alerts, loading, error } = useAlerts(); const [tab, setTab] = useState<"events" | "rules">("events"); + const [ackError, setAckError] = useState(null); async function handleAcknowledge(id: string) { + setAckError(null); try { await c2api.acknowledgeAlert(id); - } catch (e) { console.error(e); } + } catch (e) { setAckError(`Couldn't acknowledge: ${errorMessage(e)}`); } } const unacked = alerts.filter((a) => !a.acknowledged); @@ -223,6 +239,8 @@ export default function AlertsPage() { ))} + {tab === "events" && ackError && } + {tab === "events" && ( loading ? (

Loading…

diff --git a/drb-frontend/app/incidents/[id]/page.tsx b/drb-frontend/app/incidents/[id]/page.tsx index 2221983..db627d4 100644 --- a/drb-frontend/app/incidents/[id]/page.tsx +++ b/drb-frontend/app/incidents/[id]/page.tsx @@ -13,6 +13,8 @@ import { SeverityMark } from "@/components/marks/SeverityMark"; import { isKnownSeverity } from "@/lib/severity"; import { Button } from "@/components/ui/Button"; import { MachineOutputNotice } from "@/components/ui/MachineOutputNotice"; +import { ErrorBanner } from "@/components/ui/EmptyState"; +import { errorMessage } from "@/lib/c2error"; import { MAY_BE_MERGED_KM, pathStops, stopNumbers } from "@/lib/liveWall"; const MapView = dynamic(() => import("@/components/MapView"), { ssr: false }); @@ -39,6 +41,7 @@ export default function IncidentDetailPage() { const [summarizing, setSummarizing] = useState(false); const [resolving, setResolving] = useState(false); + const [actionError, setActionError] = useState(null); const [earlierShown, setEarlierShown] = useState(EARLIER_PAGE_SIZE); // Stop numbers and path length come from the same liveWall.pathStops() the @@ -54,16 +57,20 @@ export default function IncidentDetailPage() { ); async function handleResolve() { + // No reopen path exists (server-26#284), so resolving is final from the UI. + if (!window.confirm("Mark this incident resolved? This can't be undone from the app.")) return; setResolving(true); + setActionError(null); try { await c2api.updateIncident(id, { status: "resolved" }); } - catch (e) { console.error(e); } + catch (e) { setActionError(`Couldn't resolve: ${errorMessage(e)}`); } finally { setResolving(false); } } async function handleSummarize() { setSummarizing(true); + setActionError(null); try { await c2api.summarizeIncident(id); } - catch (e) { console.error(e); } + catch (e) { setActionError(`Couldn't regenerate the summary: ${errorMessage(e)}`); } finally { setSummarizing(false); } } @@ -126,6 +133,8 @@ export default function IncidentDetailPage() { )} + {actionError && } + {displayTags.length > 0 && (
{displayTags.map((t) => ( diff --git a/drb-frontend/app/incidents/page.tsx b/drb-frontend/app/incidents/page.tsx index ba49063..1e8388f 100644 --- a/drb-frontend/app/incidents/page.tsx +++ b/drb-frontend/app/incidents/page.tsx @@ -6,6 +6,7 @@ import { useAuth } from "@/components/AuthProvider"; import { useIncidents } from "@/lib/useIncidents"; import { useActiveCalls } from "@/lib/useCalls"; import { c2api } from "@/lib/c2api"; +import { errorMessage } from "@/lib/c2error"; import type { IncidentRecord } from "@/lib/types"; import { PageHeader } from "@/components/ui/PageHeader"; import { Button } from "@/components/ui/Button"; @@ -204,6 +205,7 @@ export default function IncidentsPage() { const { incidents, loading, error, hasMore } = useIncidents(pageLimit, rangeFrom, rangeTo); const activeCalls = useActiveCalls(); const [showCreate, setShowCreate] = useState(false); + const [actionError, setActionError] = useState(null); const [severityFilter, setSeverityFilter] = useState("all"); const [sortMode, setSortMode] = useState("recent"); const [statusFilter, setStatusFilter] = useState("any"); @@ -257,8 +259,11 @@ export default function IncidentsPage() { }, [filtered]); async function handleResolve(id: string) { + // No reopen path exists (server-26#284), so resolving is final from the UI. + if (!window.confirm("Mark this incident resolved? This can't be undone from the app.")) return; + setActionError(null); try { await c2api.updateIncident(id, { status: "resolved" }); } - catch (e) { console.error(e); } + catch (e) { setActionError(`Couldn't resolve: ${errorMessage(e)}`); } } return ( @@ -273,6 +278,8 @@ export default function IncidentsPage() { chips are pipeline output, so the notice rides with the list. */} + {actionError && } +
{SEVERITY_FILTERS.map(({ key, label }) => ( diff --git a/drb-frontend/app/nodes/[id]/page.tsx b/drb-frontend/app/nodes/[id]/page.tsx index e7e6c26..0174722 100644 --- a/drb-frontend/app/nodes/[id]/page.tsx +++ b/drb-frontend/app/nodes/[id]/page.tsx @@ -15,6 +15,7 @@ import { CallRow } from "@/components/CallRow"; import { MachineOutputNotice } from "@/components/ui/MachineOutputNotice"; import { useAuth } from "@/components/AuthProvider"; import { c2api } from "@/lib/c2api"; +import { errorMessage } from "@/lib/c2error"; import type { NodeRecord } from "@/lib/types"; function ApprovalBadge({ status }: { status: string | null }) { @@ -54,10 +55,14 @@ export default function NodeDetailPage() { return unsub; }, [id]); - async function sendCommand(action: string) { + async function sendCommand(action: string, label: string) { setSending(true); try { await c2api.sendCommand(id, { action }); + alert(`${label} sent to the node.`); + } catch (e) { + // Was try/finally only: an unhandled rejection and no feedback (server-26#284). + alert(`Couldn't send ${label.toLowerCase()}: ${errorMessage(e)}`); } finally { setSending(false); } @@ -249,7 +254,7 @@ export default function NodeDetailPage() {