Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"fast-check": "^3.23.2",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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.jsx

Repository: 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.

"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
Expand Down
175 changes: 124 additions & 51 deletions frontend/src/app/risk/[wallet]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
'use client';

import React, { useState, use } from 'react';
import { use, useCallback, useMemo, useState } from 'react';
import { useRiskScore } from '../../../hooks/useRiskScore';
import { useWebSocket } from '../../../hooks/useWebSocket';
import ScoreGauge from '../../../components/risk/ScoreGauge';
import ScoreBreakdown from '../../../components/risk/ScoreBreakdown';
import ScoreHistoryChart from '../../../components/risk/ScoreHistoryChart';
import LoanTimeline from '../../../components/risk/LoanTimeline';
import {
ArrowLeft,
TrendingUp,
Expand All @@ -15,24 +17,59 @@ import {
Info
} from 'lucide-react';
import Link from 'next/link';
import { useSearchParams } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';

type Range = '6m' | '1y' | 'all';

const VALID_RANGES: Range[] = ['6m', '1y', 'all'];

function parseRange(value: string | null): Range {
if (value && VALID_RANGES.includes(value as Range)) return value as Range;
return '6m';
}

export default function WalletRiskPage({ params }: { params: Promise<{ wallet: string }> }) {
const { wallet: walletAddress } = use(params);
const searchParams = useSearchParams();
const startDate = searchParams.get('start_date');
const endDate = searchParams.get('end_date');
const router = useRouter();

const [range, setRange] = useState<Range>(() => parseRange(searchParams.get('range')));

const { data, loading, error, simulateScore } = useRiskScore(walletAddress);
const [loanSim, setLoanSim] = useState<string>('0');
const [projectedScore, setProjectedScore] = useState<number | null>(null);
const {
data,
history,
historyLoading,
historyError,
loading,
error,
simulationResult,
simulationLoading,
simulationError,
activateSimulation,
deactivateSimulation,
appendHistoryPoint,
} = useRiskScore(walletAddress, range);

const handleSimulate = () => {
const amount = parseFloat(loanSim);
if (!isNaN(amount)) {
setProjectedScore(simulateScore(amount));
}
};
const { connectionFailed } = useWebSocket({
walletAddress,
onScoreUpdate: useCallback((event) => {
appendHistoryPoint({ date: event.calculated_at, score: event.overall_score });
}, [appendHistoryPoint]),
});
Comment on lines +53 to +58
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.


function handleRangeChange(newRange: Range) {
setRange(newRange);
router.replace(`?range=${newRange}`);
}
const projectedScore = simulationResult?.projectedScore ?? null;

// Compute a stable projected date (30 days from when simulation was activated)
const simulationDate = useMemo(() => {
if (!simulationResult) return null;
const d = new Date();
d.setDate(d.getDate() + 30);
return d.toISOString().split('T')[0];
}, [simulationResult]);

if (loading) {
return (
Expand Down Expand Up @@ -65,16 +102,16 @@ export default function WalletRiskPage({ params }: { params: Promise<{ wallet: s
);
}

// Optional: filter history based on dates if provided
const filteredHistory = data.history.filter(h => {
if (startDate && h.date < startDate) return false;
if (endDate && h.date > endDate) return false;
return true;
});

return (
<div className="min-h-screen bg-gray-50 p-4 md:p-8">
<div className="max-w-6xl mx-auto space-y-8">
{/* Stale-data banner */}
{connectionFailed && (
<div className="w-full bg-amber-50 border border-amber-200 text-amber-800 text-sm font-medium px-4 py-2 rounded-lg">
Live updates unavailable — data may be stale
</div>
)}

{/* Navigation & Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="space-y-1">
Expand All @@ -84,7 +121,7 @@ export default function WalletRiskPage({ params }: { params: Promise<{ wallet: s
>
<ArrowLeft className="w-4 h-4" /> Back to Assessment
</Link>
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3">
<h1 className="text-3xl font-bold text-gray-900 flex items-center gap-3 flex-wrap">
Risk Report
<span className={`text-[10px] px-2 py-0.5 rounded-full border ${data.grade === 'A' ? 'bg-green-100 text-green-700 border-green-200' :
data.grade === 'B' ? 'bg-teal-100 text-teal-700 border-teal-200' :
Expand All @@ -93,6 +130,11 @@ export default function WalletRiskPage({ params }: { params: Promise<{ wallet: s
}`}>
MODERN ASSESSMENT
</span>
{simulationResult && (
<span className="text-[10px] px-2 py-0.5 rounded-full border bg-purple-100 text-purple-700 border-purple-200">
SIMULATION
</span>
)}
</h1>
<p className="font-mono text-sm text-gray-600 bg-white border border-gray-100 px-3 py-1 rounded-lg inline-block break-all">
{walletAddress}
Expand All @@ -112,7 +154,7 @@ export default function WalletRiskPage({ params }: { params: Promise<{ wallet: s
{/* Main Score & Breakdown */}
<div className="lg:col-span-2 space-y-8">
<div className="bg-white rounded-2xl p-8 border border-gray-100 shadow-sm flex flex-col md:flex-row items-center gap-12">
<ScoreGauge score={data.score} grade={data.grade} size={240} />
<ScoreGauge score={data.score} grade={data.grade} size={240} projectedScore={projectedScore} />
<div className="flex-1 space-y-4 w-full">
<div className="p-4 bg-blue-50/50 rounded-xl border border-blue-100">
<div className="flex items-center gap-2 mb-2 text-blue-900 font-semibold">
Expand Down Expand Up @@ -145,12 +187,37 @@ export default function WalletRiskPage({ params }: { params: Promise<{ wallet: s
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-bold text-gray-900">Historical Trend</h3>
<div className="flex gap-2">
<span className="px-2 py-1 bg-gray-100 rounded text-[10px] font-bold text-gray-400">6M</span>
<span className="px-2 py-1 rounded text-[10px] font-bold text-gray-400 hover:bg-gray-100 cursor-pointer">1Y</span>
<span className="px-2 py-1 rounded text-[10px] font-bold text-gray-400 hover:bg-gray-100 cursor-pointer">ALL</span>
{(['6m', '1y', 'all'] as Range[]).map((r) => (
<button
key={r}
onClick={() => handleRangeChange(r)}
className={`px-2 py-1 rounded text-[10px] font-bold transition-colors ${
range === r
? 'bg-blue-900 text-white'
: 'text-gray-400 hover:bg-gray-100'
}`}
>
{r.toUpperCase()}
</button>
))}
</div>
</div>
<ScoreHistoryChart history={filteredHistory} />
{historyError && (
<p className="text-sm text-red-600 mb-4">{historyError}</p>
)}
<ScoreHistoryChart
history={history}
loading={historyLoading}
simulationPoint={simulationResult && simulationDate ? {
date: simulationDate,
score: simulationResult.projectedScore,
} : null}
/>
</div>

<div className="bg-white rounded-2xl p-8 border border-gray-100 shadow-sm">
<h3 className="text-lg font-bold text-gray-900 mb-6">Loan Timeline</h3>
<LoanTimeline walletAddress={walletAddress} />
</div>
</div>

Expand All @@ -166,41 +233,47 @@ export default function WalletRiskPage({ params }: { params: Promise<{ wallet: s
</div>

<div className="space-y-4">
<label className="block text-sm font-medium text-gray-700">
Hypothetical Loan Amount (USDC)
</label>
<div className="relative">
<input
type="number"
value={loanSim}
onChange={(e) => setLoanSim(e.target.value)}
className="w-full pl-4 pr-12 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:ring-2 focus:ring-purple-600 focus:outline-none text-gray-900"
placeholder="0"
/>
<span className="absolute right-4 top-3.5 text-xs font-bold text-gray-400">USDC</span>
</div>
<button
onClick={handleSimulate}
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-4 rounded-xl transition-all shadow-lg shadow-purple-600/10"
>
Project Score Delta
</button>
<p className="text-sm text-gray-600">
Simulate the impact of a <span className="font-semibold">5,000 USDC</span> loan on your risk score.
</p>
{simulationResult ? (
<button
onClick={deactivateSimulation}
className="w-full bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-4 rounded-xl transition-all shadow-lg shadow-purple-600/10 flex items-center justify-center gap-2"
>
<span className="w-2 h-2 rounded-full bg-white inline-block" />
Stop Simulation
</button>
) : (
<button
onClick={activateSimulation}
disabled={simulationLoading}
className="w-full bg-purple-600 hover:bg-purple-700 disabled:opacity-60 text-white font-bold py-3 px-4 rounded-xl transition-all shadow-lg shadow-purple-600/10"
>
{simulationLoading ? 'Simulating…' : 'Simulate 5,000 USDC Loan'}
</button>
)}
{simulationError && (
<p className="text-xs text-red-600">{simulationError}</p>
)}
</div>

{projectedScore !== null && (
{simulationResult && (
<div className="mt-6 p-4 bg-purple-50 rounded-xl border border-purple-100">
<div className="flex justify-between items-end mb-1">
<span className="text-xs text-purple-600 font-semibold uppercase tracking-wider">Projected Score</span>
<span className="text-2xl font-black text-purple-600">{projectedScore.toFixed(0)}</span>
<span className="text-2xl font-black text-purple-600">{simulationResult.projectedScore.toFixed(0)}</span>
</div>
<div className="flex items-center gap-1 text-xs text-gray-500">
<span className="font-bold text-red-600">-{(data.score - projectedScore).toFixed(0)} points</span>
<span>estimated impact</span>
<div className="flex items-center gap-1 mt-1">
<span className={`text-lg font-black ${simulationResult.scoreDelta < 0 ? 'text-red-600' : 'text-green-600'}`}>
{simulationResult.scoreDelta > 0 ? '+' : '−'}{Math.abs(simulationResult.scoreDelta).toFixed(0)} pts
</span>
<span className="text-xs text-gray-500">estimated impact</span>
</div>
<div className="mt-3 w-full h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-purple-500"
style={{ width: `${(projectedScore / 1000) * 100}%` }}
style={{ width: `${(simulationResult.projectedScore / 1000) * 100}%` }}
/>
</div>
</div>
Expand Down
Loading
Loading