Polish dashboard: real retarget prediction, symmetric 5x2, Kamado theme

The diff-adjustment tile was showing raw epoch progress (blocks-so-far
/ 2016 * 100%), which is never what anyone wants to see there. Compute
the real predicted change server-side: fetch the timestamp of the
first block in the current retarget epoch once per epoch, then each
refresh derive (expected_interval / actual_interval - 1) * 100,
clamped to Bitcoin's consensus bounds of [-75%, +300%]. The tile now
renders the signed percent, green when positive, red when negative.

Tile layout is forced to an explicit 5x2 grid so the number of
columns doesn't depend on viewport width. Reordering puts the
difficulty tile at (row 1, col 5) with the next-adjustment tile
directly below it at (row 2, col 5); block height, block reward,
total work, and expected block fill the rest of the second row.
Mobile breakpoints fall back to 3 and then 2 columns.

Header keeps only a live/connecting/offline status dot — the chain
name and block height badges were redundant now that the height tile
exists and animates on its own.

Stat-sub text bumped from 0.85em to 0.95em for readability.

Background: a warm ember-glow radial at the top of the viewport
layered with two accent radials and a low-opacity haori-checker tile
pattern (inline SVG data URI). Cards get a translucent backdrop so
the texture peeks through between them.
This commit is contained in:
satoshi
2026-04-22 21:29:11 +03:00
parent e881bc930d
commit 64ca14435c
5 changed files with 156 additions and 70 deletions
+52 -22
View File
@@ -33,16 +33,24 @@
return (uptime / expected) * 100;
});
// Blocks remaining + ETA until the retarget. The predicted adjustment
// percent comes from the backend (computed from the epoch start time).
const retarget = $derived.by(() => {
if (!height) return { progress: 0, remaining: 2016, eta: "" };
if (!height) return { remaining: 2016, eta: "", progress: 0 };
const inEpoch = height % 2016;
const remaining = 2016 - inEpoch;
const progress = (inEpoch / 2016) * 100;
const etaSec = remaining * 600;
return { progress, remaining, eta: formatDuration(etaSec) };
const progress = (inEpoch / 2016) * 100;
return { remaining, eta: formatDuration(etaSec), progress };
});
// Total hashes done by the pool = cumulative_shares * 2^32.
const diffPct = $derived(data.next_difficulty_percent ?? 0);
const diffPctLabel = $derived(
diffPct === 0
? "—"
: (diffPct > 0 ? "+" : "") + diffPct.toFixed(2) + "%",
);
const totalHashes = $derived(data.cumulative_shares * 2 ** 32);
// Block-height flash on network-wide new block.
@@ -59,7 +67,7 @@
});
</script>
<section class="grid grid-5">
<section class="grid-5">
<!-- Row 1 -->
<div class="card">
<div class="stat-label">Hashrate</div>
@@ -85,26 +93,16 @@
<div class="card">
<div class="stat-label">Network</div>
<div class="stat-value">{formatHashrate(data.network_hashrate_hs)}</div>
<div class="stat-sub">
pool share {(poolShare * 1e9).toFixed(2)} ppb
</div>
<div class="stat-sub">pool share {(poolShare * 1e9).toFixed(2)} ppb</div>
</div>
<div class="card">
<div class="stat-label">Expected block</div>
<div class="stat-value">{formatDuration(expected)}</div>
<div class="stat-sub">
effort {effort.toFixed(1)}% · uptime {formatUptime(data.uptime_seconds)}
</div>
</div>
<!-- Row 2 -->
<div class="card">
<div class="stat-label">Difficulty</div>
<div class="stat-value">{formatDifficulty(data.chain?.difficulty ?? 0)}</div>
<div class="stat-sub">network target</div>
</div>
<!-- Row 2 -->
<div class="card height-card" class:new-block={heightFlash}>
<div class="stat-label">Block height</div>
<div class="stat-value">{height ? height.toLocaleString() : "—"}</div>
@@ -127,27 +125,52 @@
</div>
<div class="card">
<div class="stat-label">Diff adjustment</div>
<div class="stat-value">{retarget.remaining} blocks</div>
<div class="stat-label">Expected block</div>
<div class="stat-value">{formatDuration(expected)}</div>
<div class="stat-sub">
effort {effort.toFixed(1)}% · uptime {formatUptime(data.uptime_seconds)}
</div>
</div>
<div class="card">
<div class="stat-label">Next adjustment</div>
<div
class="stat-value adj"
class:up={diffPct > 0}
class:down={diffPct < 0}
>{diffPctLabel}</div>
<div class="stat-sub">
<div class="retarget-bar">
<div class="retarget-fill" style="width:{retarget.progress}%"></div>
</div>
<span>{retarget.progress.toFixed(1)}% · ~{retarget.eta}</span>
<span>{retarget.remaining} blocks · ~{retarget.eta}</span>
</div>
</div>
</section>
<style>
.grid-5 {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 1rem;
}
@media (max-width: 1100px) {
.grid-5 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
@media (max-width: 720px) {
.grid-5 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
.retarget-bar {
height: 4px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
margin-bottom: 0.35em;
margin: 0.4em 0 0.4em;
}
.retarget-fill {
height: 100%;
@@ -161,6 +184,13 @@
font-weight: 400;
margin-left: 0.15em;
}
.adj.up {
color: var(--good);
}
.adj.down {
color: var(--bad);
}
.height-card {
transition: box-shadow 0.3s, border-color 0.3s;
}