Follow-up to MR !70 (merged), which rebuilt the Loan Details screen to a design. A review of that MR, checked against live staging data and the backend formulas, found four defects. Fixing them turned up three more.
What a borrower sees differently
The notice box is readable in light mode. Both tones painted 12px text in their accent colour on a tint of that same accent: info measured 2.07:1 and warning 1.64:1 against the composited background. Light mode now uses darker variants at 4.72:1 and 4.83:1; dark mode already passed and is untouched. Hue is preserved (36° and 43°), so these are the same colours, not new ones. This reaches 17 LoanNoticeBox call sites across 9 screens, not just this one.
The MARGIN_CALL badge is readable too — it was #FFB800 at 1.65:1, two rows from the notice explaining the margin call.
Dead rows are gone. Liquidation price (SYM) rendered an em dash for every stablecoin collateral. The backend suppresses that figure deliberately (isUnitPriced), so ETH and BTC are the only symbols that can ever produce one — and BTC is unusable (decimals: null). An empty row reads as a system fault; the row now hides itself.
Per-symbol thresholds no longer invite a false comparison. Margin call LTV (SYM) and Liquidation LTV (SYM) rendered beneath Current LTV for every collateral. The server liquidates on Σ collateral × price × threshold while ltv divides by an all-markets collateralValue — the two only collapse into one comparable percentage when a single market supplies both sides. With ETH and USDC both supplied, a customer read 45% against 77% and concluded they had headroom the server did not agree they had. The rows are now gated on soleCollateral; Health factor carries the answer otherwise, being correct in every configuration.
The notice says only things the state supports. It previously ended "…falls past its liquidation LTV, shown below" while the rows it pointed at could be absent. The band below 1.2 is now split three ways:
| health factor | notice |
|---|---|
>= 1.2 |
"The loan is good. There is no due date — pay back at any time. Liquidation only occurs if your health factor falls to 1." |
1 to 1.2
|
"Your health factor is low. Repay some of this loan or supply more collateral to move it up." |
< 1 |
"This loan can be liquidated now. Supply more collateral to restore it." |
The split at 1 is load-bearing. Below 1 the account is already seizable, and where liquidationValue is 0 the promise "repay … to move it up" is arithmetically false — HF = 0 / remainingDebt = 0 however much is repaid. The third string deliberately makes no claim about repayment.
The middle band is not an edge case: on every market in staging, borrowing the full limit lands at HF 1.059 (stablecoins) or 1.143 (ETH/BTC).
Tests
222 → 231 in src/components/loans + src/app; full suite 90 suites / 792 tests green, type-check and lint:translations clean, lint 0 errors.
Every behavioural change is mutation-verified — reverted by hand, watched fail, restored. That caught two things reading could not: a copy guard that banned the word "below" rather than the claim, and a false-promise guard narrow enough to let "pay part of it back to lift it" through.
Several fixtures described accounts the backend cannot return — liquidationPrice: {} with live debt and healthFactor: 1.8, when an empty map forces HF to 0. They are rederived from the identity HF = avgLT / LTV, where avgLT is bounded by the supplied markets' liquidation thresholds. Under that identity, HF 1.8 at LTV 0.5 with both ETH and USDC supplied is impossible.
Review
Per-task reviews, a whole-branch review, and Codex gate 3 — APPROVED after 4 rounds.
Known, deliberately not in scope
-
healthFactorLowcuts at 1.2 while the server margin-calls at 1.023–1.039, so the screen paints red over positions the server considers fine. Pre-existing, also drivesltvComfortable, and the correct replacement is per-account (liquidationValue / warningValue) rather than a constant — its own spec. -
#FFB800remains at 1.64–1.73:1 in five other lending files; all clear AA with the same constant used here. - "Supply more collateral" is unfollowable if an operator disables collateral on every market. Weak rather than false; needs a
markets.some(collateralEnabled)check the screen does not do today. - Two fixtures at
borrow-detail-screen.test.tsxremain internally inconsistent. Pre-existing; deliberately reverted to their untouched state rather than half-fixed inside a focused branch. - The two golds are verified by arithmetic and unit test, never on a device. A light-mode pass should cover supply / review-loan / pay-now / loan-withdraw, not just this screen.