#286: request() now throws C2ApiError — message is FastAPI's `detail` or a plain sentence for the status (401/403/404/429/5xx), with toString() so the many String(e) call sites read cleanly. status/body stay on the error; the Discord parsers rebuild the legacy "C2 API error N: body" text via legacyErrorText(), so their invite/code parsing is unchanged. #284: Resolve, Regenerate summary, alert-rule toggle/delete/load, Acknowledge, trip deletes, system delete and Restart OP25 now surface failures (ErrorBanner where the page has one, alert() where the page already uses it). Rules tab no longer says "No alert rules configured" when the load failed. Confirms added for Resolve (no reopen path exists), rule delete and trip delete. Reopen deliberately not built — manual resolve doesn't set resolved_at, so reopening is a lifecycle change. Frontend unverified: no Node on the authoring box (#207). Closes #286 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6d667e81c9
commit
f16f0a9f93
@@ -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<string | null>(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() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{actionError && <ErrorBanner message={actionError} />}
|
||||
|
||||
{displayTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{displayTags.map((t) => (
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [severityFilter, setSeverityFilter] = useState<SeverityFilter>("all");
|
||||
const [sortMode, setSortMode] = useState<SortMode>("recent");
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>("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. */}
|
||||
<MachineOutputNotice variant="inline" />
|
||||
|
||||
{actionError && <ErrorBanner message={actionError} />}
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex flex-wrap gap-1 bg-surface border border-line rounded-lg p-1 w-fit">
|
||||
{SEVERITY_FILTERS.map(({ key, label }) => (
|
||||
|
||||
Reference in New Issue
Block a user