From eda98f82176078d541ef723aa9b3a07f0af6c75a Mon Sep 17 00:00:00 2001 From: chitcommit <208086304+chitcommit@users.noreply.github.com> Date: Fri, 27 Mar 2026 00:36:11 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20address=20PR=20#68=20review=20?= =?UTF-8?q?=E2=80=94=20silent=20failures,=20path=20traversal,=20dead=20cod?= =?UTF-8?q?e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical: - Encode user-supplied caseId in evidence API URLs (path traversal risk) - Surface contradiction fetch failures in Evidence page (was silent empty) - Show error state on transaction fetch failure in Accounts (was silent empty) - Show dispute load error in LitigationAssistant picker (was empty catch) Important: - Remove dead actionIcon/Icon code from Recommendations - Remove unused RefreshCw import from Accounts - Guard useEffect pre-population to run once (prevents overwriting user edits) - Restore console.error logging in Recommendations act/dismiss - Clear stale data when loading new case in Evidence - Surface timeline.partial warning in Evidence page - Show retry on history load failure in ActionQueue - Clear error state on reload in Legal page - Surface missing dispute from URL param in LitigationAssistant Co-Authored-By: Claude Opus 4.6 --- ui/src/lib/api.ts | 8 ++++---- ui/src/pages/Accounts.tsx | 9 +++++++-- ui/src/pages/ActionQueue.tsx | 13 +++++++++++-- ui/src/pages/Evidence.tsx | 16 +++++++++++++--- ui/src/pages/Legal.tsx | 1 + ui/src/pages/LitigationAssistant.tsx | 17 +++++++++++++---- ui/src/pages/Recommendations.tsx | 13 +++---------- 7 files changed, 52 insertions(+), 25 deletions(-) diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index 31a73cf..9edc254 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -364,14 +364,14 @@ export const api = { const qs = new URLSearchParams( Object.fromEntries(Object.entries(params || {}).filter(([, v]) => v != null)), ).toString(); - return request(`/cases/${caseId}/timeline${qs ? '?' + qs : ''}`); + return request(`/cases/${encodeURIComponent(caseId)}/timeline${qs ? '?' + qs : ''}`); }, getCaseFacts: (caseId: string) => - request<{ caseId: string; facts: TimelineFact[] }>(`/cases/${caseId}/facts`), + request<{ caseId: string; facts: TimelineFact[] }>(`/cases/${encodeURIComponent(caseId)}/facts`), getCaseContradictions: (caseId: string) => - request<{ caseId: string; contradictions: Contradiction[] }>(`/cases/${caseId}/contradictions`), + request<{ caseId: string; contradictions: Contradiction[] }>(`/cases/${encodeURIComponent(caseId)}/contradictions`), getPendingFacts: (caseId: string, limit?: number) => - request<{ caseId: string; pending: TimelineFact[] }>(`/cases/${caseId}/pending-facts${limit ? '?limit=' + limit : ''}`), + request<{ caseId: string; pending: TimelineFact[] }>(`/cases/${encodeURIComponent(caseId)}/pending-facts${limit ? '?limit=' + limit : ''}`), // Litigation Assistant litigationSynthesize: (data: { rawNotes: string; property?: string; caseNumber?: string }) => diff --git a/ui/src/pages/Accounts.tsx b/ui/src/pages/Accounts.tsx index dac03e2..7b9238a 100644 --- a/ui/src/pages/Accounts.tsx +++ b/ui/src/pages/Accounts.tsx @@ -4,7 +4,7 @@ import { Card } from '../components/ui/Card'; import { ActionButton } from '../components/ui/ActionButton'; import { formatCurrency, formatDate, cn } from '../lib/utils'; import { useToast } from '../lib/toast'; -import { ChevronDown, ChevronUp, RefreshCw, ArrowDownLeft, ArrowUpRight } from 'lucide-react'; +import { ChevronDown, ChevronUp, ArrowDownLeft, ArrowUpRight } from 'lucide-react'; export function Accounts() { const [accounts, setAccounts] = useState([]); @@ -12,6 +12,7 @@ export function Accounts() { const [expandedId, setExpandedId] = useState(null); const [transactions, setTransactions] = useState([]); const [txLoading, setTxLoading] = useState(false); + const [txError, setTxError] = useState(null); const [syncing, setSyncing] = useState(false); const toast = useToast(); @@ -27,11 +28,13 @@ export function Accounts() { } setExpandedId(id); setTxLoading(true); + setTxError(null); try { const data = await api.getAccount(id); setTransactions(data.transactions || []); - } catch { + } catch (e: unknown) { setTransactions([]); + setTxError(e instanceof Error ? e.message : 'Failed to load transactions'); } finally { setTxLoading(false); } @@ -129,6 +132,8 @@ export function Accounts() {
{txLoading ? (

Loading transactions...

+ ) : txError ? ( +

Failed to load transactions: {txError}

) : transactions.length === 0 ? (

No recent transactions

) : ( diff --git a/ui/src/pages/ActionQueue.tsx b/ui/src/pages/ActionQueue.tsx index 213eb81..fec5612 100644 --- a/ui/src/pages/ActionQueue.tsx +++ b/ui/src/pages/ActionQueue.tsx @@ -89,13 +89,17 @@ export function ActionQueue() { } }, []); + const [historyError, setHistoryError] = useState(false); + const loadHistory = useCallback(async () => { setHistoryLoading(true); + setHistoryError(false); try { const data = await api.getQueueHistory(50); setHistory(data); - } catch { - // History is non-critical + } catch (e: unknown) { + console.error('[ActionQueue] history load failed:', e); + setHistoryError(true); } finally { setHistoryLoading(false); } @@ -267,6 +271,11 @@ export function ActionQueue() {
{historyLoading ? (

Loading history...

+ ) : historyError ? ( + +

Failed to load decision history.

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

No decision history yet.

diff --git a/ui/src/pages/Evidence.tsx b/ui/src/pages/Evidence.tsx index 189722d..939ad3f 100644 --- a/ui/src/pages/Evidence.tsx +++ b/ui/src/pages/Evidence.tsx @@ -23,14 +23,22 @@ export function Evidence() { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [contradictionError, setContradictionError] = useState(false); + const loadTimeline = useCallback(async () => { if (!caseId.trim()) return; setLoading(true); setError(null); + setTimeline(null); + setContradictions([]); + setContradictionError(false); try { const [tl, ctr] = await Promise.all([ api.getCaseTimeline(caseId), - api.getCaseContradictions(caseId).catch(() => ({ caseId, contradictions: [] })), + api.getCaseContradictions(caseId).catch(() => { + setContradictionError(true); + return { caseId, contradictions: [] as Contradiction[] }; + }), ]); setTimeline(tl); setContradictions(ctr.contradictions); @@ -87,9 +95,11 @@ export function Evidence() { 0 ? 'text-urgency-red' : 'text-urgency-green'} />
- {timeline.warnings && timeline.warnings.length > 0 && ( + {(timeline.partial || contradictionError || (timeline.warnings && timeline.warnings.length > 0)) && (
- {timeline.warnings.map((w, i) =>

{w}

)} + {timeline.partial &&

Warning: Timeline data may be incomplete — some sources returned partial results.

} + {contradictionError &&

Warning: Contradictions could not be loaded — data may be incomplete.

} + {timeline.warnings?.map((w, i) =>

{w}

)}
)} diff --git a/ui/src/pages/Legal.tsx b/ui/src/pages/Legal.tsx index 1f3a8ec..e084755 100644 --- a/ui/src/pages/Legal.tsx +++ b/ui/src/pages/Legal.tsx @@ -24,6 +24,7 @@ export function Legal() { }); const load = () => { + setError(null); api.getLegalDeadlines().then(setDeadlines).catch((e) => setError(e.message)); }; diff --git a/ui/src/pages/LitigationAssistant.tsx b/ui/src/pages/LitigationAssistant.tsx index 01e6a69..f6cb85b 100644 --- a/ui/src/pages/LitigationAssistant.tsx +++ b/ui/src/pages/LitigationAssistant.tsx @@ -50,24 +50,33 @@ export function LitigationAssistant() { const synthesisRef = useRef(null); const draftRef = useRef(null); + const [disputeLoadError, setDisputeLoadError] = useState(false); + const prePopulated = useRef(false); + // Load disputes for the picker useEffect(() => { - api.getDisputes().then(setDisputes).catch(() => {}); + api.getDisputes() + .then(setDisputes) + .catch(() => setDisputeLoadError(true)); }, []); - // Pre-populate from dispute context if URL has ?dispute=ID + // Pre-populate from dispute context if URL has ?dispute=ID (once only) useEffect(() => { + if (prePopulated.current) return; const params = new URLSearchParams(window.location.search); const disputeId = params.get('dispute'); if (disputeId && disputes.length > 0) { + prePopulated.current = true; setSelectedDisputeId(disputeId); const d = disputes.find(dd => dd.id === disputeId); if (d) { if (d.counterparty) setRecipient(d.counterparty); if (d.description) setRawNotes(prev => prev || d.description || ''); + } else { + toast.error('Dispute not found', `Linked dispute ${disputeId.slice(0, 8)}... was not found`); } } - }, [disputes]); + }, [disputes, toast]); const saveToDispute = async () => { if (!selectedDisputeId || !draft) return; @@ -356,7 +365,7 @@ export function LitigationAssistant() { onChange={(e) => setSelectedDisputeId(e.target.value)} className="flex-1 px-2 py-1.5 rounded-lg bg-slate-50 border border-slate-200 text-card-text text-xs focus:outline-none" > - + {disputes.map((d) => (