Add block-update latency tracking, raw share counters, and shares bar UI
Log ZMQ→mining.notify latency in ckpool (patch 0005), expose a raw reject counter (patch 0006), and surface both in the dashboard: - Block latency card shows avg/last ms, wasted work, and block count - SharesBar component shows session + all-time accepted/rejected with a Demon Slayer flame-slash animation on new shares - Miners card info moved to MinersTable section header - Latency stats and share counts persist across restarts via kv store - Removed misleading pool-wide share stats from per-worker/user pages (ckpool doesn't expose per-user raw counts)
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import { snap } from "../stores/snapshot.svelte";
|
||||
|
||||
const data = $derived(snap.data!);
|
||||
|
||||
const sessionAcc = $derived(data.session_accepted ?? 0);
|
||||
const sessionRej = $derived(data.session_rejected ?? 0);
|
||||
const allAcc = $derived(data.alltime_accepted ?? 0);
|
||||
const allRej = $derived(data.alltime_rejected ?? 0);
|
||||
|
||||
const sessionTotal = $derived(sessionAcc + sessionRej);
|
||||
const sessionRejectPct = $derived(
|
||||
sessionTotal > 0 ? (sessionRej / sessionTotal) * 100 : 0,
|
||||
);
|
||||
const allTotal = $derived(allAcc + allRej);
|
||||
const allRejectPct = $derived(
|
||||
allTotal > 0 ? (allRej / allTotal) * 100 : 0,
|
||||
);
|
||||
|
||||
// Detect new accepted shares to trigger animation
|
||||
let prevAccepted = $state(0);
|
||||
let pulse = $state(false);
|
||||
$effect(() => {
|
||||
const cur = sessionAcc;
|
||||
const prev = untrack(() => prevAccepted);
|
||||
if (cur > 0 && prev > 0 && cur > prev) {
|
||||
pulse = true;
|
||||
setTimeout(() => { pulse = false; }, 700);
|
||||
}
|
||||
prevAccepted = cur;
|
||||
});
|
||||
|
||||
function fmtCount(n: number): string {
|
||||
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M";
|
||||
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K";
|
||||
return n.toLocaleString();
|
||||
}
|
||||
</script>
|
||||
|
||||
<section class="shares-bar" class:pulse>
|
||||
<div class="slash-effect" aria-hidden="true"></div>
|
||||
<div class="content">
|
||||
<h3 class="title">Shares</h3>
|
||||
<div class="stats">
|
||||
<div class="group">
|
||||
<span class="group-label">Session</span>
|
||||
<span class="accepted">{fmtCount(sessionAcc)}</span>
|
||||
<span class="sep">/</span>
|
||||
<span class="rejected">{fmtCount(sessionRej)}</span>
|
||||
{#if sessionTotal > 0}
|
||||
<span class="pct" class:warn={sessionRejectPct > 1}>
|
||||
{sessionRejectPct.toFixed(2)}% rejected
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="group">
|
||||
<span class="group-label">All-time</span>
|
||||
<span class="accepted">{fmtCount(allAcc)}</span>
|
||||
<span class="sep">/</span>
|
||||
<span class="rejected">{fmtCount(allRej)}</span>
|
||||
{#if allTotal > 0}
|
||||
<span class="pct" class:warn={allRejectPct > 1}>
|
||||
{allRejectPct.toFixed(2)}% rejected
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.shares-bar {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-card);
|
||||
padding: 0.85rem 1.25rem;
|
||||
}
|
||||
.content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--fg);
|
||||
}
|
||||
.stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.group {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.4em;
|
||||
}
|
||||
.group-label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--fg-dim);
|
||||
margin-right: 0.3em;
|
||||
}
|
||||
.accepted {
|
||||
color: var(--good);
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.rejected {
|
||||
color: var(--bad);
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.sep {
|
||||
color: var(--fg-dim);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.pct {
|
||||
font-size: 1.1rem;
|
||||
color: var(--fg-dim);
|
||||
margin-left: 0.3em;
|
||||
}
|
||||
.pct.warn {
|
||||
color: var(--bad);
|
||||
}
|
||||
.divider {
|
||||
width: 1px;
|
||||
height: 1.6rem;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
/* Demon Slayer flame-slash animation on new accepted share */
|
||||
.slash-effect {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 122, 58, 0.0) 30%,
|
||||
rgba(255, 122, 58, 0.15) 48%,
|
||||
rgba(255, 200, 50, 0.25) 50%,
|
||||
rgba(255, 122, 58, 0.15) 52%,
|
||||
rgba(255, 122, 58, 0.0) 70%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
.shares-bar.pulse .slash-effect {
|
||||
animation: flame-slash 0.7s ease-out;
|
||||
}
|
||||
@keyframes flame-slash {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateX(-100%) scaleY(1);
|
||||
}
|
||||
40% {
|
||||
opacity: 1;
|
||||
transform: translateX(0%) scaleY(1.2);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(100%) scaleY(1);
|
||||
}
|
||||
}
|
||||
.shares-bar.pulse {
|
||||
border-color: rgba(255, 122, 58, 0.5);
|
||||
box-shadow: 0 0 12px rgba(255, 122, 58, 0.2);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.content {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user