commit ebb42a45515784cff6b5ce4683b57aabd00bff26
parent 9ada28f373b423f025fedd5738120a08ccea83ac
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Sun, 9 Aug 2026 23:25:15 +0200
Improve mining event log feedback
Diffstat:
2 files changed, 216 insertions(+), 3 deletions(-)
diff --git a/src/adapters/http.rs b/src/adapters/http.rs
@@ -3360,6 +3360,16 @@ const INDEX_HTML: &str = r#"<!doctype html>
.mine-include-status.ready { color: #d5f55f; }
.mine-include-status.muted { color: #879198; }
.mine-save-row { display: flex; justify-content: flex-start; }
+ .mining-event-log { display: grid; gap: 8px; max-height: 360px; overflow: auto; margin-top: 12px; border: 1px solid #2f363c; border-radius: 8px; padding: 8px; background: #0f1114; }
+ .mining-event-log-head { display: flex; justify-content: space-between; gap: 10px; align-items: baseline; color: #879198; font-size: 10px; font-weight: 850; text-transform: uppercase; }
+ .mining-event { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 10px; align-items: start; border: 1px solid #30383d; border-radius: 8px; padding: 10px; background: #111316; }
+ .mining-event-dot { width: 8px; height: 8px; margin-top: 5px; border-radius: 999px; background: #7f888e; }
+ .mining-event.active .mining-event-dot { background: #d5f55f; box-shadow: 0 0 12px rgba(213, 245, 95, .45); }
+ .mining-event.warning .mining-event-dot { background: #ffd070; }
+ .mining-event.info .mining-event-dot { background: #8de9cd; }
+ .mining-event-title { color: #eef6f8; font-weight: 850; overflow-wrap: anywhere; }
+ .mining-event-detail { margin-top: 3px; color: #9fa8ad; font-size: 12px; line-height: 1.35; overflow-wrap: anywhere; }
+ .mining-event-time { color: #7f888e; font-size: 11px; font-weight: 800; white-space: nowrap; }
.panel-separator { border-top: 1px solid #2f363c; margin: 14px 0 12px; }
.stratum-config { display: grid; gap: 10px; }
.stratum-note { max-width: 760px; color: #9eb3bc; font-size: 12px; line-height: 1.45; }
@@ -3566,7 +3576,7 @@ const INDEX_HTML: &str = r#"<!doctype html>
.block-card { flex-basis: 108px; }
}
</style>
- <script defer src="/assets/iuna-ui.js?v=86"></script>
+ <script defer src="/assets/iuna-ui.js?v=97"></script>
<script defer src="/assets/alpine.min.js"></script>
</head>
<body x-data="iunaApp()" x-init="init()" @keydown.window.escape="closeModals()" x-cloak>
@@ -3782,6 +3792,39 @@ const INDEX_HTML: &str = r#"<!doctype html>
<div class="mine-stat-value" x-text="localMiningMempoolLabel()"></div>
</div>
</div>
+ <div class="mining-event-log" aria-label="Mining event log">
+ <div class="mining-event-log-head"><span>Event log</span><span x-text="`${miningEventLog().length} lines`"></span></div>
+ <template x-if="miningEventLog().length === 0">
+ <div class="mining-event skeleton-card" aria-hidden="true">
+ <span class="mining-event-dot"></span>
+ <div>
+ <div class="skeleton-line medium"></div>
+ <div class="skeleton-line long"></div>
+ </div>
+ <div class="skeleton-line short"></div>
+ </div>
+ </template>
+ <template x-if="miningEventLog().length === 0">
+ <div class="mining-event skeleton-card" aria-hidden="true">
+ <span class="mining-event-dot"></span>
+ <div>
+ <div class="skeleton-line short"></div>
+ <div class="skeleton-line medium"></div>
+ </div>
+ <div class="skeleton-line short"></div>
+ </div>
+ </template>
+ <template x-for="event in miningEventLog()" :key="event.key">
+ <div class="mining-event" :class="event.kind">
+ <span class="mining-event-dot" aria-hidden="true"></span>
+ <div>
+ <div class="mining-event-title" x-text="event.title"></div>
+ <div class="mining-event-detail" x-text="event.detail"></div>
+ </div>
+ <div class="mining-event-time" x-text="event.time"></div>
+ </div>
+ </template>
+ </div>
</div>
<div class="panel">
<div class="panel-head mining-head">
@@ -6047,7 +6090,7 @@ mod tests {
#[test]
fn metrics_screen_includes_block_range_filter() {
- assert!(super::INDEX_HTML.contains("iuna-ui.js?v=86"));
+ assert!(super::INDEX_HTML.contains("iuna-ui.js?v=97"));
assert!(super::INDEX_HTML.contains("aria-label=\"Metrics block range\""));
assert!(super::INDEX_HTML.contains("setMetricsRange(100)"));
assert!(super::INDEX_HTML.contains("setMetricsRange(1000)"));
@@ -6224,6 +6267,24 @@ mod tests {
assert!(app_js.contains("powStatusShortLabel()"));
assert!(app_js.contains("setPowMiningWorkers(workers)"));
assert!(app_js.contains("localMiningMempoolLabel()"));
+ assert!(app_js.contains("miningEventLog()"));
+ assert!(app_js.contains("miningEventLimit: 1000"));
+ assert!(app_js.contains("slice(0, this.miningEventLimit)"));
+ assert!(super::INDEX_HTML.contains("aria-label=\"Mining event log\""));
+ assert!(super::INDEX_HTML.contains("miningEventLog().length === 0"));
+ assert!(app_js.contains("Resource budget:"));
+ assert!(app_js.contains("isPowMineSuccessStatus"));
+ assert!(app_js.contains("You mined a PoW action"));
+ assert!(app_js.contains("Waiting for a finalizer to include it in a block."));
+ assert!(app_js.contains("Observed block"));
+ assert!(app_js.contains("Finalized by"));
+ assert!(app_js.contains("You finalized block"));
+ assert!(app_js.contains("if (!locallyFinalized)"));
+ assert!(app_js.contains("last?.title === title"));
+ assert!(app_js.contains("this.miningEventState.pob = enabled ? \"on\" : \"off\";"));
+ assert!(app_js.contains("Automatic burn prepared at height"));
+ assert!(app_js.contains("Eligible for the next block opportunity."));
+ assert!(app_js.contains("!Number.isFinite(timestampMs)"));
assert!(!super::INDEX_HTML.contains("Needs burns"));
}
diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js
@@ -79,6 +79,10 @@ window.iunaApp = function iunaApp() {
maxPowMiningWorkers: 32,
recoveryVdfTopRankPercent: 50,
burnAmountDirty: false,
+ miningEvents: [],
+ miningEventLimit: 1000,
+ miningEventState: {},
+ miningEventCounter: 0,
transferTo: "",
transferAmount: null,
transferFee: "0.000001",
@@ -657,6 +661,7 @@ window.iunaApp = function iunaApp() {
this.burnFeeDraft = this.amountLabel(this.burnFee);
}
this.lastUpdated = new Date();
+ this.syncMiningEvents({ status, blocks });
this.scheduleFeeEstimates();
} catch (error) {
if (String(error.message || "").includes("401")) {
@@ -1250,6 +1255,7 @@ window.iunaApp = function iunaApp() {
? `Finalization burns on: ${this.amountLabel(amount)} IUNA per block with ${this.amountLabel(fee)} per byte`
: `Burn settings saved while off`
);
+ this.appendMiningEvent("Burn settings saved", `Configured ${this.amountLabel(amount)} IUNA per block with ${this.amountLabel(fee)} IUNA fee/byte.`, "info");
this.burnAmountDirty = false;
this.burnAmount = amount;
this.burnFee = fee;
@@ -1273,6 +1279,14 @@ window.iunaApp = function iunaApp() {
{ enabled, amount, fee_per_byte: fee },
enabled ? "Finalization burns turned on" : "Finalization burns turned off"
);
+ this.appendMiningEvent(
+ enabled ? "Finalization burns turned on" : "Finalization burns turned off",
+ enabled
+ ? `Burning ${this.amountLabel(amount)} IUNA per block with ${this.amountLabel(fee)} IUNA fee/byte.`
+ : "Automatic burn preparation paused.",
+ enabled ? "active" : "warning"
+ );
+ this.miningEventState.pob = enabled ? "on" : "off";
this.burnAmountDirty = false;
this.burnAmount = amount;
this.burnFee = fee;
@@ -1291,6 +1305,14 @@ window.iunaApp = function iunaApp() {
{ enabled, workers: this.powMiningWorkers },
enabled ? "PoW mining turned on" : "PoW mining turned off"
);
+ this.appendMiningEvent(
+ enabled ? "PoW mining turned on" : "PoW mining turned off",
+ enabled
+ ? `Resource budget: ${this.powMiningWorkers} worker${this.powMiningWorkers === 1 ? "" : "s"}.`
+ : "PoW worker search paused.",
+ enabled ? "active" : "warning"
+ );
+ this.miningEventState["pow-workers"] = String(this.powMiningWorkers);
} catch (error) {
this.powMiningEnabled = previous;
this.showFlash(error.message, "error");
@@ -1311,6 +1333,12 @@ window.iunaApp = function iunaApp() {
{ enabled: this.powMiningEnabled, workers: clamped },
`PoW workers set to ${clamped}`
);
+ this.appendMiningEvent(
+ "PoW worker budget changed",
+ `Resource budget: ${clamped} worker${clamped === 1 ? "" : "s"}.`,
+ "info"
+ );
+ this.miningEventState["pow-workers"] = String(clamped);
} catch (error) {
this.powMiningWorkers = previous;
this.showFlash(error.message, "error");
@@ -1447,6 +1475,130 @@ window.iunaApp = function iunaApp() {
return this.mempool.filter((tx) => tx?.kind === "mine").length;
},
+ appendMiningEvent(title, detail, kind = "info", timestamp = new Date()) {
+ const last = this.miningEvents[0];
+ if (last?.title === title && last?.detail === detail && last?.kind === kind) return;
+ this.miningEventCounter += 1;
+ const entry = {
+ key: `${timestamp.getTime()}-${this.miningEventCounter}`,
+ timestamp,
+ time: timestamp.toLocaleTimeString(),
+ kind,
+ title,
+ detail,
+ };
+ this.miningEvents = [entry, ...this.miningEvents].slice(0, this.miningEventLimit);
+ },
+
+ isPowMineSuccessStatus(status) {
+ return /queued mine action/i.test(status || "");
+ },
+
+ syncMiningEvents({ status, blocks }) {
+ const mining = status?.mining || {};
+ const chain = status?.chain || {};
+ if (!this.miningEventState.started) {
+ this.appendMiningEvent(
+ "Mining log started",
+ `Height ${chain.height ?? "-"}, PoB ${mining.automatic ? "on" : "off"}, PoW ${mining.pow_mining_enabled ? "on" : "off"}.`,
+ "info"
+ );
+ this.miningEventState.started = true;
+ }
+
+ this.noteMiningStateChange(
+ "pob",
+ mining.automatic ? "on" : "off",
+ mining.automatic ? "Finalization burns active" : "Finalization burns inactive",
+ mining.automatic
+ ? `Burning ${this.amountLabel(mining.burn_per_block || 0)} IUNA per block with ${this.amountLabel(mining.automatic_burn_fee || 0)} IUNA fee/byte.`
+ : "Automatic burn preparation is off.",
+ mining.automatic ? "active" : "warning"
+ );
+ this.noteMiningStateChange(
+ "pow-workers",
+ String(mining.pow_mining_workers ?? this.powMiningWorkers),
+ "PoW worker budget",
+ `Resource budget: ${mining.pow_mining_workers ?? this.powMiningWorkers} worker${(mining.pow_mining_workers ?? this.powMiningWorkers) === 1 ? "" : "s"}.`,
+ "info"
+ );
+ const powMineStatus = mining.last_auto_pow_mine_status || "";
+ if (this.isPowMineSuccessStatus(powMineStatus)) {
+ this.noteMiningStateChange(
+ "pow-mine-success",
+ powMineStatus,
+ "You mined a PoW action",
+ `${powMineStatus}. Waiting for a finalizer to include it in a block.`,
+ "active"
+ );
+ } else {
+ this.noteMiningStateChange(
+ "pow-status",
+ powMineStatus,
+ "PoW status",
+ powMineStatus || "Waiting for next automatic PoW mining tick.",
+ mining.pow_mining_enabled ? "active" : "info"
+ );
+ }
+ this.noteMiningStateChange(
+ "leader",
+ mining.current_leader || "",
+ mining.wallet_is_current_leader ? "This wallet is selected" : "Selected finalizer changed",
+ mining.current_leader
+ ? `Current finalizer: ${this.currentFinalizerLabel()} at height ${chain.height ?? "-"}.`
+ : `No current finalizer reported at height ${chain.height ?? "-"}.`,
+ mining.wallet_is_current_leader ? "active" : "info"
+ );
+ if (typeof mining.last_auto_burn_height === "number") {
+ this.noteMiningStateChange(
+ "last-burn-height",
+ String(mining.last_auto_burn_height),
+ `Automatic burn prepared at height ${mining.last_auto_burn_height}`,
+ "Eligible for the next block opportunity.",
+ "active"
+ );
+ }
+
+ const latestBlock = Array.isArray(blocks)
+ ? blocks.find((block) => Number(block?.height) > 0)
+ : this.blocks.find((block) => Number(block?.height) > 0);
+ if (latestBlock) {
+ const finalizer = this.addressLabel(latestBlock.miner);
+ const locallyFinalized = latestBlock.miner === status.wallet_address;
+ if (locallyFinalized) {
+ this.noteMiningStateChange(
+ "latest-local-block",
+ latestBlock.hash || String(latestBlock.height),
+ `You finalized block ${latestBlock.height}`,
+ `Success. ${this.burnCountLabel(latestBlock)} burned, fees IUNA ${this.amountLabel(latestBlock.total_fees ?? latestBlock.totalFees ?? 0)}.`,
+ "active",
+ new Date(Number(latestBlock.timestamp_ms ?? latestBlock.timestampMs) || Date.now())
+ );
+ }
+ if (!locallyFinalized) {
+ this.noteMiningStateChange(
+ "latest-block",
+ latestBlock.hash || String(latestBlock.height),
+ `Observed block ${latestBlock.height}`,
+ `Finalized by ${finalizer}. ${this.burnCountLabel(latestBlock)} burned, fees IUNA ${this.amountLabel(latestBlock.total_fees ?? latestBlock.totalFees ?? 0)}.`,
+ "active",
+ new Date(Number(latestBlock.timestamp_ms ?? latestBlock.timestampMs) || Date.now())
+ );
+ }
+ }
+ },
+
+ noteMiningStateChange(key, value, title, detail, kind = "info", timestamp = new Date()) {
+ if (this.miningEventState[key] === value) return;
+ this.miningEventState[key] = value;
+ if (value === "" && key !== "pow-status" && key !== "leader") return;
+ this.appendMiningEvent(title, detail, kind, timestamp);
+ },
+
+ miningEventLog() {
+ return this.miningEvents;
+ },
+
metricsCharts() {
return Array.isArray(this.blockchainMetrics?.charts) ? this.blockchainMetrics.charts : [];
},
@@ -2347,7 +2499,7 @@ window.iunaApp = function iunaApp() {
},
relativeTimeLabel(timestampMs) {
- if (typeof timestampMs !== "number") return "-";
+ if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return "-";
const ageSeconds = Math.max(0, Math.round((Date.now() - timestampMs) / 1000));
if (ageSeconds < 5) return "now";
if (ageSeconds < 60) return `${ageSeconds}s ago`;