diff --git a/ui/src/components/planner/RevenueSources.tsx b/ui/src/components/planner/RevenueSources.tsx index 52838ae..53b5dd3 100644 --- a/ui/src/components/planner/RevenueSources.tsx +++ b/ui/src/components/planner/RevenueSources.tsx @@ -6,6 +6,8 @@ import { ActionButton } from '../ui/ActionButton'; import { useToast } from '../../lib/toast'; import { Plus, X, Pencil, Check } from 'lucide-react'; +const emptyForm = { description: '', amount: '', recurrence: 'monthly', source: 'manual', confidence: '0.8' }; + export function RevenueSources() { const [sources, setSources] = useState([]); const [summary, setSummary] = useState({ count: 0, total_monthly: 0, weighted_monthly: 0 }); @@ -15,8 +17,6 @@ export function RevenueSources() { const [editingId, setEditingId] = useState(null); const [saving, setSaving] = useState(false); const toast = useToast(); - - const emptyForm = { description: '', amount: '', recurrence: 'monthly', source: 'manual', confidence: '0.8' }; const [form, setForm] = useState(emptyForm); const load = useCallback(async () => { diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index 31a73cf..fc10a86 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -92,15 +92,21 @@ export interface TokenActionResponse { result: unknown; } +function buildQs(params?: Record): string { + if (!params) return ''; + const qs = new URLSearchParams( + Object.fromEntries(Object.entries(params).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])), + ).toString(); + return qs ? '?' + qs : ''; +} + export const api = { // Dashboard getDashboard: () => request('/dashboard'), // Obligations - getObligations: (params?: { status?: string; category?: string }) => { - const qs = new URLSearchParams(params as Record).toString(); - return request(`/obligations${qs ? '?' + qs : ''}`); - }, + getObligations: (params?: { status?: string; category?: string }) => + request(`/obligations${buildQs(params)}`), getCalendar: (start: string, end: string) => request(`/obligations/calendar?start=${start}&end=${end}`), createObligation: (data: Partial) => @@ -325,12 +331,8 @@ export const api = { }), // Tasks - getTasks: (params?: { status?: string; task_type?: string; source?: string; limit?: number; offset?: number }) => { - const qs = new URLSearchParams( - Object.fromEntries(Object.entries(params || {}).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])), - ).toString(); - return request<{ tasks: Task[]; total: number; limit: number; offset: number }>(`/tasks${qs ? '?' + qs : ''}`); - }, + getTasks: (params?: { status?: string; task_type?: string; source?: string; limit?: number; offset?: number }) => + request<{ tasks: Task[]; total: number; limit: number; offset: number }>(`/tasks${buildQs(params)}`), getTask: (id: string) => request<{ task: Task; actions: TaskAction[] }>(`/tasks/${id}`), updateTaskStatus: (id: string, status: string, notes?: string) => @@ -360,18 +362,14 @@ export const api = { request(`/legal/${id}`, { method: 'PATCH', body: JSON.stringify(data) }), // Evidence Timeline - getCaseTimeline: (caseId: string, params?: { start?: string; end?: string }) => { - const qs = new URLSearchParams( - Object.fromEntries(Object.entries(params || {}).filter(([, v]) => v != null)), - ).toString(); - return request(`/cases/${caseId}/timeline${qs ? '?' + qs : ''}`); - }, + getCaseTimeline: (caseId: string, params?: { start?: string; end?: string }) => + request(`/cases/${encodeURIComponent(caseId)}/timeline${buildQs(params)}`), 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) => (