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, }]); }