iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

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:
Msrc/adapters/http.rs | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mwww/assets/iuna-ui.js | 154++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
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`;