feat: Real-time Risk Score & Loan Visualization#177
feat: Real-time Risk Score & Loan Visualization#177anonfedora merged 5 commits intoanonfedora:masterfrom
Conversation
|
@Pvsaint Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 12 minutes and 58 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (9)
📝 WalkthroughWalkthroughImplemented real-time risk score dashboard with WebSocket integration, loan timeline visualization, score simulation mode, and interactive tooltips. Updated risk score hook to fetch current scores, historical data, and simulation results from backend APIs. Added new components for timeline visualization and accessible tooltips, with parser utilities for backend response handling. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Page as WalletRiskPage
participant Hook as useRiskScore
participant API as Backend API
participant DB as Database
User->>Page: Navigate to risk page with [wallet]
Page->>Hook: Call useRiskScore(wallet, range)
activate Hook
Hook->>API: GET /api/risk/{wallet}
API->>DB: Query current risk score
DB-->>API: Return RiskScoreData
API-->>Hook: Score + breakdown components
Hook->>API: GET /api/risk/{wallet}/history?range={range}
API->>DB: Query historical scores
DB-->>API: Return HistoricalScore[]
API-->>Hook: Parsed RiskHistoryEntry[]
Hook-->>Page: {score, grade, breakdown, history, historyLoading}
deactivate Hook
Page->>Page: Render ScoreGauge, ScoreHistoryChart, ScoreBreakdown
User-->>Page: Charts display current & historical data
sequenceDiagram
participant User
participant Page as WalletRiskPage
participant WS as useWebSocket
participant WSServer as WebSocket Server
participant RealTime as RiskScore Calculator
Page->>WS: Call useWebSocket(wallet, onScoreUpdate)
activate WS
WS->>WSServer: Connect to /ws
WSServer-->>WS: Connection established
WS-->>Page: {connected: true}
deactivate WS
RealTime->>RealTime: New score calculated
RealTime->>WSServer: Emit RiskScoreUpdated event
WSServer->>WS: Push JSON message
activate WS
WS->>WS: Parse & validate message type
WS->>Page: Invoke onScoreUpdate callback
deactivate WS
Page->>Page: appendHistoryPoint(newScore)
Page->>Page: Re-render ScoreHistoryChart with updated history
User-->>Page: Chart animates new data point
sequenceDiagram
participant User
participant Page as WalletRiskPage
participant Hook as useRiskScore
participant API as Backend API
User->>Page: Click "Simulate 5,000 USDC" button
Page->>Hook: Call simulationStart()
activate Hook
Hook->>API: POST /api/risk/{wallet}/simulate<br/>{loanAmount: 5000}
API->>API: Calculate projected score & delta
API-->>Hook: SimulationResult{currentScore, projectedScore, scoreDelta}
Hook-->>Page: {simulationResult, simulationLoading: false}
deactivate Hook
Page->>Page: Compute simulationPoint {date+30d, projectedScore}
Page->>Page: Render ScoreGauge with projectedScore arc
Page->>Page: Render ScoreHistoryChart with dashed projection line
User-->>Page: Simulation visualization displayed
User->>Page: Click "Deactivate Simulation" button
Page->>Hook: Call simulationStop()
Hook-->>Page: {simulationResult: null, simulationLoading: false}
Page->>Page: Remove projected arc & projection line
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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 |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/package.json`:
- Line 26: Remove the unused devDependency "fast-check" from frontend
package.json: open package.json, locate the "fast-check" entry in
devDependencies and delete that line, then run dependency cleanup (e.g.,
npm/yarn install or prune) to update lockfile and node_modules; if you later add
property-based tests, re-add "fast-check" at that time.
In `@frontend/src/app/risk/`[wallet]/page.tsx:
- Around line 53-58: The websocket onScoreUpdate currently only calls
appendHistoryPoint and doesn't update the primary score state used by ScoreGauge
and ScoreBreakdown, so make the handler also update that score state/reducer
with the incoming ScoreUpdateEvent fields (event.overall_score, event.grade,
event.components). Inside the useWebSocket call's onScoreUpdate callback (the
useCallback shown), after appendHistoryPoint(...) dispatch or call the existing
score state updater (e.g., setScore, updateScore, or the score reducer action
used elsewhere) to set the new overall score, grade, and components; ensure the
updater is added to the useCallback dependency array. Keep appendHistoryPoint so
history still updates.
In `@frontend/src/components/risk/LoanTimeline.tsx`:
- Around line 19-27: The component's ApiLoan shape doesn't match the API's
camelCase response, causing mapApiLoan to read undefined values; update the
ApiLoan interface in LoanTimeline.tsx to use the actual response keys (amount,
interestRate, createdAt, updatedAt, dueDate) and adjust any uses in mapApiLoan
to those camelCase properties, or alternatively implement a serializer in the
server loan controller that maps the API's camelCase fields to the snake_case
names expected by ApiLoan before returning data so mapApiLoan receives the
expected shape.
In `@frontend/src/components/risk/Tooltip.tsx`:
- Around line 61-80: The tooltip trigger div should reference the tooltip panel
via aria-describedby and also close when the trigger loses focus; generate or
use a stable unique id (e.g., tooltipId from useId or a prop) and set that id on
the tooltip div (the element rendered when visible) and add
aria-describedby={tooltipId} to the trigger div, and add an onBlur handler on
the trigger (calling scheduleHide) so keyboard users closing/tabbing away will
hide the tooltip; ensure the tooltip still has role="tooltip" and keep existing
mouse enter/leave handlers (clearHideTimer/scheduleHide/visible) intact so
behavior is unchanged.
In `@frontend/src/components/risk/tooltipConfig.ts`:
- Around line 1-6: TOOLTIP_CONFIG currently uses keys like on_chain_activity and
document_verification that don't match the risk engine's emitted keys, so update
the TOOLTIP_CONFIG object to use the engine's actual component keys (e.g.
transactionHistory, repaymentRecord, collateralCoverage, disputeHistory) and
replace or remove document_verification; keep the human-readable descriptions
mapped to these exact keys so ScoreBreakdown.tsx can find them (update the
description text as needed to match the original messages for on-chain activity,
repayment history, collateral quality and dispute history).
In `@frontend/src/hooks/useRiskScore.ts`:
- Around line 111-115: In useRiskScore's early-return branch inside the
useEffect that checks `!walletAddress`, reset the loading flags so they can't
remain true when an in-flight request is cancelled: call `setLoading(false)` and
`setHistoryLoading(false)` along with `setData(null)` and `setError(null)`; do
the same for the other similar early-return at lines ~147-151 (the other effect
in useRiskScore) so `loading`/`historyLoading` are always cleared when the
wallet is cleared.
- Around line 106-108: The simulation state (simulationResult, simulationError,
simulationLoading) must be reset when the active wallet changes and guarded so
late POST responses don't overwrite state for a new wallet: in the hook
(useRiskScore) clear setSimulationResult(null), setSimulationError(null), and
setSimulationLoading(false) whenever the wallet identifier changes (or at start
of a new simulate call), and scope each async request by attaching a per-request
token (e.g., incrementing requestId or an AbortController) captured inside the
async simulate function — before calling setSimulationResult/setSimulationError
check that the token still matches the latest request (or that the controller
wasn't aborted). Apply the same pattern to the other simulation-related logic
referenced around the 188-224 block so stale responses are ignored and state is
wallet-scoped.
In `@frontend/src/hooks/useWebSocket.ts`:
- Around line 68-90: The WebSocket connect() currently accepts every
RiskScoreUpdated frame from the shared '/ws' stream, leaking other wallets'
updates into the active view; update connect()/ws lifecycle to subscribe for the
active walletAddress on open and/or filter incoming messages by walletAddress:
when ws.onopen fires send a subscription message including walletAddress (e.g.
ws.send(JSON.stringify({ type: 'subscribe', walletAddress }))) and ensure
ws.onmessage only processes events where data.type === 'RiskScoreUpdated' AND
data.walletAddress === walletAddress before calling
onScoreUpdateRef.current((data as ScoreUpdateEvent)), and also ensure you
unsubscribe or resubscribe when walletAddress changes or on stop.
In `@frontend/src/utils/riskScoreParsers.ts`:
- Around line 67-80: The map over Object.entries(components) currently assumes
each componentRaw is an object with .score and .weight, so
component.score/weight fall back to 0 when the engine returns flat numeric
values; update the mapping in the function that builds breakdown so that if
componentRaw is a number (typeof componentRaw === 'number') you call
safeNumber(componentRaw, `components.${key}`) to set value and set weight to a
sensible default (e.g., 0 or another chosen default) instead of reading
component.score/weight; otherwise keep the existing object handling (use
safeNumber(component.score, `components.${key}.score`) and
safeNumber(component.weight, `components.${key}.weight`)) so breakdown, label,
componentKey, value and weight are populated correctly.
🪄 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: 136ff8eb-9f14-4453-b0c5-02d99ad9cfa5
📒 Files selected for processing (11)
frontend/package.jsonfrontend/src/app/risk/[wallet]/page.tsxfrontend/src/components/risk/LoanTimeline.tsxfrontend/src/components/risk/ScoreBreakdown.tsxfrontend/src/components/risk/ScoreGauge.tsxfrontend/src/components/risk/ScoreHistoryChart.tsxfrontend/src/components/risk/Tooltip.tsxfrontend/src/components/risk/tooltipConfig.tsfrontend/src/hooks/useRiskScore.tsfrontend/src/hooks/useWebSocket.tsfrontend/src/utils/riskScoreParsers.ts
| }, | ||
| "devDependencies": { | ||
| "@tailwindcss/postcss": "^4", | ||
| "fast-check": "^3.23.2", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Search for imports and usage of fast-check in the codebase
# Search for fast-check imports in test files and source files
echo "=== Searching for fast-check imports ==="
rg -n --type=ts --type=tsx --type=js --type=jsx -C3 "from ['\"]fast-check['\"]|require\(['\"]fast-check['\"]"
# Search for fc.* usage patterns (common fast-check API usage)
echo -e "\n=== Searching for fast-check API usage (fc.*) ==="
rg -n --type=ts --type=tsx --type=js --type=jsx -C3 '\bfc\.(property|assert|integer|string|array|record)'
# List test files that might use it
echo -e "\n=== Listing test files ==="
fd -e test.ts -e test.tsx -e spec.ts -e spec.tsx -e test.js -e test.jsxRepository: anonfedora/stellovault
Length of output: 434
🏁 Script executed:
#!/bin/bash
# Description: Check fast-check version validity and security advisories
# Check npm for the specific version
echo "=== Checking if fast-check@3.23.2 exists on npm ==="
curl -s https://registry.npmjs.org/fast-check/3.23.2 | jq -r '.version // "Version not found"'
# Check for latest version
echo -e "\n=== Latest version of fast-check ==="
curl -s https://registry.npmjs.org/fast-check/latest | jq -r '.version'
# Check for security advisories
echo -e "\n=== Security advisories for fast-check ==="
gh api graphql -f query='
{
securityVulnerabilities(first: 5, ecosystem: NPM, package: "fast-check") {
nodes {
advisory {
summary
severity
publishedAt
}
vulnerableVersionRange
firstPatchedVersion {
identifier
}
}
}
}'Repository: anonfedora/stellovault
Length of output: 373
Remove unused fast-check dependency.
The fast-check library was added to devDependencies but is not imported or used anywhere in the codebase. Codebase search returned no results for imports or API usage patterns despite 4 existing test files. This is a dead dependency that adds unnecessary overhead to the project and should be removed.
If property-based testing will be used in future test files, consider adding this dependency only when tests are written.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/package.json` at line 26, Remove the unused devDependency
"fast-check" from frontend package.json: open package.json, locate the
"fast-check" entry in devDependencies and delete that line, then run dependency
cleanup (e.g., npm/yarn install or prune) to update lockfile and node_modules;
if you later add property-based tests, re-add "fast-check" at that time.
| const { connectionFailed } = useWebSocket({ | ||
| walletAddress, | ||
| onScoreUpdate: useCallback((event) => { | ||
| appendHistoryPoint({ date: event.calculated_at, score: event.overall_score }); | ||
| }, [appendHistoryPoint]), | ||
| }); |
There was a problem hiding this comment.
Push websocket payloads into the primary score state too.
appendHistoryPoint only updates the history reducer, even though each ScoreUpdateEvent already carries the new overall_score, grade, and components. That means the trend line refreshes, but ScoreGauge and ScoreBreakdown keep rendering stale data, so the page is not actually real-time beyond the chart.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/app/risk/`[wallet]/page.tsx around lines 53 - 58, The websocket
onScoreUpdate currently only calls appendHistoryPoint and doesn't update the
primary score state used by ScoreGauge and ScoreBreakdown, so make the handler
also update that score state/reducer with the incoming ScoreUpdateEvent fields
(event.overall_score, event.grade, event.components). Inside the useWebSocket
call's onScoreUpdate callback (the useCallback shown), after
appendHistoryPoint(...) dispatch or call the existing score state updater (e.g.,
setScore, updateScore, or the score reducer action used elsewhere) to set the
new overall score, grade, and components; ensure the updater is added to the
useCallback dependency array. Keep appendHistoryPoint so history still updates.
| interface ApiLoan { | ||
| id: string; | ||
| principal_amount: number; | ||
| interest_rate: number; | ||
| status: string; | ||
| created_at: string; | ||
| due_at: string; | ||
| updated_at: string; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf 'Inspecting loan API response shape and serializers...\n\n'
rg -n -C3 '/api/loans|listLoans\(|principal_amount|created_at|due_at|updated_at|amount|interestRate|createdAt|dueDate|updatedAt'Repository: anonfedora/stellovault
Length of output: 50380
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Finding API route handlers for /api/loans ==="
fd -e ts -e js | xargs rg -l 'api.*loans|/loans' | head -20
echo ""
echo "=== Searching for routes directory and loan endpoint ==="
fd -type d -name 'routes|api' | head -10Repository: anonfedora/stellovault
Length of output: 476
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Inspecting LoanTimeline.tsx component ==="
cat -n frontend/src/components/risk/LoanTimeline.tsx | head -100Repository: anonfedora/stellovault
Length of output: 3683
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Searching for /api/loans route definition ==="
rg -n '/api/loans|loanRoutes|loanController' --type ts --type js -A 10 | head -50Repository: anonfedora/stellovault
Length of output: 3252
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Inspecting loan controller listLoans function ==="
rg -n 'export.*listLoans|function listLoans|const listLoans' server/src/controllers/loan.controller.ts -A 30 | head -60Repository: anonfedora/stellovault
Length of output: 1002
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Full loan controller file ==="
wc -l server/src/controllers/loan.controller.tsRepository: anonfedora/stellovault
Length of output: 145
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Looking for loan controller file ==="
fd -name 'loan.controller.ts'Repository: anonfedora/stellovault
Length of output: 354
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Checking loan.service.ts listLoans return value ==="
sed -n '187,210p' server/src/services/loan.service.tsRepository: anonfedora/stellovault
Length of output: 895
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Checking Prisma Loan model schema ==="
rg -n 'model Loan|schema.prisma' server/prisma/ -A 20 | head -50Repository: anonfedora/stellovault
Length of output: 1591
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Looking for the Loan model definition in schema ==="
fd -name 'schema.prisma' -o -name '*.prisma'Repository: anonfedora/stellovault
Length of output: 369
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Checking for response serialization middleware ==="
rg -n 'middleware|serialize|transform' server/src/app.ts -B 2 -A 2 | head -40Repository: anonfedora/stellovault
Length of output: 472
🏁 Script executed:
#!/bin/bash
set -eulo pipefail
echo "=== Full app.ts setup to see if there's response transformation ==="
head -100 server/src/app.tsRepository: anonfedora/stellovault
Length of output: 3945
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Checking if Prisma has any response transformation ==="
rg -n 'prisma.*select|serialization|toJSON' server/src/services/loan.service.ts -B 1 -A 1Repository: anonfedora/stellovault
Length of output: 124
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Double-check: what does Prisma actually return? ==="
rg -n 'model Loan|dueDate|dueAt' server/prisma/schema.prismaRepository: anonfedora/stellovault
Length of output: 167
The API response shape does not match the component's expected interface—fix the mismatch before calling mapApiLoan.
The /api/loans endpoint returns camelCase fields (amount, interestRate, createdAt, updatedAt, dueDate) from the Prisma schema, but ApiLoan expects snake_case (principal_amount, interest_rate, created_at, due_at, updated_at). This causes mapApiLoan to read undefined values, producing NaN and Invalid Date objects that break SVG rendering and timeline calculations.
Either update the component's ApiLoan interface to match the actual camelCase response shape, or add a response serializer in the loan controller to transform the data to snake_case before returning it.
Also applies to: 56-69, 95-98
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/risk/LoanTimeline.tsx` around lines 19 - 27, The
component's ApiLoan shape doesn't match the API's camelCase response, causing
mapApiLoan to read undefined values; update the ApiLoan interface in
LoanTimeline.tsx to use the actual response keys (amount, interestRate,
createdAt, updatedAt, dueDate) and adjust any uses in mapApiLoan to those
camelCase properties, or alternatively implement a serializer in the server loan
controller that maps the API's camelCase fields to the snake_case names expected
by ApiLoan before returning data so mapApiLoan receives the expected shape.
| <div | ||
| onMouseEnter={show} | ||
| onMouseLeave={scheduleHide} | ||
| onKeyDown={handleTriggerKeyDown} | ||
| tabIndex={0} | ||
| role="button" | ||
| aria-expanded={visible} | ||
| aria-haspopup="true" | ||
| className="inline-flex items-center cursor-pointer focus:outline-none" | ||
| > | ||
| {children} | ||
| </div> | ||
|
|
||
| {/* Tooltip body */} | ||
| {visible && ( | ||
| <div | ||
| role="tooltip" | ||
| onMouseEnter={clearHideTimer} | ||
| onMouseLeave={scheduleHide} | ||
| className="absolute top-full left-1/2 -translate-x-1/2 mt-2 z-50 w-64 rounded-lg bg-gray-900 text-white text-xs shadow-xl p-3 space-y-1.5" |
There was a problem hiding this comment.
Associate the tooltip with its trigger and close it on blur.
The panel is rendered with role="tooltip", but the trigger never points to it with aria-describedby, so assistive tech will not announce the help text on focus. Keyboard users can also tab away with the tooltip still open because the trigger has no blur handler.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/risk/Tooltip.tsx` around lines 61 - 80, The tooltip
trigger div should reference the tooltip panel via aria-describedby and also
close when the trigger loses focus; generate or use a stable unique id (e.g.,
tooltipId from useId or a prop) and set that id on the tooltip div (the element
rendered when visible) and add aria-describedby={tooltipId} to the trigger div,
and add an onBlur handler on the trigger (calling scheduleHide) so keyboard
users closing/tabbing away will hide the tooltip; ensure the tooltip still has
role="tooltip" and keep existing mouse enter/leave handlers
(clearHideTimer/scheduleHide/visible) intact so behavior is unchanged.
| export const TOOLTIP_CONFIG: Record<string, string> = { | ||
| on_chain_activity: "Measures the volume and diversity of your on-chain transactions over the past 90 days.", | ||
| repayment_history: "Tracks your record of repaying loans on time. Defaults and late payments reduce this score.", | ||
| collateral_quality: "Evaluates the value and diversity of collateral assets you have posted.", | ||
| document_verification: "Reflects the completeness and recency of your KYC document verification.", | ||
| }; |
There was a problem hiding this comment.
Use the same component keys the risk engine actually emits.
The UI now looks up tooltip copy by item.componentKey, but the current engine emits transactionHistory, repaymentRecord, collateralCoverage, and disputeHistory, not the keys configured here. As written, frontend/src/components/risk/ScoreBreakdown.tsx never finds a description, and document_verification advertises a metric we do not currently score.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/components/risk/tooltipConfig.ts` around lines 1 - 6,
TOOLTIP_CONFIG currently uses keys like on_chain_activity and
document_verification that don't match the risk engine's emitted keys, so update
the TOOLTIP_CONFIG object to use the engine's actual component keys (e.g.
transactionHistory, repaymentRecord, collateralCoverage, disputeHistory) and
replace or remove document_verification; keep the human-readable descriptions
mapped to these exact keys so ScoreBreakdown.tsx can find them (update the
description text as needed to match the original messages for on-chain activity,
repayment history, collateral quality and dispute history).
| const [simulationResult, setSimulationResult] = useState<SimulationResult | null>(null); | ||
| const [simulationLoading, setSimulationLoading] = useState(false); | ||
| const [simulationError, setSimulationError] = useState<string | null>(null); |
There was a problem hiding this comment.
Reset and scope simulation state per wallet/request.
simulationResult and simulationError survive wallet changes, and a late POST response can still overwrite state after the hook has already switched to a different wallet. That leaks stale simulation output into the wrong view.
Suggested shape
+ // Also add `useRef` to the React import at Line 1.
+ const simulationRequestIdRef = useRef(0);
+
+ useEffect(() => {
+ simulationRequestIdRef.current += 1;
+ setSimulationResult(null);
+ setSimulationError(null);
+ setSimulationLoading(false);
+ }, [walletAddress]);
+
const activateSimulation = useCallback(async () => {
if (!walletAddress || !data) return;
+ const requestId = ++simulationRequestIdRef.current;
setSimulationLoading(true);
setSimulationError(null);
try {
@@
- setSimulationResult({
- currentScore,
- projectedScore,
- scoreDelta: projectedScore - currentScore,
- scenarioDescription,
- });
+ if (simulationRequestIdRef.current === requestId) {
+ setSimulationResult({
+ currentScore,
+ projectedScore,
+ scoreDelta: projectedScore - currentScore,
+ scenarioDescription,
+ });
+ }
} catch (err: unknown) {
- setSimulationError((err as Error).message ?? 'Simulation failed');
- setSimulationResult(null);
+ if (simulationRequestIdRef.current === requestId) {
+ setSimulationError((err as Error).message ?? 'Simulation failed');
+ setSimulationResult(null);
+ }
} finally {
- setSimulationLoading(false);
+ if (simulationRequestIdRef.current === requestId) {
+ setSimulationLoading(false);
+ }
}
}, [walletAddress, data]);Also applies to: 188-224
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/hooks/useRiskScore.ts` around lines 106 - 108, The simulation
state (simulationResult, simulationError, simulationLoading) must be reset when
the active wallet changes and guarded so late POST responses don't overwrite
state for a new wallet: in the hook (useRiskScore) clear
setSimulationResult(null), setSimulationError(null), and
setSimulationLoading(false) whenever the wallet identifier changes (or at start
of a new simulate call), and scope each async request by attaching a per-request
token (e.g., incrementing requestId or an AbortController) captured inside the
async simulate function — before calling setSimulationResult/setSimulationError
check that the token still matches the latest request (or that the controller
wasn't aborted). Apply the same pattern to the other simulation-related logic
referenced around the 188-224 block so stale responses are ignored and state is
wallet-scoped.
| useEffect(() => { | ||
| if (!walletAddress) { | ||
| setData(null); | ||
| setError(null); | ||
| return; |
There was a problem hiding this comment.
Reset loading state on the !walletAddress fast path.
If the wallet is cleared while either request is still in flight, the previous effect's finally is skipped by cancelled, so these branches can leave loading / historyLoading stuck at true.
Suggested fix
useEffect(() => {
if (!walletAddress) {
setData(null);
setError(null);
+ setLoading(false);
return;
}
@@
useEffect(() => {
if (!walletAddress) {
dispatchHistory({ type: 'SET', entries: [] });
setHistoryError(null);
+ setHistoryLoading(false);
return;
}Also applies to: 147-151
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/hooks/useRiskScore.ts` around lines 111 - 115, In useRiskScore's
early-return branch inside the useEffect that checks `!walletAddress`, reset the
loading flags so they can't remain true when an in-flight request is cancelled:
call `setLoading(false)` and `setHistoryLoading(false)` along with
`setData(null)` and `setError(null)`; do the same for the other similar
early-return at lines ~147-151 (the other effect in useRiskScore) so
`loading`/`historyLoading` are always cleared when the wallet is cleared.
| function connect() { | ||
| if (stopped) return; | ||
|
|
||
| attempt += 1; | ||
| ws = new WebSocket('/ws'); | ||
|
|
||
| ws.onopen = () => { | ||
| if (stopped) { | ||
| ws?.close(); | ||
| return; | ||
| } | ||
| consecutiveFailures = 0; | ||
| setConnected(true); | ||
| setConnectionFailed(false); | ||
| }; | ||
|
|
||
| ws.onmessage = (event: MessageEvent) => { | ||
| if (stopped) return; | ||
| try { | ||
| const data = JSON.parse(event.data as string); | ||
| if (data && data.type === 'RiskScoreUpdated') { | ||
| onScoreUpdateRef.current(data as ScoreUpdateEvent); | ||
| } |
There was a problem hiding this comment.
Route WebSocket updates through the active wallet.
This socket never subscribes with walletAddress, and the handler currently accepts every RiskScoreUpdated frame it sees. On any shared /ws stream, that will append another wallet’s updates into the active view.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/hooks/useWebSocket.ts` around lines 68 - 90, The WebSocket
connect() currently accepts every RiskScoreUpdated frame from the shared '/ws'
stream, leaking other wallets' updates into the active view; update connect()/ws
lifecycle to subscribe for the active walletAddress on open and/or filter
incoming messages by walletAddress: when ws.onopen fires send a subscription
message including walletAddress (e.g. ws.send(JSON.stringify({ type:
'subscribe', walletAddress }))) and ensure ws.onmessage only processes events
where data.type === 'RiskScoreUpdated' AND data.walletAddress === walletAddress
before calling onScoreUpdateRef.current((data as ScoreUpdateEvent)), and also
ensure you unsubscribe or resubscribe when walletAddress changes or on stop.
| let breakdown: RiskScoreBreakdown[] = []; | ||
| if (raw.components !== null && typeof raw.components === 'object' && !Array.isArray(raw.components)) { | ||
| const components = raw.components as Record<string, unknown>; | ||
| breakdown = Object.entries(components).map(([key, componentRaw]) => { | ||
| const component = | ||
| componentRaw !== null && typeof componentRaw === 'object' && !Array.isArray(componentRaw) | ||
| ? (componentRaw as Record<string, unknown>) | ||
| : {}; | ||
| const value = safeNumber(component.score, `components.${key}.score`); | ||
| const weight = safeNumber(component.weight, `components.${key}.weight`); | ||
| // Convert snake_case key to a human-readable label | ||
| const label = key.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); | ||
| return { label, componentKey: key, value, weight }; | ||
| }); |
There was a problem hiding this comment.
Parse flat component scores instead of { score, weight } objects.
The current risk engine returns flat numeric component values, not nested objects, so component.score and component.weight both fall back to 0 for every entry. That makes the new breakdown bars, weights, and tooltip payloads empty even when overall_score is correct.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/utils/riskScoreParsers.ts` around lines 67 - 80, The map over
Object.entries(components) currently assumes each componentRaw is an object with
.score and .weight, so component.score/weight fall back to 0 when the engine
returns flat numeric values; update the mapping in the function that builds
breakdown so that if componentRaw is a number (typeof componentRaw === 'number')
you call safeNumber(componentRaw, `components.${key}`) to set value and set
weight to a sensible default (e.g., 0 or another chosen default) instead of
reading component.score/weight; otherwise keep the existing object handling (use
safeNumber(component.score, `components.${key}.score`) and
safeNumber(component.weight, `components.${key}.weight`)) so breakdown, label,
componentKey, value and weight are populated correctly.
Summary
Real-time Risk Score & Loan Visualization
Closes #126
Type of Change
Changes Made
Testing
cargo testpasses (contracts)npm testpasses (server)tsc --noEmitpasses (server)cargo fmt+cargo clippyclean (contracts)Contract Changes (if applicable)
Checklist
.env.example, etc.)type(scope): descriptionformat (e.g.feat(escrow): add release timeout)Summary by CodeRabbit
New Features
Chores