commit 88e44d9fdf00b6fcda64a0414e5f5c7f61bbe082
parent a7e332d19a52a64bc516d78ddc35a339d17592d6
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Fri, 4 Sep 2026 15:16:35 +0200
Refine chain and leaderboard interactions
Diffstat:
3 files changed, 77 insertions(+), 40 deletions(-)
diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs
@@ -183,7 +183,7 @@ pub(super) const INDEX_HTML: &str = concat!(
.metrics-head h2 { margin: 0; }
.metrics-range { flex: 0 0 auto; }
.metrics-range button { padding: 5px 9px; font-size: 12px; white-space: nowrap; }
- .metrics-subhead { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; margin-top: 8px; }
+ .metrics-subhead { width: min(100%, 960px); justify-self: center; display: flex; justify-content: space-between; gap: 12px; align-items: baseline; margin-top: 8px; }
.metrics-subhead h2 { margin: 0; }
.metrics-summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
.metrics-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 430px), 1fr)); gap: 12px; }
@@ -205,7 +205,8 @@ pub(super) const INDEX_HTML: &str = concat!(
.metric-chart-hover-point { position: absolute; width: 8px; height: 8px; border-radius: 50%; background: #d5f55f; pointer-events: none; transform: translate(-50%, -50%); box-shadow: 0 0 0 4px rgba(213, 245, 95, .18); }
.metric-chart-tooltip { position: absolute; z-index: 1; max-width: min(180px, 80%); border: 1px solid #566d25; border-radius: 6px; padding: 5px 7px; background: #202615; color: #e8edf0; font-size: 11px; font-weight: 850; font-variant-numeric: tabular-nums; line-height: 1.25; pointer-events: none; box-shadow: 0 8px 20px rgba(0, 0, 0, .28); white-space: nowrap; }
.metrics-empty { border: 1px dashed #3a4248; border-radius: 8px; padding: 14px; color: #8d989f; background: #111316; }
- .leaderboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); gap: 12px; }
+ .leaderboard-tabs { flex-wrap: wrap; }
+ .leaderboard-grid { width: min(100%, 960px); justify-self: center; display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
.leaderboard-card { min-width: 0; border: 1px solid #2a3035; border-radius: 8px; padding: 12px; background: #181b1f; }
.leaderboard-card h3 { margin: 0 0 10px; }
.leaderboard-list { display: grid; gap: 8px; }
@@ -386,6 +387,7 @@ pub(super) const INDEX_HTML: &str = concat!(
.block-rail-head { display: flex; justify-content: space-between; gap: 10px; align-items: center; margin-bottom: 10px; }
.block-rail { display: flex; gap: 8px; overflow-x: auto; padding: 1px 0 10px; scroll-snap-type: x proximity; }
.block-card { flex: 0 0 122px; min-height: 100px; display: grid; gap: 6px; border: 1px solid #2f363c; border-radius: 8px; padding: 9px; background: #111316; color: #e8edf0; text-align: left; scroll-snap-align: start; }
+ button.block-card { cursor: pointer; user-select: none; }
.block-card:hover { border-color: #d5f55f; color: #d5f55f; }
.block-card.selected { background: #202616; border-color: #d5f55f; box-shadow: inset 0 0 0 1px #d5f55f; }
.block-card.new-block { animation: block-arrive .45s ease both; }
@@ -510,8 +512,11 @@ pub(super) const INDEX_HTML: &str = concat!(
.settings-mode-row .segmented { width: 100%; }
.settings-mode-row .segmented button { flex: 1; }
.metrics-head { align-items: flex-start; flex-direction: column; }
+ .metrics-subhead { align-items: stretch; flex-direction: column; }
.metrics-range { width: 100%; }
.metrics-range button { flex: 1 1 0; }
+ .leaderboard-tabs { width: 100%; }
+ .leaderboard-tabs button { flex: 1 1 auto; }
.segmented.setup-mode-picker { grid-template-columns: 1fr; }
.metrics-grid { grid-template-columns: 1fr; }
form { width: 100%; align-items: stretch; }
@@ -1061,17 +1066,17 @@ pub(super) const INDEX_HTML: &str = concat!(
</div>
<div class="block-rail" x-ref="blockRail" @scroll.debounce.200ms="maybeLoadOlderBlocks($event)">
<template x-for="block in blocks" :key="block.hash">
- <article class="block-card" :class="{ selected: selectedBlock?.hash === block.hash, 'new-block': newBlockHashes.has(block.hash) }">
- <button class="block-card-select" @click="selectBlock(block)" type="button" title="Open block details">
+ <button class="block-card" :class="{ selected: selectedBlock?.hash === block.hash, 'new-block': newBlockHashes.has(block.hash) }" @click="selectBlock(block)" type="button" title="Open block details">
+ <div class="block-card-select">
<div class="block-height" x-text="block.height"></div>
<div class="block-meta">
<span x-text="burnCountLabel(block)"></span>
<span x-text="transferCountLabel(block)"></span>
<span x-text="mineCountLabel(block)"></span>
</div>
- </button>
+ </div>
<div class="block-miner" x-text="blockFinalizerLabel(block)"></div>
- </article>
+ </button>
</template>
<template x-if="loadingInitialBlocks && blocks.length === 0">
<div class="block-skeleton-group" aria-hidden="true">
@@ -1290,42 +1295,50 @@ pub(super) const INDEX_HTML: &str = concat!(
</div>
<div class="metrics-subhead">
<h2>Leaderboards</h2>
+ <div class="segmented leaderboard-tabs" role="tablist" aria-label="Leaderboard">
+ <button type="button" role="tab" :aria-selected="leaderboardTab === 'balances'" :class="{ active: leaderboardTab === 'balances' }" @click="leaderboardTab = 'balances'">Balance</button>
+ <button type="button" role="tab" :aria-selected="leaderboardTab === 'miners'" :class="{ active: leaderboardTab === 'miners' }" @click="leaderboardTab = 'miners'">Miners</button>
+ <button type="button" role="tab" :aria-selected="leaderboardTab === 'burners'" :class="{ active: leaderboardTab === 'burners' }" @click="leaderboardTab = 'burners'">Burners</button>
+ <button type="button" role="tab" :aria-selected="leaderboardTab === 'mineProofs'" :class="{ active: leaderboardTab === 'mineProofs' }" @click="leaderboardTab = 'mineProofs'">Mine Proof Bits</button>
+ </div>
</div>
<div class="leaderboard-grid">
- <template x-for="board in [{ key: 'balances', title: 'Top 10 Balance' }, { key: 'miners', title: 'Top 10 Miners' }, { key: 'burners', title: 'Top 10 Burners' }]" :key="board.key">
+ <template x-if="leaderboardTab !== 'mineProofs'">
<article class="leaderboard-card">
- <h3 x-text="board.title"></h3>
+ <h3 x-text="leaderboardTitle(leaderboardTab)"></h3>
<div class="leaderboard-list">
- <template x-for="(row, index) in leaderboardRows(board.key)" :key="`${board.key}-${row.address}`">
+ <template x-for="(row, index) in leaderboardRows(leaderboardTab)" :key="`${leaderboardTab}-${row.address}`">
<div class="leaderboard-row">
<div class="leaderboard-rank" :class="leaderboardRankClass(index)" x-text="leaderboardRankLabel(index)"></div>
<div class="leaderboard-main">
<code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="shortAddressLabel(row.address)" @click="openAddressContact(row.address)" @keydown.enter.prevent="openAddressContact(row.address)" @keydown.space.prevent="openAddressContact(row.address)" title="Add or edit contact"></code>
- <div class="muted" x-text="leaderboardCountLabel(board.key, row)"></div>
+ <div class="muted" x-text="leaderboardCountLabel(leaderboardTab, row)"></div>
</div>
<div class="leaderboard-amount" x-text="leaderboardAmountLabel(row)"></div>
</div>
</template>
- <div class="metrics-empty" x-show="leaderboardRows(board.key).length === 0">No entries</div>
+ <div class="metrics-empty" x-show="leaderboardRows(leaderboardTab).length === 0">No entries</div>
</div>
</article>
</template>
- <article class="leaderboard-card">
- <h3>Top 10 Mine Proof Bits</h3>
- <div class="leaderboard-list">
- <template x-for="(row, index) in topMineProofRows()" :key="`${row.proofHash}-${row.height}`">
- <div class="leaderboard-row">
- <div class="leaderboard-rank" :class="leaderboardRankClass(index)" x-text="leaderboardRankLabel(index)"></div>
- <div class="leaderboard-main">
- <code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="shortAddressLabel(row.address)" @click="openAddressContact(row.address)" @keydown.enter.prevent="openAddressContact(row.address)" @keydown.space.prevent="openAddressContact(row.address)" title="Add or edit contact"></code>
- <div class="muted">Block #<span x-text="row.height"></span> · target <span x-text="row.difficultyBits"></span> bits · <code x-text="short(row.proofHash)"></code></div>
+ <template x-if="leaderboardTab === 'mineProofs'">
+ <article class="leaderboard-card">
+ <h3>Top 10 Mine Proof Bits</h3>
+ <div class="leaderboard-list">
+ <template x-for="(row, index) in topMineProofRows()" :key="`${row.proofHash}-${row.height}`">
+ <div class="leaderboard-row">
+ <div class="leaderboard-rank" :class="leaderboardRankClass(index)" x-text="leaderboardRankLabel(index)"></div>
+ <div class="leaderboard-main">
+ <code class="tx-value hash wallet-address-link" role="button" tabindex="0" x-text="shortAddressLabel(row.address)" @click="openAddressContact(row.address)" @keydown.enter.prevent="openAddressContact(row.address)" @keydown.space.prevent="openAddressContact(row.address)" title="Add or edit contact"></code>
+ <div class="muted">Block #<span x-text="row.height"></span></div>
+ </div>
+ <div class="leaderboard-amount"><span x-text="row.proofBits"></span> bits</div>
</div>
- <div class="leaderboard-amount"><span x-text="row.proofBits"></span> bits</div>
- </div>
- </template>
- <div class="metrics-empty" x-show="topMineProofRows().length === 0">No entries</div>
- </div>
- </article>
+ </template>
+ <div class="metrics-empty" x-show="topMineProofRows().length === 0">No entries</div>
+ </div>
+ </article>
+ </template>
</div>
</div>
</section>
diff --git a/src/adapters/http/metrics.rs b/src/adapters/http/metrics.rs
@@ -176,8 +176,8 @@ pub(super) fn top_mine_proofs(blocks: &[Block], limit: usize) -> Vec<MineProofLe
right
.proof_bits
.cmp(&left.proof_bits)
- .then_with(|| left.proof_hash.cmp(&right.proof_hash))
.then_with(|| left.height.cmp(&right.height))
+ .then_with(|| left.proof_hash.cmp(&right.proof_hash))
});
proofs.truncate(limit);
proofs
@@ -441,8 +441,8 @@ mod tests {
proof_header: None,
signature: signature.to_string(),
};
- let block = Block {
- height: 7,
+ let block = |height, transactions| Block {
+ height,
prev_hash: "0".repeat(64),
timestamp_ms: 0,
miner: "finalizer".to_string(),
@@ -453,21 +453,36 @@ mod tests {
vdf_output: String::new(),
leader_proof: None,
burn_bundle_section: BurnBundleSection::default(),
- transactions: vec![
- mine("four-bits", "0fff"),
- mine("twelve-bits", "000f"),
- mine("eight-bits", "00ff"),
- ],
- hash: "block-hash".to_string(),
+ transactions,
+ hash: format!("block-{height}"),
};
- let proofs = top_mine_proofs(&[block], 2);
+ let proofs = top_mine_proofs(
+ &[
+ block(
+ 7,
+ vec![mine("four-bits", "0fff"), mine("late-twelve-bits", "000f")],
+ ),
+ block(
+ 3,
+ vec![
+ mine("early-twelve-bits", "000a"),
+ mine("eight-bits", "00ff"),
+ ],
+ ),
+ ],
+ 3,
+ );
- assert_eq!(proofs.len(), 2);
- assert_eq!(proofs[0].address, "twelve-bits");
+ assert_eq!(proofs.len(), 3);
+ assert_eq!(proofs[0].address, "early-twelve-bits");
assert_eq!(proofs[0].proof_bits, 12);
- assert_eq!(proofs[1].address, "eight-bits");
- assert_eq!(proofs[1].proof_bits, 8);
+ assert_eq!(proofs[0].height, 3);
+ assert_eq!(proofs[1].address, "late-twelve-bits");
+ assert_eq!(proofs[1].proof_bits, 12);
+ assert_eq!(proofs[1].height, 7);
+ assert_eq!(proofs[2].address, "eight-bits");
+ assert_eq!(proofs[2].proof_bits, 8);
}
#[test]
diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js
@@ -23,6 +23,7 @@ window.iunaApp = function iunaApp() {
loadingMetrics: false,
metricsRequestSeq: 0,
metricHover: null,
+ leaderboardTab: "balances",
metricsRange: (() => {
try {
const stored = localStorage.getItem("iunaMetricsRange");
@@ -2018,6 +2019,14 @@ window.iunaApp = function iunaApp() {
return Array.isArray(rows) ? rows : [];
},
+ leaderboardTitle(kind) {
+ return {
+ balances: "Top 10 Balance",
+ miners: "Top 10 Miners",
+ burners: "Top 10 Burners",
+ }[kind] || "Leaderboard";
+ },
+
leaderboardRankLabel(index) {
return ["Gold", "Silver", "Bronze"][index] || `#${index + 1}`;
},