commit 66a9a7e72ff84bb29f81a28213479a0c0130c4d8
parent 677adbdc77de421f825418eec318652ad31dfbf0
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Fri, 4 Sep 2026 21:14:12 +0200
Refine development leaderboards UI
Diffstat:
2 files changed, 68 insertions(+), 47 deletions(-)
diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs
@@ -179,12 +179,9 @@ pub(super) const INDEX_HTML: &str = concat!(
.danger-copy { color: #d69a92; line-height: 1.45; }
.danger-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 12px; }
.metrics-shell { display: grid; gap: 12px; }
- .metrics-head { display: flex; justify-content: space-between; gap: 12px; align-items: center; }
- .metrics-head h2 { margin: 0; }
+ .metrics-head { display: flex; justify-content: flex-end; gap: 12px; align-items: center; }
.metrics-range { flex: 0 0 auto; }
.metrics-range button { padding: 5px 9px; font-size: 12px; white-space: nowrap; }
- .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; }
.metric-chart-card { display: grid; gap: 10px; min-width: 0; border: 1px solid #2a3035; border-radius: 8px; padding: 12px; background: #181b1f; }
@@ -205,12 +202,17 @@ 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-tabs { flex-wrap: wrap; }
- .leaderboard-grid { width: min(100%, 960px); justify-self: center; display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
+ .leaderboard-tabs { width: 100%; display: flex; gap: 12px; margin-bottom: 16px; border-bottom: 1px solid #2f363c; overflow-x: auto; scrollbar-width: none; }
+ .leaderboard-tabs::-webkit-scrollbar { display: none; }
+ .leaderboard-tabs button { flex: 1 0 auto; border: 0; border-bottom: 2px solid transparent; border-radius: 0; padding: 6px 2px 8px; background: transparent; color: #8d989f; font-size: 12px; white-space: nowrap; }
+ .leaderboard-tabs button:hover { border-bottom-color: #566d25; color: #d6dee2; }
+ .leaderboard-tabs button.active { border-bottom-color: #d5f55f; background: transparent; color: #d5f55f; }
+ .leaderboard-grid { width: min(100%, 400px); justify-self: center; display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; margin-top: 8px; }
.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; }
.leaderboard-row { display: grid; grid-template-columns: 54px minmax(0, 1fr) auto; gap: 10px; align-items: center; border: 1px solid #2f363c; border-radius: 8px; padding: 9px; background: #111316; }
+ .leaderboard-row-empty { opacity: .45; }
.leaderboard-rank { display: grid; place-items: center; min-height: 30px; border: 1px solid #3a4248; border-radius: 999px; color: #a8b2b8; font-size: 11px; font-weight: 900; }
.leaderboard-rank.medal-1 { border-color: #ffd070; background: #2d2513; color: #ffd070; }
.leaderboard-rank.medal-2 { border-color: #c7d0d5; background: #20252a; color: #e8edf0; }
@@ -489,7 +491,7 @@ pub(super) const INDEX_HTML: &str = concat!(
@media (max-width: 760px) {
body { padding-bottom: calc(68px + env(safe-area-inset-bottom)); }
.app-shell { display: block; min-height: calc(100vh - 68px - env(safe-area-inset-bottom)); min-height: calc(100dvh - 68px - env(safe-area-inset-bottom)); overflow: visible; }
- .sidebar { position: fixed; z-index: 20; inset: auto 0 0; width: 100%; height: auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(48px, 1fr)); gap: 4px; padding: 6px max(8px, env(safe-area-inset-right)) calc(6px + env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left)); border: 0; border-top: 1px solid #30363b; background: rgba(21, 23, 26, .96); box-shadow: 0 -12px 32px rgba(0, 0, 0, .28); backdrop-filter: blur(14px); }
+ .sidebar { position: fixed; z-index: 20; inset: auto 0 0; width: 100%; height: auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(34px, 1fr)); gap: 4px; padding: 6px max(8px, env(safe-area-inset-right)) calc(6px + env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left)); border: 0; border-top: 1px solid #30363b; background: rgba(21, 23, 26, .96); box-shadow: 0 -12px 32px rgba(0, 0, 0, .28); backdrop-filter: blur(14px); }
.version-panel { display: none; }
.brand-mark { width: 100%; min-height: 50px; padding: 5px 2px; }
.side-nav { display: contents; }
@@ -512,11 +514,8 @@ 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; }
@@ -600,6 +599,10 @@ pub(super) const INDEX_HTML: &str = concat!(
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 19V5"></path><path d="M4 19h16"></path><path d="M7 15l3-4 3 2 4-7"></path><path d="M7 17h10"></path></svg>
<span>Metrics</span>
</button>
+ <button class="nav-button" x-show="developmentMode()" :class="{ active: tab === 'leaderboards' }" @click="setTab('leaderboards')" type="button" title="Leaderboards" aria-label="Leaderboards">
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 4h8v4a4 4 0 0 1-8 0V4Z"></path><path d="M8 6H5v1a4 4 0 0 0 4 4"></path><path d="M16 6h3v1a4 4 0 0 1-4 4"></path><path d="M12 12v4"></path><path d="M8 20h8"></path><path d="M9 16h6v4H9z"></path></svg>
+ <span>Leaders</span>
+ </button>
</nav>
<button class="settings-button" :class="{ active: tab === 'settings' }" type="button" @click="setTab('settings')" title="Settings" aria-label="Settings">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9.7 3.2 9.2 5.5a7.2 7.2 0 0 0-1.4.8L5.6 5.6 3.2 9.8l1.7 1.6a7.8 7.8 0 0 0 0 1.6l-1.7 1.6 2.4 4.2 2.2-.7a7.2 7.2 0 0 0 1.4.8l.5 2.3h4.8l.5-2.3a7.2 7.2 0 0 0 1.4-.8l2.2.7 2.4-4.2-1.7-1.6a7.8 7.8 0 0 0 0-1.6L21 9.8l-2.4-4.2-2.2.7a7.2 7.2 0 0 0-1.4-.8l-.5-2.3H9.7Z"></path><circle cx="12" cy="12.2" r="3.1"></circle></svg>
@@ -1231,7 +1234,6 @@ pub(super) const INDEX_HTML: &str = concat!(
<section x-show="tab === 'metrics'">
<div class="metrics-shell">
<div class="metrics-head">
- <h2>Metrics</h2>
<div class="segmented metrics-range" role="group" aria-label="Metrics block range">
<button type="button" :class="{ active: metricsRange === 100 }" @click="setMetricsRange(100)">Last 100</button>
<button type="button" :class="{ active: metricsRange === 1000 }" @click="setMetricsRange(1000)">Last 1000</button>
@@ -1293,18 +1295,19 @@ pub(super) const INDEX_HTML: &str = concat!(
</article>
</template>
</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>
+ </section>
+ <section x-show="developmentMode() && tab === 'leaderboards'">
<div class="leaderboard-grid">
- <template x-if="leaderboardTab !== 'mineProofs'">
- <article class="leaderboard-card">
+ <article class="leaderboard-card">
+ <div class="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'">Proof bits</button>
+ </div>
+ <template x-if="leaderboardTab !== 'mineProofs'">
+ <div>
<h3 x-text="leaderboardTitle(leaderboardTab)"></h3>
<div class="leaderboard-list">
<template x-for="(row, index) in leaderboardRows(leaderboardTab)" :key="`${leaderboardTab}-${row.address}`">
@@ -1317,12 +1320,18 @@ pub(super) const INDEX_HTML: &str = concat!(
<div class="leaderboard-amount" x-text="leaderboardAmountLabel(row)"></div>
</div>
</template>
- <div class="metrics-empty" x-show="leaderboardRows(leaderboardTab).length === 0">No entries</div>
+ <template x-for="index in leaderboardPlaceholderRanks(leaderboardRows(leaderboardTab).length)" :key="`${leaderboardTab}-empty-${index}`">
+ <div class="leaderboard-row leaderboard-row-empty" aria-hidden="true">
+ <div class="leaderboard-rank" x-text="`#${index + 1}`"></div>
+ <div class="leaderboard-main"> </div>
+ <div class="leaderboard-amount"> </div>
+ </div>
+ </template>
</div>
- </article>
- </template>
- <template x-if="leaderboardTab === 'mineProofs'">
- <article class="leaderboard-card">
+ </div>
+ </template>
+ <template x-if="leaderboardTab === 'mineProofs'">
+ <div>
<h3>Top 10 Mine Proof Bits</h3>
<div class="leaderboard-list">
<template x-for="(row, index) in topMineProofRows()" :key="`${row.proofHash}-${row.height}`">
@@ -1335,12 +1344,18 @@ pub(super) const INDEX_HTML: &str = concat!(
<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>
+ <template x-for="index in leaderboardPlaceholderRanks(topMineProofRows().length)" :key="`mine-proofs-empty-${index}`">
+ <div class="leaderboard-row leaderboard-row-empty" aria-hidden="true">
+ <div class="leaderboard-rank" x-text="`#${index + 1}`"></div>
+ <div class="leaderboard-main"> </div>
+ <div class="leaderboard-amount"> </div>
+ </div>
+ </template>
</div>
- </article>
- </template>
+ </div>
+ </template>
+ </article>
</div>
- </div>
</section>
<section x-show="tab === 'settings'">
<div class="settings-grid">
@@ -1361,19 +1376,6 @@ pub(super) const INDEX_HTML: &str = concat!(
</label>
</div>
</div>
- <div class="panel">
- <div class="settings-mode-row">
- <div class="settings-mode-copy">
- <div class="settings-mode-title">Development mode</div>
- <div class="muted" x-text="developmentMode() ? 'Detailed P2P data and the metrics screen are available.' : 'P2P stays focused on a simple peer list.'"></div>
- </div>
- <label class="toggle-switch" :class="{ active: keepTrackOfMetrics }">
- <input type="checkbox" :checked="keepTrackOfMetrics" @change="setKeepTrackOfMetrics($event.target.checked)">
- <span class="toggle-track" aria-hidden="true"><span class="toggle-thumb"></span></span>
- <span class="toggle-text" x-text="developmentMode() ? 'On' : 'Off'"></span>
- </label>
- </div>
- </div>
<div class="panel" x-show="advancedMode()">
<div class="settings-mode-row">
<div class="settings-mode-copy">
@@ -1435,6 +1437,19 @@ pub(super) const INDEX_HTML: &str = concat!(
<div class="setup-actions"><button class="primary" type="submit">Change password</button></div>
</form>
</div>
+ <div class="panel">
+ <div class="settings-mode-row">
+ <div class="settings-mode-copy">
+ <div class="settings-mode-title">Development mode</div>
+ <div class="muted" x-text="developmentMode() ? 'Detailed P2P data, metrics, and leaderboards are available.' : 'P2P stays focused on a simple peer list.'"></div>
+ </div>
+ <label class="toggle-switch" :class="{ active: keepTrackOfMetrics }">
+ <input type="checkbox" :checked="keepTrackOfMetrics" @change="setKeepTrackOfMetrics($event.target.checked)">
+ <span class="toggle-track" aria-hidden="true"><span class="toggle-thumb"></span></span>
+ <span class="toggle-text" x-text="developmentMode() ? 'On' : 'Off'"></span>
+ </label>
+ </div>
+ </div>
<div class="panel danger-panel">
<h3>Danger Zone</h3>
<div class="settings-mode-row">
diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js
@@ -210,7 +210,7 @@ window.iunaApp = function iunaApp() {
? ["dashboard", "wallet", "mining", "p2p", "chain", "settings"]
: ["dashboard", "wallet", "p2p", "chain", "settings"];
if (this.developmentMode()) {
- tabs.splice(tabs.indexOf("chain") + 1, 0, "metrics");
+ tabs.splice(tabs.indexOf("chain") + 1, 0, "metrics", "leaderboards");
}
return tabs;
},
@@ -249,6 +249,7 @@ window.iunaApp = function iunaApp() {
p2p: "P2P",
chain: "Chain",
metrics: "Metrics",
+ leaderboards: "Leaderboards",
settings: "Settings",
}[this.tab] || "iuna";
},
@@ -697,7 +698,7 @@ window.iunaApp = function iunaApp() {
const tab = this.tab;
const shouldLoadBlocks = tab === "chain" || tab === "mining";
const shouldLoadP2pMetrics = tab === "p2p" && this.developmentMode();
- const shouldLoadMetrics = tab === "metrics";
+ const shouldLoadMetrics = tab === "metrics" || tab === "leaderboards";
if (shouldLoadMetrics) {
await this.refreshMetrics(options);
this.refreshShellState({ addressBookVersion, silent: true });
@@ -1631,7 +1632,7 @@ window.iunaApp = function iunaApp() {
enabled ? "Development mode is preparing" : "Development mode turned off"
);
await this.refreshConfig();
- if (!enabled && this.tab === "metrics") {
+ if (!enabled && (this.tab === "metrics" || this.tab === "leaderboards")) {
this.setTab("settings");
}
} catch (error) {
@@ -2019,6 +2020,11 @@ window.iunaApp = function iunaApp() {
return Array.isArray(rows) ? rows : [];
},
+ leaderboardPlaceholderRanks(rowCount) {
+ const count = Math.min(10, Math.max(0, Number(rowCount) || 0));
+ return Array.from({ length: 10 - count }, (_, index) => count + index);
+ },
+
leaderboardTitle(kind) {
return {
balances: "Top 10 Balance",