From 00d7593ffdbf1f14ca594d00178c68ad7a9f2017 Mon Sep 17 00:00:00 2001 From: robbond Date: Wed, 9 Sep 2026 19:45:55 +0100 Subject: [PATCH] fix(confidence-engine): preserve scenario during outage --- components/scenario-form.jsx | 35 ++++++++++++++++++++++++++++++--- tests/ui/scenario-form.test.jsx | 22 +++++++++++++++++++++ 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/components/scenario-form.jsx b/components/scenario-form.jsx index d8bf14c..f6d535d 100644 --- a/components/scenario-form.jsx +++ b/components/scenario-form.jsx @@ -33,6 +33,28 @@ export async function submitScenarioForStartCase(fetchImpl, scenario) { }); } +export function isUnavailableStartResponse(response, data) { + return response.status === 503 && + data?.success === false && + data?.error === "Reasoning service is temporarily unavailable."; +} + +export function UnavailableStartPanel({ onRetry }) { + return ( +
+

Confidence Engine is temporarily unavailable.

+

We couldn't process this right now. Your scenario is still here and you can try again.

+ +
+ ); +} + export async function submitAnswerForUpdateCase( fetchImpl, { situationGraph, previousQuestion, answer, findings }, @@ -672,8 +694,7 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) { updateStatus === "loading" ); - const handleSubmit = async (e) => { - e.preventDefault(); + const handleStart = async () => { setStatus("loading"); setResult(null); setAnswer(""); @@ -699,6 +720,8 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) { /* ── v0.59a — provenance: first meaningful change sets revision to 1 ── */ setInvestigationRevision(1); persist({ id: investigationId, scenario, situationGraph: normalised.situationGraph, selectedQuestion: normalised.selectedQuestion, summary: data.summary ?? null, updatedAt: new Date().toISOString(), focusedContributions, findings: [], investigationReport, investigationRevision: 1 }); + } else if (isUnavailableStartResponse(res, data)) { + setStatus("unavailable"); } else { setStatus("error"); setCurrentUnderstanding(data.summary ?? null); @@ -710,6 +733,11 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) { } }; + const handleSubmit = (e) => { + e.preventDefault(); + void handleStart(); + }; + const handleUpdate = async (e) => { e.preventDefault(); @@ -799,7 +827,7 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) { return (
{/* ── Idle form for scenario input ─ */} - {!result?.situationGraph && status === "idle" && ( + {!result?.situationGraph && (status === "idle" || status === "unavailable") && (
{/* Two-column landing workspace */} @@ -864,6 +892,7 @@ export default function ScenarioForm({ investigationId, onNavigateToReport }) {

You do not need all the answers yet.

+ {status === "unavailable" && }
diff --git a/tests/ui/scenario-form.test.jsx b/tests/ui/scenario-form.test.jsx index 96df7bd..de8c698 100644 --- a/tests/ui/scenario-form.test.jsx +++ b/tests/ui/scenario-form.test.jsx @@ -14,7 +14,9 @@ import ReasoningWorkspace, { } from "@/components/reasoning-workspace.jsx"; import { ScenarioResultPanels, + UnavailableStartPanel, UpdateErrorPanel, + isUnavailableStartResponse, submitAnswerForUpdateCase, submitScenarioForStartCase, } from "@/components/scenario-form.jsx"; @@ -372,6 +374,26 @@ function makeCommercialUpdateSuccess(overrides = {}) { } describe("scenario-form UI helpers", () => { + it("recognises only the established sanitized unavailable start response", () => { + expect(isUnavailableStartResponse( + { status: 503 }, + { success: false, error: "Reasoning service is temporarily unavailable." }, + )).toBe(true); + expect(isUnavailableStartResponse( + { status: 500 }, + { success: false, error: "Reasoning service is temporarily unavailable." }, + )).toBe(false); + }); + + it("presents generic recovery without provider details", () => { + const html = renderToStaticMarkup(); + + expect(html).toContain("Confidence Engine is temporarily unavailable."); + expect(html).toContain("Your scenario is still here and you can try again."); + expect(html).toContain(">Retry<"); + expect(html).not.toMatch(/ollama|provider|local model|server|infrastructure/i); + }); + it("submits to /api/cases/start", async () => { const fetchImpl = vi.fn().mockResolvedValue({ ok: true });