Files
KamadoPool/ui/src/lib/BlocksTable.svelte
T
satoshi 1157f3501a Add best share analysis page, miner column, and UI improvements
- Best share page: hex + binary hash comparison against network target,
  per-bit coloring showing exactly which bits prevented a valid block,
  toggle between network diff at time of finding vs current diff
- Capture best share hash from ckpool logs with one-time backfill
- Persist network difficulty at time of best share for historical accuracy
- Add miner (worker) column to blocks table via coinbase address matching
- Truncate block hashes in table with full hash on hover
- Increase hashrate chart Y-axis to 7 ticks for better readability
2026-05-18 17:19:35 +03:00

198 lines
5.5 KiB
Svelte

<script lang="ts">
import { snap } from "../stores/snapshot.svelte";
import { formatAgo, formatDifficulty, explorerBaseFor } from "../format";
const chainDisplayName: Record<string, string> = {
main: "mainnet",
};
function displayChain(chain: string): string {
return chainDisplayName[chain] ?? chain;
}
const blocks = $derived.by(() => {
const b = snap.data?.recent_blocks ?? [];
return [...b].reverse();
});
const explorerBase = $derived(
explorerBaseFor(snap.data?.chain?.chain, snap.data?.mempool_base_url),
);
const currentChain = $derived(snap.data?.chain?.chain ?? "");
function truncHash(hash: string): string {
if (!hash || hash.length <= 16) return hash;
return hash.slice(0, 8) + "…" + hash.slice(-8);
}
function minerLabel(miner: string | undefined): string {
if (!miner) return "—";
// Show address.worker truncated
const dot = miner.indexOf(".");
if (dot < 0) {
// Just an address, truncate middle
if (miner.length > 20) return miner.slice(0, 8) + "…" + miner.slice(-6);
return miner;
}
const addr = miner.slice(0, dot);
const worker = miner.slice(dot + 1);
const shortAddr = addr.length > 12 ? addr.slice(0, 6) + "…" + addr.slice(-4) : addr;
return shortAddr + "." + worker;
}
</script>
<section class="card">
<h2>Blocks found</h2>
{#if blocks.length === 0}
<div class="empty">No blocks found yet.</div>
{:else}
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Height</th>
<th>Miner</th>
<th>Chain</th>
<th>Hash</th>
<th class="num">Reward</th>
<th class="num">Winning share</th>
<th class="num">When</th>
</tr>
</thead>
<tbody>
{#each blocks as b (b.height + "-" + b.found_at)}
<tr class:orphaned={!!b.orphaned_at} class:other-chain={!!b.chain && !!currentChain && b.chain !== currentChain}>
<td class="mono">
{b.height}
{#if b.orphaned_at}
<span class="orphan-tag" title="Reorged out of the canonical chain at {b.orphaned_at}">orphaned</span>
{/if}
</td>
<td class="miner-cell" title={b.miner ?? ""}>{minerLabel(b.miner)}</td>
<td>
{#if b.chain && currentChain && b.chain !== currentChain}
<span class="chain-tag" title="Mined on {displayChain(b.chain)} - current node is on {displayChain(currentChain)}">{displayChain(b.chain)}</span>
{:else if b.chain}
<span class="chain-current">{displayChain(b.chain)}</span>
{:else}
<span class="chain-unknown"></span>
{/if}
</td>
<td class="hash-cell">
{#if b.hash}
<a
class="hash mono"
href="{explorerBase}/block/{b.hash}"
target="_blank"
rel="noopener noreferrer"
title={b.hash}
>{truncHash(b.hash)}</a>
{:else}
<span class="hash mono">&mdash;</span>
{/if}
</td>
<td class="num">
{#if b.reward_btc}
<span class="reward-num">{b.reward_btc.toFixed(4)}</span>
<span class="unit">BTC</span>
{:else}
<span class="reward-num">&mdash;</span>
{/if}
</td>
<td class="num">{b.share_diff ? formatDifficulty(b.share_diff) : "—"}</td>
<td class="num">{formatAgo(new Date(b.found_at).getTime() / 1000)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</section>
<style>
h2 {
margin: 0 0 1rem;
font-size: 1.05rem;
font-weight: 600;
}
.empty {
color: var(--fg-dim);
padding: 0.5rem 0;
}
.table-wrap {
overflow-x: auto;
}
.miner-cell {
font-size: 0.85em;
color: var(--fg-dim);
white-space: nowrap;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
}
.hash-cell {
white-space: nowrap;
}
.hash {
color: var(--fg-dim);
font-size: 0.85em;
line-height: 1.4;
}
a.hash {
color: var(--fg-dim);
text-decoration: none;
border-bottom: 1px dashed transparent;
}
a.hash:hover {
color: var(--accent);
border-bottom-color: var(--accent);
}
.reward-num {
font-variant-numeric: tabular-nums;
}
tr.orphaned td {
color: var(--fg-dim);
text-decoration: line-through;
text-decoration-color: rgba(220, 80, 80, 0.55);
}
tr.orphaned .orphan-tag {
display: inline-block;
margin-left: 0.4em;
padding: 0.05em 0.4em;
border-radius: 3px;
background: rgba(220, 80, 80, 0.15);
color: rgb(220, 110, 110);
font-size: 0.7em;
font-weight: 600;
text-decoration: none;
vertical-align: middle;
}
.chain-tag {
display: inline-block;
padding: 0.05em 0.4em;
border-radius: 3px;
background: rgba(80, 160, 220, 0.15);
color: rgb(100, 170, 220);
font-size: 0.8em;
font-weight: 600;
text-decoration: none;
}
.chain-current {
color: var(--fg-dim);
font-size: 0.8em;
}
.chain-unknown {
color: var(--fg-dim);
font-size: 0.8em;
}
tr.other-chain td {
opacity: 0.6;
}
.unit {
color: var(--fg-dim);
font-size: 0.75em;
font-weight: 400;
margin-left: 0.25em;
}
</style>