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 =>