diff --git a/ui/src/lib/PoolOverview.svelte b/ui/src/lib/PoolOverview.svelte
index 82c9aa3..57d2a7c 100644
--- a/ui/src/lib/PoolOverview.svelte
+++ b/ui/src/lib/PoolOverview.svelte
@@ -56,6 +56,20 @@
const totalHashes = $derived(data.cumulative_shares * 2 ** 32);
+ // Luck: best_share / cumulative_work * 100%.
+ // 100% = exactly expected, >100% = lucky, <100% = unlucky.
+ const luckPct = $derived.by(() => {
+ const best = data.best_diff;
+ const work = data.cumulative_shares;
+ if (!best || !work || work <= 0) return 0;
+ return (best / work) * 100;
+ });
+ const luckLabel = $derived.by(() => {
+ if (!data.best_diff || !data.cumulative_shares) return "—";
+ if (luckPct >= 1000) return `${(luckPct / 100).toFixed(1)}x`;
+ return luckPct.toFixed(0) + "%";
+ });
+
// Block-height flash on network-wide new block.
let prevHeight = $state(0);
let heightFlash = $state(false);
@@ -84,7 +98,15 @@
Best share
{formatDifficulty(data.best_diff)}
-
all-time pool record
+
100}
+ class:unlucky={luckPct > 0 && luckPct < 100}
+ title="Luck = best share / total work. 100% means your best share matches the work done. Above 100% is lucky (found a harder share than expected), below is unlucky."
+ >
+ {luckLabel}
+ luck
+
@@ -194,6 +216,35 @@
color: var(--bad);
}
+ .luck-sub {
+ display: flex;
+ align-items: baseline;
+ gap: 0.4em;
+ margin-top: 0.45em;
+ cursor: help;
+ }
+ .luck-value {
+ font-size: 1.15rem;
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
+ letter-spacing: -0.01em;
+ }
+ .luck-label {
+ color: var(--fg-dim);
+ font-size: 0.85rem;
+ font-weight: 400;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ }
+ .luck-sub.lucky .luck-value {
+ color: var(--good);
+ text-shadow: 0 0 10px rgba(92, 224, 168, 0.35);
+ }
+ .luck-sub.unlucky .luck-value {
+ color: var(--bad);
+ text-shadow: 0 0 10px rgba(255, 107, 107, 0.25);
+ }
+
.height-card {
transition: box-shadow 0.3s, border-color 0.3s;
}