diff --git a/src/api/models/MinersDashboard.ts b/src/api/models/MinersDashboard.ts index 0b8bad5..78b5c39 100644 --- a/src/api/models/MinersDashboard.ts +++ b/src/api/models/MinersDashboard.ts @@ -51,6 +51,8 @@ export type ScoreFactors = { closedSwaps: number; credibilityRamp: number; credibilityRampTarget: number; + // Timed-out swaps in the credibility window — used to explain a hard-zeroed ramp. + credibilityTimedOut: number; successRate30d: number; successMultiplier: number; }; diff --git a/src/components/miners/ScoreFactorsStrip.tsx b/src/components/miners/ScoreFactorsStrip.tsx index f195c12..c3593d3 100644 --- a/src/components/miners/ScoreFactorsStrip.tsx +++ b/src/components/miners/ScoreFactorsStrip.tsx @@ -1,5 +1,12 @@ import React from 'react'; -import { Box, Stack, Typography, alpha, useTheme } from '@mui/material'; +import { + Box, + Stack, + Tooltip, + Typography, + alpha, + useTheme, +} from '@mui/material'; import type { ScoreFactors } from '../../api'; import { FONTS } from '../../theme'; import { formatTao } from '../../utils/format'; @@ -19,10 +26,17 @@ type Card = { description: string; delta?: Delta; weak?: boolean; + tooltip?: string; }; +// Mirrors CREDIBILITY_MAX_TIMEOUTS in allways/das-allways — used for display copy +// only; the zeroing itself is computed server-side. +const CREDIBILITY_MAX_TIMEOUTS = 2; + const buildCards = (sf: ScoreFactors): Card[] => { const credibilityRamped = sf.closedSwaps >= sf.credibilityRampTarget; + // Ramp forced to 0 while closed swaps exist = the timeout hard-floor tripped. + const zeroedByTimeouts = sf.closedSwaps > 0 && sf.credibilityRamp === 0; return [ { label: 'Crown share', @@ -72,14 +86,21 @@ const buildCards = (sf: ScoreFactors): Card[] => { { label: 'Credibility', window: 'last 30d', - headline: credibilityRamped - ? fmtMultiplier(1.0) - : fmtMultiplier(sf.credibilityRamp), + headline: zeroedByTimeouts + ? fmtMultiplier(0) + : credibilityRamped + ? fmtMultiplier(1.0) + : fmtMultiplier(sf.credibilityRamp), fill: sf.credibilityRamp, - description: credibilityRamped - ? `fully ramped · ${sf.closedSwaps} of ${sf.credibilityRampTarget} closed` - : `${sf.closedSwaps} / ${sf.credibilityRampTarget} closed · resets if you fall below`, - weak: !credibilityRamped, + description: zeroedByTimeouts + ? `auto-zeroed · ${sf.credibilityTimedOut} timeouts (limit ${CREDIBILITY_MAX_TIMEOUTS})` + : credibilityRamped + ? `fully ramped · ${sf.closedSwaps} of ${sf.credibilityRampTarget} closed` + : `${sf.closedSwaps} / ${sf.credibilityRampTarget} closed · resets if you fall below`, + tooltip: zeroedByTimeouts + ? `More than ${CREDIBILITY_MAX_TIMEOUTS} timed-out swaps in the 30-day window zero your credibility — and with it your whole reward. It recovers as old timeouts age out of the window.` + : undefined, + weak: zeroedByTimeouts || !credibilityRamped, }, ]; }; @@ -169,7 +190,7 @@ const FactorCard: React.FC<{ card: Card }> = ({ card }) => { : theme.palette.primary.main; const headlineColor = card.weak ? 'text.secondary' : 'text.primary'; - return ( + const body = ( = ({ card }) => { ); + + if (!card.tooltip) return body; + return ( + + {body} + + ); }; const composeMultiplier = (sf: ScoreFactors): number =>