fix: address PR #68 review — silent failures, path traversal, dead code#69
fix: address PR #68 review — silent failures, path traversal, dead code#69chitcommit merged 2 commits intomainfrom
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
📝 WalkthroughWalkthroughAdded a shared query-string builder and URL-encoding for Evidence Timeline case IDs; introduced explicit error state handling and user-facing error UI across multiple pages; removed unused icon imports; added console.error logging; moved a constant to module scope; updated TypeScript build metadata. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
To use Codex here, create a Codex account and connect to github. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ui/src/pages/LitigationAssistant.tsx`:
- Around line 67-76: The code sets selectedDisputeId before verifying the
dispute exists, which can leave an invalid ID in state; update the logic in the
block that reads disputeId/disputes so you first find the dispute via
disputes.find(dd => dd.id === disputeId) and only then call
setSelectedDisputeId(disputeId) and mark prePopulated.current = true; if the
dispute is found, also setRecipient(d.counterparty) and setRawNotes(...) as you
already do, otherwise leave selectedDisputeId unchanged and show the toast.error
(toast.error(...) remains in the else branch); ensure prePopulated.current is
only set when the dispute exists to avoid signaling a successful pre-population
for an invalid ID.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c310ab0e-4695-440a-95a8-0e86cf8534c4
📒 Files selected for processing (7)
ui/src/lib/api.tsui/src/pages/Accounts.tsxui/src/pages/ActionQueue.tsxui/src/pages/Evidence.tsxui/src/pages/Legal.tsxui/src/pages/LitigationAssistant.tsxui/src/pages/Recommendations.tsx
29da8b5 to
2ac8fba
Compare
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
chittycommand-ui | 0234755 | Mar 28 2026, 08:11 AM |
|
|
To use Codex here, create a Codex account and connect to github. |
🧩 Analysis chain
🏁 Script executed: Length of output: 111313
🔐 Security Review — PR
|
| Method | Unencoded segment | Risk level |
|---|---|---|
triggerSync(source) |
/sync/trigger/${source} |
Medium — source is a string, not a UUID; could be user-visible |
getDisputes(status?) |
?status=' + status |
Low — query param injection |
getCalendar(start, end) |
?start=${start}&end=${end} |
Low — query param injection |
getAccount(id), markPaid(id), getDispute(id), etc. |
/resource/${id}/... |
Low — IDs are likely DB UUIDs from server |
The highest-risk outlier is triggerSync:
// api.ts — still unguarded
triggerSync: (source: string) =>
request<{ message: string }>(`/sync/trigger/${source}`, { method: 'POST' }),If source values are ever populated from a user-editable field or URL param, this is a viable path traversal vector. It should be encoded for consistency:
triggerSync: (source: string) =>
request<{ message: string }>(`/sync/trigger/${encodeURIComponent(source)}`, { method: 'POST' }),All other UUID-style ID params carry lower risk since the server issued them, but the fix pattern applied here should eventually be applied consistently.
🟡 Low — ActionQueue: Repeated Failed API Calls on Tab Switch
After historyError is set, history.length remains 0. The trigger condition is:
// ActionQueue.tsx
useEffect(() => {
if (activeTab === 'history' && history.length === 0) loadHistory();
}, [activeTab, history.length, loadHistory]);Every time a user switches away from and back to the History tab while in error state, loadHistory() fires again — silently hammering a failing endpoint. The Retry button is the right mechanism; the auto-trigger should be suppressed in error state:
useEffect(() => {
if (activeTab === 'history' && history.length === 0 && !historyError) loadHistory();
}, [activeTab, history.length, historyError, loadHistory]);🟡 Low — ActionQueue: Retry Button Has No In-Flight Guard
The Retry button renders as:
<ActionButton label="Retry" variant="secondary" onClick={loadHistory} className="mt-2" />loadHistory sets historyLoading = true but ActionButton is not passed loading={historyLoading} or disabled={historyLoading}, so rapid clicks can launch concurrent fetches. Add loading={historyLoading} to the button.
🟡 Low — confirmFollowThrough Missing console.error (Inconsistency)
act and dismiss in Recommendations.tsx now log errors, but confirmFollowThrough's catch block still silently sets error state:
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'Action failed');
// ← no console.error here
}Minor, but inconsistent with the stated fix objective.
✅ Confirmed Safe
| Area | Finding |
|---|---|
encodeURIComponent on caseId |
Correctly prevents ../-style traversal; / is encoded to %2F |
timeline.warnings rendered as JSX |
React auto-escapes; no XSS path |
disputeId from URL in toast |
.slice(0, 8) + React auto-escape; no XSS path |
txError rendered in Accounts |
React auto-escapes API error strings |
prePopulated.current guard |
Correct — fires only once disputes load; no bypass path |
setError(null) before fetch in Legal |
Clean pattern, no race condition introduced by this PR |
| Dead code removal (Recommendations) | Reduces surface; tsc --noEmit impact is positive |
Summary
| Severity | Count | Blocking? |
|---|---|---|
| 🔴 Critical | 0 | — |
| 🟠 Medium | 1 (triggerSync path encoding) |
Recommend fix before merge |
| 🟡 Low | 3 (history retry loop, concurrent retry, console.error inconsistency) | Non-blocking |
| ✅ Clean | All stated fixes verified | — |
The core security objective of the PR (path traversal via caseId) is correctly addressed. The medium finding above (triggerSync) is a pre-existing pattern that this PR makes newly inconsistent — worth fixing in this PR or tracked as a follow-up.
🧠 Learnings used
Learnt from: CR
Repo: chittyos/chittycommand PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-04T04:42:57.380Z
Learning: Resolve must-fix review comments (security, correctness, compliance, merge blockers) before merge
Learnt from: CR
Repo: chittyos/chittycommand PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-09T16:02:20.597Z
Learning: Applies to src/routes/ledger.ts : Implement ChittyLedger evidence/custody passthrough in `src/routes/ledger.ts`
✅ Actions performed
Review triggered.
Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
ui/src/pages/Evidence.tsx (1)
28-50:⚠️ Potential issue | 🟠 MajorPrevent stale async responses from overwriting newer case results.
If users trigger
loadTimelinetwice quickly, an older request can finish later and still write state (Line 43, Line 44, Line 46), replacing newer case data.Suggested fix (request sequencing guard)
-import { useState, useCallback } from 'react'; +import { useState, useCallback, useRef } from 'react'; @@ export function Evidence() { @@ const [contradictionError, setContradictionError] = useState(false); + const latestRequestRef = useRef(0); @@ const loadTimeline = useCallback(async () => { - if (!caseId.trim()) return; + const requestedCaseId = caseId.trim(); + if (!requestedCaseId) return; + const requestId = ++latestRequestRef.current; + setLoading(true); setError(null); setTimeline(null); setContradictions([]); setContradictionError(false); try { const [tl, ctr] = await Promise.all([ - api.getCaseTimeline(caseId), - api.getCaseContradictions(caseId).catch(() => { + api.getCaseTimeline(requestedCaseId), + api.getCaseContradictions(requestedCaseId).catch(() => { setContradictionError(true); - return { caseId, contradictions: [] as Contradiction[] }; + return { caseId: requestedCaseId, contradictions: [] as Contradiction[] }; }), ]); + if (requestId !== latestRequestRef.current) return; setTimeline(tl); setContradictions(ctr.contradictions); } catch (e: unknown) { + if (requestId !== latestRequestRef.current) return; setError(e instanceof Error ? e.message : 'Failed to load timeline'); } finally { - setLoading(false); + if (requestId === latestRequestRef.current) setLoading(false); } }, [caseId]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/pages/Evidence.tsx` around lines 28 - 50, The loadTimeline async can be overwritten by out-of-order responses; add a request-sequencing guard: introduce a mutable requestId (e.g., useRef<number> or module-scope counter) incremented at the start of loadTimeline, capture the current id in the closure before awaiting, and before calling setTimeline, setContradictions, setContradictionError or setError check that the captured id matches the latest requestId; only apply state updates when it matches (otherwise ignore stale results). This change should be applied inside the loadTimeline function around the Promise.all resolution and error/finally handlers so stale responses do not overwrite newer case data.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@ui/src/pages/Evidence.tsx`:
- Around line 28-50: The loadTimeline async can be overwritten by out-of-order
responses; add a request-sequencing guard: introduce a mutable requestId (e.g.,
useRef<number> or module-scope counter) incremented at the start of
loadTimeline, capture the current id in the closure before awaiting, and before
calling setTimeline, setContradictions, setContradictionError or setError check
that the captured id matches the latest requestId; only apply state updates when
it matches (otherwise ignore stale results). This change should be applied
inside the loadTimeline function around the Promise.all resolution and
error/finally handlers so stale responses do not overwrite newer case data.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 837fafc7-bbb1-422d-84ca-768d219d1e32
📒 Files selected for processing (7)
ui/src/lib/api.tsui/src/pages/Accounts.tsxui/src/pages/ActionQueue.tsxui/src/pages/Evidence.tsxui/src/pages/Legal.tsxui/src/pages/LitigationAssistant.tsxui/src/pages/Recommendations.tsx
✅ Files skipped from review due to trivial changes (2)
- ui/src/pages/Legal.tsx
- ui/src/pages/Recommendations.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- ui/src/pages/Accounts.tsx
- ui/src/pages/LitigationAssistant.tsx
- ui/src/lib/api.ts
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 <noreply@anthropic.com>
2ac8fba to
eda98f8
Compare
|
|
To use Codex here, create a Codex account and connect to github. |
|
Adversarial Review Summary — PR
|
| Category | Result |
|---|---|
| Merge-gating bypass | ✅ None detected |
| Policy bypass paths | ✅ None detected |
| Regression risk | 🟡 Low (see below) |
| All 10 stated fixes present | ✅ Verified |
Findings:
- 🟠 Medium —
triggerSyncpath encoding (api.ts): The PR fixes 4caseIdendpoints but leavestriggerSync(source)with raw interpolation (/sync/trigger/${source}). Ifsourceis ever user-supplied, this is a traversal vector. RecommendencodeURIComponent(source)for consistency. - 🟡 Low — ActionQueue retry loop:
historyErrorstate is not checked in theuseEffecttrigger condition, so switching tabs while in error state re-firesloadHistory()on each visit. Fix: add&& !historyErrorto the guard. - 🟡 Low — Retry button concurrent requests: The Retry button does not receive
loading={historyLoading}, allowing rapid-click concurrency. - 🟡 Low —
confirmFollowThroughmissingconsole.error: Inconsistent with the stated fix objective foract/dismissinRecommendations.tsx.
No security regressions were introduced. The primary objective (path traversal via caseId) is correctly addressed.
🧠 Learnings used
Learnt from: CR
Repo: chittyos/chittycommand PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-04T04:42:57.380Z
Learning: Resolve must-fix review comments (security, correctness, compliance, merge blockers) before merge
Learnt from: CR
Repo: chittyos/chittycommand PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-09T16:02:20.597Z
Learning: Applies to src/routes/ledger.ts : Implement ChittyLedger evidence/custody passthrough in `src/routes/ledger.ts`
✅ Actions performed
Review triggered.
Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
ui/src/pages/LitigationAssistant.tsx (1)
68-77:⚠️ Potential issue | 🟡 MinorInvalid dispute ID can remain in state.
setSelectedDisputeId(disputeId)is called on line 70 before validation. If the dispute isn't found, the invalid ID stays in state and could affect follow-up actions (e.g.,saveToDisputechecksselectedDisputeIdbut the ID won't match any dispute).🔧 Proposed fix — set ID only after validation
if (disputeId && disputes.length > 0) { prePopulated.current = true; - setSelectedDisputeId(disputeId); const d = disputes.find(dd => dd.id === disputeId); if (d) { + setSelectedDisputeId(disputeId); 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`); } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/pages/LitigationAssistant.tsx` around lines 68 - 77, The code sets selectedDisputeId before validating it, so an invalid ID can remain in state; change the flow in the LitigatonAssistant component to only call setSelectedDisputeId(disputeId) after finding a matching dispute via disputes.find(...) (and setting prePopulated.current, recipient via d.counterparty, and raw notes via setRawNotes), and if no match is found, do not set the ID (or explicitly clear selectedDisputeId) and keep the existing toast.error behavior; also ensure subsequent logic that relies on selectedDisputeId (e.g., saveToDispute) will only run when selectedDisputeId matches an actual dispute.
🧹 Nitpick comments (3)
ui/src/pages/ActionQueue.tsx (2)
92-93: Guard auto-history fetch after a known failure.After a failed load, switching tabs back to History will re-trigger
loadHistorybecausehistory.lengthremains0. Gate the effect with!historyErrorso retries are user-driven.♻️ Suggested fix
useEffect(() => { - if (activeTab === 'history' && history.length === 0) loadHistory(); - }, [activeTab, history.length, loadHistory]); + if (activeTab === 'history' && history.length === 0 && !historyError) { + loadHistory(); + } + }, [activeTab, history.length, historyError, loadHistory]);Also applies to: 113-115
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/pages/ActionQueue.tsx` around lines 92 - 93, The effect that auto-fetches history should be guarded by the historyError state so a prior failure doesn't cause repeated automatic retries; update the effect that currently checks history.length (and the similar effect at lines 113-115) to also require !historyError before calling loadHistory, and ensure any loadHistory failure sets setHistoryError(true) so subsequent tab switches won't re-trigger automatic loads until the user explicitly clears or retries.
274-278: Retry action should reflect in-flight state.Line 277 triggers reload but the button does not expose loading/disabled behavior; quick repeated clicks can fire concurrent retries.
♻️ Suggested fix
- <ActionButton label="Retry" variant="secondary" onClick={loadHistory} className="mt-2" /> + <ActionButton + label="Retry" + variant="secondary" + onClick={loadHistory} + className="mt-2" + loading={historyLoading} + disabled={historyLoading} + />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/pages/ActionQueue.tsx` around lines 274 - 278, The retry button in ActionQueue.tsx uses onClick={loadHistory} but does not reflect or prevent an in-flight reload; add a loading flag (e.g., isLoadingHistory) driven by the loadHistory function and the historyError state, pass that flag to the ActionButton's loading/disabled props so the button shows a spinner and is disabled while loadHistory is running, and ensure loadHistory sets isLoadingHistory=true at start and false on both success and error to prevent concurrent retries.ui/src/pages/LitigationAssistant.tsx (1)
57-61: Consider addingconsole.errorfor debugging consistency.The PR restored
console.errorlogging in other flows (Recommendations). Adding it here would aid debugging when disputes fail to load.♻️ Suggested improvement
useEffect(() => { api.getDisputes() .then(setDisputes) - .catch(() => setDisputeLoadError(true)); + .catch((err) => { + console.error('Failed to load disputes:', err); + setDisputeLoadError(true); + }); }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/pages/LitigationAssistant.tsx` around lines 57 - 61, The fetch in useEffect calling api.getDisputes swallows error details—when the promise rejects it only calls setDisputeLoadError(true); update the catch handler for api.getDisputes to also log the error (e.g., console.error) along with a clear message so failures to load disputes are visible; make the change in the useEffect that references api.getDisputes, setDisputes, and setDisputeLoadError.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@ui/src/pages/LitigationAssistant.tsx`:
- Around line 68-77: The code sets selectedDisputeId before validating it, so an
invalid ID can remain in state; change the flow in the LitigatonAssistant
component to only call setSelectedDisputeId(disputeId) after finding a matching
dispute via disputes.find(...) (and setting prePopulated.current, recipient via
d.counterparty, and raw notes via setRawNotes), and if no match is found, do not
set the ID (or explicitly clear selectedDisputeId) and keep the existing
toast.error behavior; also ensure subsequent logic that relies on
selectedDisputeId (e.g., saveToDispute) will only run when selectedDisputeId
matches an actual dispute.
---
Nitpick comments:
In `@ui/src/pages/ActionQueue.tsx`:
- Around line 92-93: The effect that auto-fetches history should be guarded by
the historyError state so a prior failure doesn't cause repeated automatic
retries; update the effect that currently checks history.length (and the similar
effect at lines 113-115) to also require !historyError before calling
loadHistory, and ensure any loadHistory failure sets setHistoryError(true) so
subsequent tab switches won't re-trigger automatic loads until the user
explicitly clears or retries.
- Around line 274-278: The retry button in ActionQueue.tsx uses
onClick={loadHistory} but does not reflect or prevent an in-flight reload; add a
loading flag (e.g., isLoadingHistory) driven by the loadHistory function and the
historyError state, pass that flag to the ActionButton's loading/disabled props
so the button shows a spinner and is disabled while loadHistory is running, and
ensure loadHistory sets isLoadingHistory=true at start and false on both success
and error to prevent concurrent retries.
In `@ui/src/pages/LitigationAssistant.tsx`:
- Around line 57-61: The fetch in useEffect calling api.getDisputes swallows
error details—when the promise rejects it only calls setDisputeLoadError(true);
update the catch handler for api.getDisputes to also log the error (e.g.,
console.error) along with a clear message so failures to load disputes are
visible; make the change in the useEffect that references api.getDisputes,
setDisputes, and setDisputeLoadError.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 979321ac-6477-49f7-9547-2ce299eef907
📒 Files selected for processing (7)
ui/src/lib/api.tsui/src/pages/Accounts.tsxui/src/pages/ActionQueue.tsxui/src/pages/Evidence.tsxui/src/pages/Legal.tsxui/src/pages/LitigationAssistant.tsxui/src/pages/Recommendations.tsx
✅ Files skipped from review due to trivial changes (2)
- ui/src/pages/Recommendations.tsx
- ui/src/pages/Accounts.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- ui/src/pages/Legal.tsx
- ui/src/lib/api.ts
- Recommendations: setRecs uses functional updater to prevent stale closure - api.ts: extract buildQs() helper, replaces 3 inline query string builders - RevenueSources: hoist emptyForm to module scope (avoids per-render alloc) - LitigationAssistant: remove duplicate prepopulated ref from conflict merge Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
|
To use Codex here, create a Codex account and connect to github. |
|
Adversarial Review — PR
|
| Severity | Issue | Location |
|---|---|---|
| 🟠 Medium | triggerSync(source) interpolates raw source into path — apply encodeURIComponent(source) for consistency with the 4 caseId fixes in this PR |
ui/src/lib/api.ts |
| 🟡 Low | useEffect trigger does not check historyError, so tab-switching in error state repeatedly fires loadHistory() — add && !historyError to the guard |
ui/src/pages/ActionQueue.tsx |
| 🟡 Low | Retry button lacks loading={historyLoading} / disabled guard — concurrent retries possible on rapid clicks |
ui/src/pages/ActionQueue.tsx |
| 🟡 Low | confirmFollowThrough catch block sets error state without console.error, inconsistent with the act/dismiss fix in this PR |
ui/src/pages/Recommendations.tsx |
No blocking/critical issues. The medium finding (triggerSync) is recommended to fix before merge or tracked as a follow-up.
🧠 Learnings used
Learnt from: CR
Repo: chittyos/chittycommand PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2026-03-04T04:42:57.380Z
Learning: Resolve must-fix review comments (security, correctness, compliance, merge blockers) before merge
Learnt from: CR
Repo: chittyos/chittycommand PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-09T16:02:20.597Z
Learning: Applies to src/routes/ledger.ts : Implement ChittyLedger evidence/custody passthrough in `src/routes/ledger.ts`
✅ Actions performed
Review triggered.
Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
ui/src/pages/Recommendations.tsx (1)
92-99:⚠️ Potential issue | 🟡 MinorUse functional state update in
dismissto avoid stale-state overwrites.The
dismissfunction capturesrecsat line 95. When multiple dismissals overlap, the captured state becomes stale, potentially reintroducing removed items. Use a functional updater instead.Proposed fix
- setRecs(recs.filter(r => r.id !== id)); + setRecs(prev => prev.filter(r => r.id !== id));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/pages/Recommendations.tsx` around lines 92 - 99, The dismiss function currently uses the captured recs variable when calling setRecs which can lead to stale-state overwrites during concurrent dismissals; update the setRecs call inside dismiss to use the functional updater form (e.g., setRecs(prev => prev.filter(r => r.id !== id))) so the update is applied against the latest state; keep the rest of the try/catch (including setError) unchanged and reference the dismiss function, setRecs, and recs when making the change.ui/src/lib/api.ts (1)
202-203:⚠️ Potential issue | 🟠 MajorEncode
sourceintriggerSyncpath.At Line 203, interpolating raw
sourcecan alter the URL path when it contains reserved characters. Encode this segment for the same safety posture used forcaseId.Suggested diff
triggerSync: (source: string) => - request<{ message: string }>(`/sync/trigger/${source}`, { method: 'POST' }), + request<{ message: string }>(`/sync/trigger/${encodeURIComponent(source)}`, { method: 'POST' }),Based on learnings: Resolve must-fix review comments (security, correctness, compliance, merge blockers) before merge.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/lib/api.ts` around lines 202 - 203, The triggerSync helper interpolates raw source into the URL which can break or alter the path for reserved characters; update the triggerSync function to URL-encode the source segment (e.g., use encodeURIComponent on the source parameter) before building the request path so it mirrors the safe handling done for caseId in other calls.
🧹 Nitpick comments (2)
ui/src/pages/Recommendations.tsx (1)
75-89: Add missing error log inconfirmFollowThroughcatch.This flow still swallows diagnostic context compared to
act/dismiss.Proposed patch
} catch (e: unknown) { - setError(e instanceof Error ? e.message : 'Action failed'); + const msg = e instanceof Error ? e.message : 'Action failed'; + console.error('[Recommendations] confirmFollowThrough failed:', msg, e); + setError(msg); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/pages/Recommendations.tsx` around lines 75 - 89, In the confirmFollowThrough catch block, add diagnostic logging of the caught error (including followThrough.recId and the computed action) before calling setError so the failure mirrors the act/dismiss flows; locate the confirmFollowThrough function and inside its catch for api.actOnRecommendation log the error (e.g., console.error or the app logger) with context about the recId and action_taken, then continue to setError(e instanceof Error ? e.message : 'Action failed') and keep existing state handling.ui/src/lib/api.ts (1)
95-101: ConstrainbuildQsto query-safe primitive values.
Record<string, unknown>can silently stringify objects to"[object Object]". Tightening the type prevents accidental malformed query parameters.Suggested diff
+type QueryParamValue = string | number | boolean; + -function buildQs(params?: Record<string, unknown>): string { +function buildQs(params?: Record<string, QueryParamValue | null | undefined>): 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 : ''; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/lib/api.ts` around lines 95 - 101, The buildQs function can accidentally stringify objects to "[object Object]"; restrict and validate allowed query-safe primitive types by changing the params signature from Record<string, unknown> to something like Record<string, string | number | boolean | null | undefined> and add a runtime filter in the Object.entries mapping (before passing to URLSearchParams) that excludes values where typeof v is 'object' or 'function' (or v === null handled explicitly) so only string/number/boolean are converted with String(v); update references to buildQs, URLSearchParams, and the Object.entries mapping accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@ui/src/lib/api.ts`:
- Line 372: The ternary using `limit ? '?limit=' + limit : ''` in the `request`
call when building the `/cases/${encodeURIComponent(caseId)}/pending-facts` URL
drops valid zero values; change the URL construction to include `limit` when it
is explicitly provided (e.g., use the existing `buildQs({ limit })` helper or
test `limit != null`) so `limit=0` is preserved; update the template passed to
`request` (the call where `pending-facts` is built) to incorporate the corrected
query-string logic.
---
Outside diff comments:
In `@ui/src/lib/api.ts`:
- Around line 202-203: The triggerSync helper interpolates raw source into the
URL which can break or alter the path for reserved characters; update the
triggerSync function to URL-encode the source segment (e.g., use
encodeURIComponent on the source parameter) before building the request path so
it mirrors the safe handling done for caseId in other calls.
In `@ui/src/pages/Recommendations.tsx`:
- Around line 92-99: The dismiss function currently uses the captured recs
variable when calling setRecs which can lead to stale-state overwrites during
concurrent dismissals; update the setRecs call inside dismiss to use the
functional updater form (e.g., setRecs(prev => prev.filter(r => r.id !== id)))
so the update is applied against the latest state; keep the rest of the
try/catch (including setError) unchanged and reference the dismiss function,
setRecs, and recs when making the change.
---
Nitpick comments:
In `@ui/src/lib/api.ts`:
- Around line 95-101: The buildQs function can accidentally stringify objects to
"[object Object]"; restrict and validate allowed query-safe primitive types by
changing the params signature from Record<string, unknown> to something like
Record<string, string | number | boolean | null | undefined> and add a runtime
filter in the Object.entries mapping (before passing to URLSearchParams) that
excludes values where typeof v is 'object' or 'function' (or v === null handled
explicitly) so only string/number/boolean are converted with String(v); update
references to buildQs, URLSearchParams, and the Object.entries mapping
accordingly.
In `@ui/src/pages/Recommendations.tsx`:
- Around line 75-89: In the confirmFollowThrough catch block, add diagnostic
logging of the caught error (including followThrough.recId and the computed
action) before calling setError so the failure mirrors the act/dismiss flows;
locate the confirmFollowThrough function and inside its catch for
api.actOnRecommendation log the error (e.g., console.error or the app logger)
with context about the recId and action_taken, then continue to setError(e
instanceof Error ? e.message : 'Action failed') and keep existing state
handling.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c60a7266-1a7b-448f-aa34-c5537fa69c4e
📒 Files selected for processing (4)
ui/src/components/planner/RevenueSources.tsxui/src/lib/api.tsui/src/pages/Recommendations.tsxui/tsconfig.tsbuildinfo
✅ Files skipped from review due to trivial changes (2)
- ui/src/components/planner/RevenueSources.tsx
- ui/tsconfig.tsbuildinfo
Summary
Addresses 10 findings from comprehensive PR review of #68 (8 outcome-driven UI flows):
Critical (4):
encodeURIComponenton user-suppliedcaseIdin evidence API URLs — prevents path traversal.catch(() => {}))Important (6):
actionIcon/Iconcode + unusedRefreshCwimportuseEffectpre-population to run once only (prevents overwriting user edits on re-render)console.errorlogging in Recommendations act/dismisstimeline.partialwarning banner in Evidence pageTest plan
?dispute=ID— should show toast errortsc --noEmit+vite buildpass🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Style