From d6c7154fa9ec193e96e557697cced1b52fc81bf1 Mon Sep 17 00:00:00 2001 From: Sameer6305 Date: Fri, 24 Jul 2026 00:16:46 +0530 Subject: [PATCH] Fix #767: Harden workspaces against silent error swallowing and 207 statuses Ensures network errors, API failures, and 207 Partial Success statuses are surfaced correctly to the user instead of failing silently in the frontend UI. --- .../DecisionWorkspace/DecisionWorkspace.tsx | 29 +++++++- .../EnrichWorkspace/EntityResolutionTab.tsx | 6 +- .../LineageWorkspace/LineageDiagram.tsx | 18 +++-- .../ManageWorkspace/KGOverviewTab.tsx | 59 +++++++++------ .../OntologyWorkspace/OntologyManager.tsx | 10 +-- .../OntologyWorkspace/OntologySearch.tsx | 6 +- .../SKOSVocabularyManager.tsx | 30 ++++++-- .../OntologyWorkspace/ShaclStudio.tsx | 5 +- .../OntologyWorkspace/VersionsTab.tsx | 73 ++++++++++++++----- .../src/workspaces/OntologyWorkspace/api.ts | 6 +- .../src/workspaces/ReasoningWorkspace.tsx | 1 + .../SparqlWorkspace/SparqlWorkspace.tsx | 11 +++ 12 files changed, 187 insertions(+), 67 deletions(-) diff --git a/explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx b/explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx index 601148a1..f22fb547 100644 --- a/explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx +++ b/explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx @@ -92,6 +92,7 @@ export function DecisionWorkspace() { const [chainLoading, setChainLoading] = useState(false); const [listLoading, setListLoading] = useState(true); const [filter, setFilter] = useState(""); + const [error, setError] = useState(""); // Tracks the active chain request so stale responses from rapid selections are ignored. const chainCtrlRef = useRef(null); @@ -100,13 +101,22 @@ export function DecisionWorkspace() { const ctrl = new AbortController(); setListLoading(true); fetch("/api/decisions", { signal: ctrl.signal }) - .then((r) => r.ok ? r.json() : Promise.reject(r.status)) + .then(async (r) => { + if (!r.ok) throw new Error(`HTTP ${r.status}`); + const data = await r.json(); + if (r.status === 207) setError(data.message || "Warning: Partial success loading decisions."); + return data; + }) .then((data) => { if (ctrl.signal.aborted) return; setDecisions(data); if (data.length > 0) void loadChain(data[0]); }) - .catch((e) => { if (e?.name !== "AbortError") console.error(e); }) + .catch((e) => { + if (e?.name !== "AbortError") { + setError(e instanceof Error ? e.message : "Failed to load decisions."); + } + }) .finally(() => { if (!ctrl.signal.aborted) setListLoading(false); }); @@ -127,11 +137,16 @@ export function DecisionWorkspace() { setChain([]); try { const res = await fetch(`/api/decisions/${encodeURIComponent(d.decision_id)}/chain`, { signal: ctrl.signal }); - if (!res.ok) throw new Error(`${res.status}`); + if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); + if (res.status === 207 && !ctrl.signal.aborted) { + setError(data.message || "Warning: Partial success loading chain."); + } if (!ctrl.signal.aborted) setChain(data.chain || []); } catch (e) { - if (e instanceof Error && e.name !== "AbortError") console.error(e); + if (e instanceof Error && e.name !== "AbortError") { + setError(e.message); + } } finally { if (!ctrl.signal.aborted) setChainLoading(false); } @@ -213,6 +228,12 @@ export function DecisionWorkspace() {
+ {error ? ( +
+ {error} +
+ ) : null} + {selected ? (
{/* Decision header */} diff --git a/explorer/src/workspaces/EnrichWorkspace/EntityResolutionTab.tsx b/explorer/src/workspaces/EnrichWorkspace/EntityResolutionTab.tsx index 5d1c9151..dfaf846b 100644 --- a/explorer/src/workspaces/EnrichWorkspace/EntityResolutionTab.tsx +++ b/explorer/src/workspaces/EnrichWorkspace/EntityResolutionTab.tsx @@ -192,6 +192,7 @@ export function EntityResolutionTab() { }, [threshold]); const handleMerge = useCallback(async (primaryId: string, duplicateId: string) => { + setScanError(""); try { const res = await fetch("/api/enrich/merge", { method: "POST", @@ -200,6 +201,9 @@ export function EntityResolutionTab() { }); if (!res.ok) throw new Error(`Merge failed (${res.status})`); const data = await res.json(); + if (res.status === 207) { + setScanError(data.message || "Warning: Partial merge."); + } logEvent("merge", `Merged ${duplicateId} → ${primaryId} · ${data.edges_updated ?? 0} edges redirected`, { primary: primaryId, duplicate: duplicateId, @@ -207,7 +211,7 @@ export function EntityResolutionTab() { }); setPairs((prev) => prev.filter((p) => !(p.a.id === primaryId && p.b.id === duplicateId))); } catch (err) { - console.error("[EntityResolution] merge failed", err); + setScanError(err instanceof Error ? err.message : "Merge failed"); } }, []); diff --git a/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx b/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx index bd53cc14..8b3f4e0d 100644 --- a/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx +++ b/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx @@ -33,6 +33,7 @@ export function LineageDiagram() { const [edges, setEdges] = useState([]); const [searchId, setSearchId] = useState(""); const [activeId, setActiveId] = useState(""); + const [error, setError] = useState(""); const downloadReport = async (format: "json" | "markdown") => { if (!activeId) return; @@ -70,17 +71,18 @@ export function LineageDiagram() { if (!res.ok) { const text = await res.text(); - console.error(`HTTP ${res.status}: API Route missing or failed.`, text.substring(0, 100)); - return; + throw new Error(`HTTP ${res.status}: API Route missing or failed. ${text.substring(0, 100)}`); } const contentType = res.headers.get("content-type"); if (!contentType || !contentType.includes("application/json")) { - console.error("Backend returned non-JSON response (likely an HTML fallback). Check FastAPI routing."); - return; + throw new Error("Backend returned non-JSON response (likely an HTML fallback)."); } const data = await res.json(); + if (res.status === 207) { + setError(data.message || "Warning: Partial success loading lineage."); + } const counters: Record = { "group_agent": 0, "group_activity": 0, "group_entity": 0 }; @@ -109,7 +111,7 @@ export function LineageDiagram() { setNodes([...xLanes, ...mappedNodes]); setEdges(mappedEdges); } catch (err) { - console.error(err); + setError(err instanceof Error ? err.message : "Failed to load lineage."); } }; fetchLineage(); @@ -143,6 +145,12 @@ export function LineageDiagram() {
+ {error ? ( +
+ {error} +
+ ) : null} + {activeId ? ( diff --git a/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx b/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx index e882ba91..ce76e2bf 100644 --- a/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx +++ b/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx @@ -81,35 +81,40 @@ export function KGOverviewTab() { fetch("/api/graph/nodes?limit=500"), ]); - if (statsRes.ok) { - const statsData: KGStats = await statsRes.json(); - setStats(statsData); + if (!statsRes.ok) throw new Error(`Stats fetch failed (${statsRes.status})`); + if (!nodesRes.ok) throw new Error(`Nodes fetch failed (${nodesRes.status})`); + + const statsData: KGStats = await statsRes.json(); + setStats(statsData); + if (statsRes.status === 207) { + setError((statsData as any).message || "Warning: Partial success loading stats."); } - if (nodesRes.ok) { - const nodesData: NodeListResponse = await nodesRes.json(); - const nodes = nodesData.nodes ?? []; - setNodeTypeMap(buildTypeMap(nodes, "type")); + const nodesData: NodeListResponse = await nodesRes.json(); + const nodes = nodesData.nodes ?? []; + setNodeTypeMap(buildTypeMap(nodes, "type")); + if (nodesRes.status === 207 && statsRes.status !== 207) { // Only overwrite error if not already set, or just append + setError((prev) => prev ? prev + " " + ((nodesData as any).message || "") : ((nodesData as any).message || "Warning: Partial success loading nodes.")); + } - // Simulate neighbor counts via edges fetch for top-N - const edgesRes = await fetch("/api/graph/edges?limit=2000"); - if (edgesRes.ok) { - const edgesData = await edgesRes.json(); - const edges: { source: string; target: string }[] = edgesData.edges ?? []; - const degreeMap: Record = {}; - for (const edge of edges) { - degreeMap[edge.source] = (degreeMap[edge.source] ?? 0) + 1; - degreeMap[edge.target] = (degreeMap[edge.target] ?? 0) + 1; - } - const sorted = nodes - .map((n) => ({ node: n, neighborCount: degreeMap[n.id] ?? 0 })) - .sort((a, b) => b.neighborCount - a.neighborCount) - .slice(0, 10); - setTopNodes(sorted); + // Simulate neighbor counts via edges fetch for top-N + const edgesRes = await fetch("/api/graph/edges?limit=2000"); + if (edgesRes.ok) { + const edgesData = await edgesRes.json(); + const edges: { source: string; target: string }[] = edgesData.edges ?? []; + const degreeMap: Record = {}; + for (const edge of edges) { + degreeMap[edge.source] = (degreeMap[edge.source] ?? 0) + 1; + degreeMap[edge.target] = (degreeMap[edge.target] ?? 0) + 1; } + const sorted = nodes + .map((n) => ({ node: n, neighborCount: degreeMap[n.id] ?? 0 })) + .sort((a, b) => b.neighborCount - a.neighborCount) + .slice(0, 10); + setTopNodes(sorted); } - } catch { - setError("Failed to load graph overview. Ensure the server is running."); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load graph overview. Ensure the server is running."); } finally { setLoading(false); } @@ -135,6 +140,12 @@ export function KGOverviewTab() { return (
+ {error ? ( +
+ {error} +
+ ) : null} + {/* Header */}
diff --git a/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx b/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx index 363de1bc..3c6de057 100644 --- a/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx +++ b/explorer/src/workspaces/OntologyWorkspace/OntologyManager.tsx @@ -255,13 +255,13 @@ export function OntologyManager() { const params = new URLSearchParams(); if (searchQ) params.set("q", searchQ); const res = await fetch(`/api/ontology/registry?${params}`); - if (res.ok) { - setEntries(await res.json()); - } else { - setEntries([]); - } + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = await res.json(); + setEntries(data); + if (res.status === 207) flashMsg("err", data.message || "Warning: Partial success loading registry."); } catch { setEntries([]); + flashMsg("err", "Failed to load ontology registry"); } finally { setLoading(false); } diff --git a/explorer/src/workspaces/OntologyWorkspace/OntologySearch.tsx b/explorer/src/workspaces/OntologyWorkspace/OntologySearch.tsx index 5285aad4..8e8c79c8 100644 --- a/explorer/src/workspaces/OntologyWorkspace/OntologySearch.tsx +++ b/explorer/src/workspaces/OntologyWorkspace/OntologySearch.tsx @@ -182,9 +182,11 @@ function DetailPanel({ setLoading(true); setError(""); fetch(`/api/ontology/entity/${encodeURIComponent(uri)}`) - .then((r) => { + .then(async (r) => { if (!r.ok) throw new Error("Not found"); - return r.json(); + const data = await r.json(); + if (r.status === 207) setError(data.message || "Warning: Partial success loading entity."); + return data; }) .then(setDetail) .catch((e) => setError(e.message)) diff --git a/explorer/src/workspaces/OntologyWorkspace/SKOSVocabularyManager.tsx b/explorer/src/workspaces/OntologyWorkspace/SKOSVocabularyManager.tsx index d46951d5..62dff1e1 100644 --- a/explorer/src/workspaces/OntologyWorkspace/SKOSVocabularyManager.tsx +++ b/explorer/src/workspaces/OntologyWorkspace/SKOSVocabularyManager.tsx @@ -81,9 +81,11 @@ function ConceptDetailPanel({ setLoading(true); setError(""); fetch(`/api/ontology/skos/concept/${encodeURIComponent(uri)}`) - .then((r) => { + .then(async (r) => { if (!r.ok) throw new Error("Concept not found"); - return r.json(); + const data = await r.json(); + if (r.status === 207) setError(data.message || "Warning: Partial success loading concept."); + return data; }) .then(setDetail) .catch((e) => setError(e.message)) @@ -323,14 +325,23 @@ function SchemePanel({ const [expanded, setExpanded] = useState(true); const [hierarchy, setHierarchy] = useState([]); const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); useEffect(() => { if (!expanded) return; setLoading(true); fetch(`/api/vocabulary/hierarchy?scheme=${encodeURIComponent(scheme.uri)}`) - .then((r) => (r.ok ? r.json() : [])) + .then(async (r) => { + if (!r.ok) throw new Error(`HTTP ${r.status}`); + const data = await r.json(); + if (r.status === 207) setError(data.message || "Warning: Partial success loading hierarchy."); + return data; + }) .then(setHierarchy) - .catch(() => setHierarchy([])) + .catch((err) => { + setHierarchy([]); + setError(err instanceof Error ? err.message : "Failed to load hierarchy."); + }) .finally(() => setLoading(false)); }, [scheme.uri, expanded]); @@ -366,6 +377,7 @@ function SchemePanel({ {expanded && (
+ {error ?
{error}
: null} {loading ? (
@@ -412,7 +424,12 @@ export function SKOSVocabularyManager({ schemeUri }: Props) { useEffect(() => { setLoading(true); fetch("/api/ontology/skos/schemes") - .then((r) => (r.ok ? r.json() : [])) + .then(async (r) => { + if (!r.ok) throw new Error(`Failed to load schemes (${r.status})`); + const data = await r.json(); + if (r.status === 207) setError(data.message || "Warning: Partial success loading schemes."); + return data; + }) .then(setSchemes) .catch((e) => setError(e.message)) .finally(() => setLoading(false)); @@ -424,6 +441,7 @@ export function SKOSVocabularyManager({ schemeUri }: Props) { return (
+ {error ?
{error}
: null} {/* Search bar */}
@@ -628,6 +646,8 @@ const navLinkStyle: React.CSSProperties = { textAlign: "left", }; +const errorStyle: React.CSSProperties = { padding: 12, borderRadius: 14, color: "#ffb4c2", background: "rgba(255,157,175,0.1)", border: "1px solid rgba(255,157,175,0.18)", margin: "0 10px 10px 10px", fontSize: 12 }; + const centerStyle: React.CSSProperties = { display: "flex", flexDirection: "column", diff --git a/explorer/src/workspaces/OntologyWorkspace/ShaclStudio.tsx b/explorer/src/workspaces/OntologyWorkspace/ShaclStudio.tsx index 5a64ee9b..7c152d1d 100644 --- a/explorer/src/workspaces/OntologyWorkspace/ShaclStudio.tsx +++ b/explorer/src/workspaces/OntologyWorkspace/ShaclStudio.tsx @@ -33,7 +33,10 @@ export function ShaclStudio({ onJumpToNode }: ShaclStudioProps) { setRegistry(entries); setSelectedUri((current) => current || entries[0]?.uri || ""); }) - .catch(() => { /* backend unavailable — leave registry empty */ }); + .catch((err) => { + if (cancelled) return; + setError(err instanceof Error ? err.message : "Failed to load ontology registry."); + }); return () => { cancelled = true; }; diff --git a/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx b/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx index f9176c32..d7188fc4 100644 --- a/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx +++ b/explorer/src/workspaces/OntologyWorkspace/VersionsTab.tsx @@ -47,6 +47,7 @@ export function VersionsTab() { const [comparePair, setComparePair] = useState<{ v1: string; v2: string } | null>(null); const [compareResult, setCompareResult] = useState | null>(null); const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(""); const loadVersions = useCallback(async () => { if (!ontologyUri) return; @@ -55,9 +56,12 @@ export function VersionsTab() { if (response.ok) { const data = await response.json(); setVersions(data); + if (response.status === 207) setError(data.message || "Warning: Partial success loading versions."); + } else { + setError(`Failed to load versions (${response.status})`); } - } catch (error) { - console.error("Failed to load versions:", error); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load versions."); } }, [ontologyUri]); @@ -67,9 +71,12 @@ export function VersionsTab() { if (response.ok) { const data = await response.json(); setProposals(data); + if (response.status === 207) setError(data.message || "Warning: Partial success loading proposals."); + } else { + setError(`Failed to load proposals (${response.status})`); } - } catch (error) { - console.error("Failed to load proposals:", error); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to load proposals."); } }, []); @@ -79,54 +86,76 @@ export function VersionsTab() { }, [loadVersions, loadProposals]); const approveProposal = useCallback(async (proposalId: string) => { + setError(""); try { const response = await fetch(`/api/ontology/proposals/${proposalId}/approve`, { method: "POST", }); if (response.ok) { - alert("Proposal approved"); + if (response.status === 207) { + const data = await response.json().catch(() => ({})); + setError(data.message || "Warning: Partial success approving proposal."); + } else { + alert("Proposal approved"); + } loadProposals(); + } else { + setError(`Failed to approve proposal (${response.status})`); } - } catch (error) { - console.error("Failed to approve proposal:", error); - alert("Failed to approve proposal"); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to approve proposal."); } }, [loadProposals]); const rejectProposal = useCallback(async (proposalId: string) => { + setError(""); try { const response = await fetch(`/api/ontology/proposals/${proposalId}/reject`, { method: "POST", }); if (response.ok) { - alert("Proposal rejected"); + if (response.status === 207) { + const data = await response.json().catch(() => ({})); + setError(data.message || "Warning: Partial success rejecting proposal."); + } else { + alert("Proposal rejected"); + } loadProposals(); + } else { + setError(`Failed to reject proposal (${response.status})`); } - } catch (error) { - console.error("Failed to reject proposal:", error); - alert("Failed to reject proposal"); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to reject proposal."); } }, [loadProposals]); const publishProposal = useCallback(async (proposalId: string) => { + setError(""); try { const response = await fetch(`/api/ontology/proposals/${proposalId}/publish`, { method: "POST", }); if (response.ok) { - alert("Proposal published"); + if (response.status === 207) { + const data = await response.json().catch(() => ({})); + setError(data.message || "Warning: Partial success publishing proposal."); + } else { + alert("Proposal published"); + } loadProposals(); loadVersions(); + } else { + setError(`Failed to publish proposal (${response.status})`); } - } catch (error) { - console.error("Failed to publish proposal:", error); - alert("Failed to publish proposal"); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to publish proposal."); } }, [loadProposals, loadVersions]); const runVersionComparison = useCallback(async () => { if (!comparePair || !ontologyUri) return; setIsLoading(true); + setError(""); try { const response = await fetch(`/api/ontology/versions/${encodeURIComponent(ontologyUri)}/compare`, { method: "POST", @@ -138,11 +167,13 @@ export function VersionsTab() { }); if (response.ok) { const data = await response.json(); + if (response.status === 207) setError(data.message || "Warning: Partial success comparing versions."); setCompareResult(data); + } else { + setError(`Failed to compare versions (${response.status})`); } - } catch (error) { - console.error("Failed to compare versions:", error); - alert("Failed to compare versions"); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to compare versions."); } finally { setIsLoading(false); } @@ -265,6 +296,8 @@ export function VersionsTab() { marginBottom: "12px", }; + const errorStyle: React.CSSProperties = { padding: "12px", borderRadius: "14px", color: "#ffb4c2", background: "rgba(255,157,175,0.1)", border: "1px solid rgba(255,157,175,0.18)", marginBottom: "16px" }; + return (
@@ -278,6 +311,8 @@ export function VersionsTab() { />
+ {error ?
{error}
: null} +

diff --git a/explorer/src/workspaces/OntologyWorkspace/api.ts b/explorer/src/workspaces/OntologyWorkspace/api.ts index d6294fa7..9ad6a353 100644 --- a/explorer/src/workspaces/OntologyWorkspace/api.ts +++ b/explorer/src/workspaces/OntologyWorkspace/api.ts @@ -20,7 +20,11 @@ async function parseResponse(response: Response): Promise { } throw new Error(detail); } - return response.json() as Promise; + const data = await response.json(); + if (response.status === 207) { + console.warn("Partial Success:", data.message || "Warning: 207 Multi-Status"); + } + return data as T; } export async function loadOntologyRegistry(): Promise { diff --git a/explorer/src/workspaces/ReasoningWorkspace.tsx b/explorer/src/workspaces/ReasoningWorkspace.tsx index 1c700bda..f8405c26 100644 --- a/explorer/src/workspaces/ReasoningWorkspace.tsx +++ b/explorer/src/workspaces/ReasoningWorkspace.tsx @@ -57,6 +57,7 @@ export function ReasoningWorkspace() { }); const data = await response.json(); if (!response.ok) throw new Error(data.detail || `Status ${response.status}`); + if (response.status === 207) setError(data.message || "Warning: Partial success reasoning."); setResult(data); if (data.mutated) queryClient.invalidateQueries({ queryKey: ["graph", "full-load"] }); } catch (e) { diff --git a/explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx b/explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx index 82bf90dd..dfcfa60f 100644 --- a/explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx +++ b/explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx @@ -72,7 +72,17 @@ export function SparqlWorkspace() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query }), }); + if (!res.ok && res.status !== 400 && res.status !== 500) { + // Some specific errors may return 400/500 with JSON payload + if (!res.headers.get("content-type")?.includes("application/json")) { + throw new Error(`HTTP ${res.status}`); + } + } const data = await res.json(); + if (res.status === 207) { + data.error = data.message || "Warning: Partial success running query."; + } + if (data.error && data.error_line && monaco && editorRef.current) { // eslint-disable-next-line @typescript-eslint/no-explicit-any (monaco as any).editor.setModelMarkers((editorRef.current as any).getModel(), "sparql", [{ @@ -81,6 +91,7 @@ export function SparqlWorkspace() { endLineNumber: data.error_line, endColumn: 100, message: data.error, + // eslint-disable-next-line @typescript-eslint/no-explicit-any severity: (monaco as any).MarkerSeverity.Error, }]); }