Add RiskDashboardPanel for risk profiling and leverage index (#307)#333
Conversation
…KJ#307) Renders risk score, leverage index, and color-coded alerts derived from Blend lending positions, reusing calculateBlendHealth/getHealthTone for consistency with the existing Blend panel. Known gap: liquidation price is shown as N/A — BlendPositionResponse has no asset price/oracle data to compute it from. Flagging for follow-up.
📝 WalkthroughWalkthroughAdds a new frontend file implementing a Risk Management & Leverage Dashboard panel. Includes exported types and functions for computing risk profiles and alerts from Blend position data, and a ChangesRisk Dashboard Panel
Estimated code review effort: 2 (Simple) | ~15 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant RiskDashboardPanel
participant RiskDashboardClient
participant DOM
User->>RiskDashboardPanel: click refresh button
RiskDashboardPanel->>RiskDashboardPanel: validate wallet input
RiskDashboardPanel->>RiskDashboardClient: fetch wallet Blend position
RiskDashboardClient-->>RiskDashboardPanel: BlendPositionResponse
RiskDashboardPanel->>RiskDashboardPanel: calculateRiskProfile / buildRiskAlerts
RiskDashboardPanel->>DOM: update gauges, alerts, status, JSON output
Related issues: Suggested labels: frontend, phase-3, enhancement Suggested reviewers: none identified Poem: 🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
packages/frontend/src/panels/risk-dashboard.tsOops! Something went wrong! :( ESLint: 9.39.2 YAMLException: Cannot read config file: /.eslintrc.js.bak 1 | module.exports = { 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 |
|
Hello @KevinMB0220 Please check and review and get back to me |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (3)
packages/frontend/src/panels/risk-dashboard.ts (3)
166-172: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider basic wallet format validation before dispatching the request.
Only emptiness is checked; a lightweight Stellar public key check (starts with
G, 56 chars) client-side would give faster feedback than round-tripping togetPosition.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/frontend/src/panels/risk-dashboard.ts` around lines 166 - 172, The refresh flow in risk-dashboard should validate the wallet format before calling getPosition, not just check for emptiness. Update the private async refresh() method to add a lightweight client-side Stellar public key check on the rd-wallet input (must start with G and be 56 characters) and set an error status immediately when it fails, so invalid requests are blocked before dispatch.
43-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winNo unit tests for the pure risk-calculation helpers.
calculateRiskProfile,buildRiskAlerts, and the threshold arithmetic (e.g. the1/healthFactor * 60scoring formula, theleverageIndex > 0.7alert boundary) are good candidates for isolated tests given the non-trivial edge cases (zero collateral, infinite health factor).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/frontend/src/panels/risk-dashboard.ts` around lines 43 - 98, Add isolated unit tests for the pure risk helpers in risk-dashboard.ts, covering calculateRiskProfile and buildRiskAlerts with edge cases like zero collateral, infinite health factor, and threshold boundaries. Verify the scoring formula in calculateRiskProfile (the 1/healthFactor * 60 logic with clamping), the tone selection via getHealthTone, and the leverageIndex > 0.7 alert behavior. Use the helper symbols calculateRiskProfile, buildRiskAlerts, clamp, and formatHealthFactor to anchor the tests and keep them focused on behavior.
135-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueGauges use
role="img"for numeric values.
role="meter"witharia-valuenow/aria-valuemin/aria-valuemaxwould better convey these as measurable quantities to assistive tech, rather than a static image.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/frontend/src/panels/risk-dashboard.ts` around lines 135 - 146, The gauges in risk-dashboard are exposed as static images even though they represent changing numeric measurements. Update the markup in the risk gauge components (the rd-score-gauge and rd-leverage-gauge blocks) to use an appropriate meter semantics instead of role="img", and add aria-valuemin, aria-valuemax, and aria-valuenow so assistive tech can read the current values as measurable quantities.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/frontend/src/panels/risk-dashboard.ts`:
- Around line 166-188: The refresh flow in risk-dashboard’s refresh method can
race when called multiple times, so add an in-flight guard around the
client.getPosition call and UI updates. Use a flag or request token in refresh()
to prevent overlapping executions, disable rd-refresh while a request is
pending, and ignore or cancel stale responses so only the latest result can call
renderGauges, renderAlerts, and update rd-position. Make sure the
loading/error/success status handling and button re-enable logic are tied to the
same guard.
- Around line 43-60: The leverage calculation in calculateRiskProfile is masking
worst-case risk when collateral is zero. Update the leverageIndex logic so that
a zero-collateral position with debt does not return 0; instead, represent it as
an effectively infinite or maximal leverage value that will surface in the UI
and trigger the leverage alert path in buildRiskAlerts. Keep the existing
healthFactor/tone behavior unchanged, and make the new leverageIndex handling
consistent with the RiskProfile shape and any display formatting used by the
risk dashboard.
---
Nitpick comments:
In `@packages/frontend/src/panels/risk-dashboard.ts`:
- Around line 166-172: The refresh flow in risk-dashboard should validate the
wallet format before calling getPosition, not just check for emptiness. Update
the private async refresh() method to add a lightweight client-side Stellar
public key check on the rd-wallet input (must start with G and be 56 characters)
and set an error status immediately when it fails, so invalid requests are
blocked before dispatch.
- Around line 43-98: Add isolated unit tests for the pure risk helpers in
risk-dashboard.ts, covering calculateRiskProfile and buildRiskAlerts with edge
cases like zero collateral, infinite health factor, and threshold boundaries.
Verify the scoring formula in calculateRiskProfile (the 1/healthFactor * 60
logic with clamping), the tone selection via getHealthTone, and the
leverageIndex > 0.7 alert behavior. Use the helper symbols calculateRiskProfile,
buildRiskAlerts, clamp, and formatHealthFactor to anchor the tests and keep them
focused on behavior.
- Around line 135-146: The gauges in risk-dashboard are exposed as static images
even though they represent changing numeric measurements. Update the markup in
the risk gauge components (the rd-score-gauge and rd-leverage-gauge blocks) to
use an appropriate meter semantics instead of role="img", and add aria-valuemin,
aria-valuemax, and aria-valuenow so assistive tech can read the current values
as measurable quantities.
🪄 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: e3b967c7-78a1-440d-95ca-75b6b4d8dc63
📒 Files selected for processing (1)
packages/frontend/src/panels/risk-dashboard.ts
| export function calculateRiskProfile(position: BlendPositionResponse): RiskProfile { | ||
| const metrics: BlendHealthMetrics = calculateBlendHealth(position); | ||
| const leverageIndex = metrics.collateral > 0 ? metrics.debt / metrics.collateral : 0; | ||
| const tone = Number.isFinite(metrics.healthFactor) | ||
| ? getHealthTone(metrics.healthFactor) | ||
| : 'green'; | ||
|
|
||
| const riskScore = Number.isFinite(metrics.healthFactor) | ||
| ? clamp(Math.round((1 / Math.max(metrics.healthFactor, 0.01)) * 60), 0, 100) | ||
| : 0; | ||
|
|
||
| return { | ||
| riskScore, | ||
| leverageIndex, | ||
| tone, | ||
| healthFactor: metrics.healthFactor, | ||
| }; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Leverage index misreports risk when collateral is zero.
Line 45: leverageIndex = metrics.collateral > 0 ? metrics.debt / metrics.collateral : 0. If collateral is 0 but debt > 0, healthFactor becomes 0 (correctly triggers red tone/max risk score via calculateBlendHealth), yet leverageIndex reports 0 — the opposite of reality. The UI would show a red gauge alongside a "0.0%" leverage value, which is misleading, and the leverageIndex > 0.7 alert in buildRiskAlerts (Line 82) never fires in this worst-case scenario.
🐛 Proposed fix
- const leverageIndex = metrics.collateral > 0 ? metrics.debt / metrics.collateral : 0;
+ const leverageIndex = metrics.collateral > 0
+ ? metrics.debt / metrics.collateral
+ : metrics.debt > 0
+ ? 1
+ : 0;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function calculateRiskProfile(position: BlendPositionResponse): RiskProfile { | |
| const metrics: BlendHealthMetrics = calculateBlendHealth(position); | |
| const leverageIndex = metrics.collateral > 0 ? metrics.debt / metrics.collateral : 0; | |
| const tone = Number.isFinite(metrics.healthFactor) | |
| ? getHealthTone(metrics.healthFactor) | |
| : 'green'; | |
| const riskScore = Number.isFinite(metrics.healthFactor) | |
| ? clamp(Math.round((1 / Math.max(metrics.healthFactor, 0.01)) * 60), 0, 100) | |
| : 0; | |
| return { | |
| riskScore, | |
| leverageIndex, | |
| tone, | |
| healthFactor: metrics.healthFactor, | |
| }; | |
| } | |
| export function calculateRiskProfile(position: BlendPositionResponse): RiskProfile { | |
| const metrics: BlendHealthMetrics = calculateBlendHealth(position); | |
| const leverageIndex = metrics.collateral > 0 | |
| ? metrics.debt / metrics.collateral | |
| : metrics.debt > 0 | |
| ? 1 | |
| : 0; | |
| const tone = Number.isFinite(metrics.healthFactor) | |
| ? getHealthTone(metrics.healthFactor) | |
| : 'green'; | |
| const riskScore = Number.isFinite(metrics.healthFactor) | |
| ? clamp(Math.round((1 / Math.max(metrics.healthFactor, 0.01)) * 60), 0, 100) | |
| : 0; | |
| return { | |
| riskScore, | |
| leverageIndex, | |
| tone, | |
| healthFactor: metrics.healthFactor, | |
| }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/frontend/src/panels/risk-dashboard.ts` around lines 43 - 60, The
leverage calculation in calculateRiskProfile is masking worst-case risk when
collateral is zero. Update the leverageIndex logic so that a zero-collateral
position with debt does not return 0; instead, represent it as an effectively
infinite or maximal leverage value that will surface in the UI and trigger the
leverage alert path in buildRiskAlerts. Keep the existing healthFactor/tone
behavior unchanged, and make the new leverageIndex handling consistent with the
RiskProfile shape and any display formatting used by the risk dashboard.
| private async refresh(): Promise<void> { | ||
| const wallet = this.byId<HTMLInputElement>('rd-wallet').value.trim(); | ||
|
|
||
| if (!wallet) { | ||
| this.setStatus('Wallet public key is required.', 'error'); | ||
| return; | ||
| } | ||
|
|
||
| this.setStatus('Loading risk profile...', 'info'); | ||
|
|
||
| try { | ||
| const position = await this.client.getPosition(wallet); | ||
| const profile = calculateRiskProfile(position); | ||
| const alerts = buildRiskAlerts(profile); | ||
|
|
||
| this.renderGauges(profile); | ||
| this.renderAlerts(alerts); | ||
| this.byId<HTMLElement>('rd-position').textContent = JSON.stringify(position, null, 2); | ||
| this.setStatus('Risk profile refreshed.', 'success'); | ||
| } catch (err) { | ||
| this.setStatus(`Error: ${formatError(err)}`, 'error'); | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
No guard against overlapping refresh calls.
refresh() doesn't disable rd-refresh or track an in-flight request. If a user clicks it twice (or triggers it before the previous fetch resolves), responses can resolve out of order, and a slower/earlier response can overwrite the UI with stale data after a newer one already rendered.
🔧 Proposed fix
private async refresh(): Promise<void> {
const wallet = this.byId<HTMLInputElement>('rd-wallet').value.trim();
if (!wallet) {
this.setStatus('Wallet public key is required.', 'error');
return;
}
this.setStatus('Loading risk profile...', 'info');
+ const button = this.byId<HTMLButtonElement>('rd-refresh');
+ button.disabled = true;
try {
const position = await this.client.getPosition(wallet);
const profile = calculateRiskProfile(position);
const alerts = buildRiskAlerts(profile);
this.renderGauges(profile);
this.renderAlerts(alerts);
this.byId<HTMLElement>('rd-position').textContent = JSON.stringify(position, null, 2);
this.setStatus('Risk profile refreshed.', 'success');
} catch (err) {
this.setStatus(`Error: ${formatError(err)}`, 'error');
+ } finally {
+ button.disabled = false;
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private async refresh(): Promise<void> { | |
| const wallet = this.byId<HTMLInputElement>('rd-wallet').value.trim(); | |
| if (!wallet) { | |
| this.setStatus('Wallet public key is required.', 'error'); | |
| return; | |
| } | |
| this.setStatus('Loading risk profile...', 'info'); | |
| try { | |
| const position = await this.client.getPosition(wallet); | |
| const profile = calculateRiskProfile(position); | |
| const alerts = buildRiskAlerts(profile); | |
| this.renderGauges(profile); | |
| this.renderAlerts(alerts); | |
| this.byId<HTMLElement>('rd-position').textContent = JSON.stringify(position, null, 2); | |
| this.setStatus('Risk profile refreshed.', 'success'); | |
| } catch (err) { | |
| this.setStatus(`Error: ${formatError(err)}`, 'error'); | |
| } | |
| } | |
| private async refresh(): Promise<void> { | |
| const wallet = this.byId<HTMLInputElement>('rd-wallet').value.trim(); | |
| if (!wallet) { | |
| this.setStatus('Wallet public key is required.', 'error'); | |
| return; | |
| } | |
| this.setStatus('Loading risk profile...', 'info'); | |
| const button = this.byId<HTMLButtonElement>('rd-refresh'); | |
| button.disabled = true; | |
| try { | |
| const position = await this.client.getPosition(wallet); | |
| const profile = calculateRiskProfile(position); | |
| const alerts = buildRiskAlerts(profile); | |
| this.renderGauges(profile); | |
| this.renderAlerts(alerts); | |
| this.byId<HTMLElement>('rd-position').textContent = JSON.stringify(position, null, 2); | |
| this.setStatus('Risk profile refreshed.', 'success'); | |
| } catch (err) { | |
| this.setStatus(`Error: ${formatError(err)}`, 'error'); | |
| } finally { | |
| button.disabled = false; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/frontend/src/panels/risk-dashboard.ts` around lines 166 - 188, The
refresh flow in risk-dashboard’s refresh method can race when called multiple
times, so add an in-flight guard around the client.getPosition call and UI
updates. Use a flag or request token in refresh() to prevent overlapping
executions, disable rd-refresh while a request is pending, and ignore or cancel
stale responses so only the latest result can call renderGauges, renderAlerts,
and update rd-position. Make sure the loading/error/success status handling and
button re-enable logic are tied to the same guard.
What this does
Adds
packages/frontend/src/panels/risk-dashboard.ts, implementingRiskDashboardPanelper issue #307 (roadmap item #55).The panel renders:
It reuses
calculateBlendHealthandgetHealthTonefrom the existingBlendPanel(same health-factor thresholds: green > 1.5, yellow > 1.2,red <= 1.2) so the two panels never disagree on the same position data.
What's NOT here (and why)
Liquidation price is shown as
N/A.BlendPositionResponse(
services/blend.client.ts) has no asset price or liquidation-thresholdfield — only
collateralValue,debtValue, andhealthFactor. Computing areal liquidation price needs price-oracle data that isn't currently exposed
by the Blend position API. Rather than fabricate a number, the panel
surfaces this honestly with an explanatory label. Flagging as a follow-up —
happy to pick it up once oracle pricing is available on the position
response, or to help define what that API change would look like.
Pre-existing issues encountered (not introduced by this PR)
While verifying this change,
npm run type-checkfailed both on this branchand on a clean
main:@galaxy-kj/core-oracles: missing modules (IOracleSource.js,CoinGeckoSource.js,CoinMarketCapSource.js)@galaxy-kj/frontend:../core/wallet/src/index.ts:6— re-export needsexport typeunderisolatedModulesConfirmed via
git stashthat both fail identically onmainbefore thisbranch's changes are applied. Not fixed here to keep this PR scoped to #307;
flagging in case they're not already tracked.
Testing
npm run type-check(blocked by the pre-existingcore/walleterrorabove — reaches an unrelated file before ever compiling
risk-dashboard.ts)Closes #307
Summary by CodeRabbit