Skip to content

Add RiskDashboardPanel for risk profiling and leverage index (#307)#333

Merged
KevinMB0220 merged 1 commit into
Galaxy-KJ:mainfrom
EmannuellaO:feat/risk-dashboard-panel-307
Jul 3, 2026
Merged

Add RiskDashboardPanel for risk profiling and leverage index (#307)#333
KevinMB0220 merged 1 commit into
Galaxy-KJ:mainfrom
EmannuellaO:feat/risk-dashboard-panel-307

Conversation

@EmannuellaO

@EmannuellaO EmannuellaO commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What this does

Adds packages/frontend/src/panels/risk-dashboard.ts, implementing
RiskDashboardPanel per issue #307 (roadmap item #55).

The panel renders:

  • A risk score (0-100) gauge
  • A leverage index (debt / collateral) gauge
  • Color-coded alerts (green / yellow / red)

It reuses calculateBlendHealth and getHealthTone from the existing
BlendPanel (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-threshold
field — only collateralValue, debtValue, and healthFactor. Computing a
real 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-check failed both on this branch
and 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 needs
    export type under isolatedModules

Confirmed via git stash that both fail identically on main before this
branch'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-existing core/wallet error
    above — reaches an unrelated file before ever compiling
    risk-dashboard.ts)
  • Manual panel render / wallet lookup against a Blend position

Closes #307

Summary by CodeRabbit

  • New Features
    • Added a new risk management and leverage dashboard.
    • Shows key risk metrics, including a risk score, leverage level, and health factor.
    • Displays alert messages based on the current risk level.
    • Lets you refresh a wallet’s position and view the latest position details.

…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.
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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 RiskDashboardPanel class that renders UI, fetches wallet positions, and displays gauges, alerts, and status messages.

Changes

Risk Dashboard Panel

Layer / File(s) Summary
Risk profile and alert computation
packages/frontend/src/panels/risk-dashboard.ts
Defines RiskTone, RiskProfile, RiskAlert types and calculateRiskProfile/buildRiskAlerts functions computing leverage index, tone, health factor, risk score, and tone-specific alert messages, with clamp/formatHealthFactor helpers.
Dashboard panel UI and refresh flow
packages/frontend/src/panels/risk-dashboard.ts
Implements RiskDashboardPanel class rendering gauges/alerts/status HTML, wiring a refresh button to fetch wallet position data via RiskDashboardClient/BlendClient, updating the UI, and formatting errors/escaping HTML output.

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
Loading

Related issues: #307 (Frontend: Risk Management and Leverage Dashboard)

Suggested labels: frontend, phase-3, enhancement

Suggested reviewers: none identified

Poem:
A rabbit checks the leverage gauge,
Green, yellow, red — a color stage.
Health factors clamp, alerts appear,
Wallet risks now crystal clear.
Hop, refresh, the panel glows bright — 🐇✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the feature and testing, but omits several template sections like documentation updates, AI-friendly notes, screenshots, and final checklist. Reformat the PR body to the repo template and add the missing documentation, AI-friendly details, screenshots, breaking-change/deployment notes, and final checklist sections.
Linked Issues check ⚠️ Warning The panel covers risk scores, leverage, alerts, and responsive UI, but it does not implement the requested liquidation price display. Implement liquidation price support from available data, or explicitly update #307 to defer that requirement before merging.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the new RiskDashboardPanel and its risk/leverage focus.
Out of Scope Changes check ✅ Passed The changes stay within the requested frontend risk dashboard and supporting helpers, with no unrelated files or features added.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

packages/frontend/src/panels/risk-dashboard.ts

Oops! Something went wrong! :(

ESLint: 9.39.2

YAMLException: Cannot read config file: /.eslintrc.js.bak
Error: end of the stream or a document separator is expected (2:7)

1 | module.exports = {
2 | root: true,
-----------^
3 | env: {
4 | node: true,
at generateError (/node_modules/js-yaml/lib/loader.js:199:10)
at throwError (/node_modules/js-yaml/lib/loader.js:203:9)
at readDocument (/node_modules/js-yaml/lib/loader.js:1651:5)
at loadDocuments (/node_modules/js-yaml/lib/loader.js:1694:5)
at Object.load (/node_modules/js-yaml/lib/loader.js:1720:19)
at loadLegacyConfigFile (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2666:21)
at loadConfigFile (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2782:20)
at ConfigArrayFactory._loadConfigData (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3088:42)
at ConfigArrayFactory.loadFile (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:2952:40)
at createCLIConfigArray (/node_modules/@eslint/eslintrc/dist/eslintrc.cjs:3768:35)
(node:2) ESLintRCWarning: You are using an eslintrc configuration file, which is deprecated and support will be removed in v10.0.0. Please migrate to an eslint.config.js file. See https://eslint.org/docs/latest/use/configure/migration-guide for details. An eslintrc configuration file is used because you have the ESLINT_USE_FLAT_CONFIG environment variable set to false. If you want to use an eslint.config.js file, remove the environment variable. If you want to find the location of the eslintrc configuration file, use the --debug flag.
(Use node --trace-warnings ... to show where the warning was created)


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@EmannuellaO

Copy link
Copy Markdown
Contributor Author

Hello @KevinMB0220 Please check and review and get back to me

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
packages/frontend/src/panels/risk-dashboard.ts (3)

166-172: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider 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 to getPosition.

🤖 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 win

No unit tests for the pure risk-calculation helpers.

calculateRiskProfile, buildRiskAlerts, and the threshold arithmetic (e.g. the 1/healthFactor * 60 scoring formula, the leverageIndex > 0.7 alert 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 value

Gauges use role="img" for numeric values.

role="meter" with aria-valuenow/aria-valuemin/aria-valuemax would 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

📥 Commits

Reviewing files that changed from the base of the PR and between fdf2894 and 1496d82.

📒 Files selected for processing (1)
  • packages/frontend/src/panels/risk-dashboard.ts

Comment on lines +43 to +60
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,
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
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.

Comment on lines +166 to +188
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');
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 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.

Suggested change
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.

@KevinMB0220 KevinMB0220 merged commit 4662000 into Galaxy-KJ:main Jul 3, 2026
7 checks passed
@KevinMB0220 KevinMB0220 self-requested a review July 3, 2026 19:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE] Frontend: Risk Management and Leverage Dashboard (#55)

2 participants