diff --git a/CHANGELOG.md b/CHANGELOG.md index 36497a33..c4152a0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,30 @@ per the process in [`docs/dev/releasing.md`](docs/dev/releasing.md). ## [Unreleased] +## [1.13.0] - 2026-07-22 + +### Added + +- Mine cart train (#748): a pixel-art strip under the hero band retelling the chart window as + cargo — one cart per interval, token coins for what landed (orange ɱ per share interval, a + pair for a found block, a purple gem per confirmed Tari payout, a blue X per XvB raffle win), + with a tipple, a sleeping cat, and drifting clouds. Static under reduced motion. `/api/state` + gains a `payouts` key: confirmed payouts per chain as `{x, amount}` timeline points, gated on + the payout-confirmation flags. + +### Changed + +- Earnings calculator standardized (#748): every tab presents its estimate in one + Day / Month / Year table with a shared-precision coin column and a `≈` fiat column once the + price is known. Tari gains month/year spans, XvB gains the published current-tier reward + table, Energy gains Revenue (est.) / Power Cost / Net columns. Net figures carry the one + judgment colour — green in profit, red in loss. +- Dashboard panel round (#748): stat-heavy cards open collapsed to their headline stats with a + "Show all" toggle, the Advanced grid gains Your Stack / The Wider Pool section labels, the + workers table gains tabular numerics + row hover with offline-red scoped to the rig name, + chart active chips move from green to accent, and the topology diagram gains zone tints and + marching-ants on live routes. + ## [1.12.0] - 2026-07-22 ### Added diff --git a/VERSION b/VERSION index 0eed1a29..feaae22b 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.12.0 +1.13.0 diff --git a/build/dashboard/mining_dashboard/web/static/chart.mjs b/build/dashboard/mining_dashboard/web/static/chart.mjs index 9b3dee8f..570db891 100644 --- a/build/dashboard/mining_dashboard/web/static/chart.mjs +++ b/build/dashboard/mining_dashboard/web/static/chart.mjs @@ -464,7 +464,7 @@ export class ChartCard extends Component { return html`
- Range: + Range: ${RANGES.map( // Real buttons like the Avg/legend siblings (#657); the ?range= deep link // survives because setRange writes it via history.replaceState. @@ -481,7 +481,7 @@ export class ChartCard extends Component { }
- Avg: + Avg: ${WINDOWS.map( ([w, label]) => html`
`; +// Standardized Day/Month/Year estimate table — the one shape every earnings tab presents its +// estimate in: coin column in accent, a ≈-fiat column appearing once the coin's price is known +// (the same visibility gate the old per-cell fiat cards used). Replaces per-tab stat-grids whose +// two-column flow paired unrelated cells (XMR/year beside ≈/day). The coin triplet shares one +// precision (coinTriplet) so the rows align. +const EstTable = ({ unit, day, month, year, price, currency, title }) => { + const coins = coinTriplet([day, month, year], unit); + const haveFiat = Number.isFinite(price) && price > 0; + const rows = [ + ["Day", day, coins[0]], + ["Month", month, coins[1]], + ["Year", year, coins[2]], + ]; + return html` +
+ + + + + ${haveFiat ? html`` : null} + + + ${rows.map( + ([label, raw, coin]) => html` + + + + ${haveFiat ? html`` : null} + `, + )} + +
${unit}≈ ${currency || "USD"}
${label}${coin}${formatFiatAmount(coinFiat(raw, price))}
+
`; +}; + +// Sign colour for a net figure — the one judgment colour in the calculator: green when the +// operation profits, red when it loses. Everything that is a plain estimate stays accent/plain. +const netCls = (v) => (v !== null && Number.isFinite(v) && v < 0 ? "c-bad" : "c-ok"); + const SharesStat = ({ sw, label = "Share in Window" }) => html`
${label}

${sw.count}

`; +// Progressive disclosure for a busy stat-grid card ("show more" pattern). A card splits its +// StatCards into `headline` (always visible — the figures an operator actually glances at) and +// `detail` (behind the toggle, collapsed by default); both share one `stat-grid` so the layout is +// identical to the old flat list once expanded. Expansion is persisted per card via +// loadPref/savePref — the same helpers dashboardEarningsTab already uses — keyed on `prefKey` so +// each card remembers its own state independently across a reload. The toggle is a real `; + } +} + // Tari merge-mine status. The ✔ means the gRPC channel is actually up, so it's gated on `connected` // (channel_state READY) — NOT on `active` (a chain is merely configured). When configured but the // channel is down (e.g. TRANSIENT_FAILURE) we show the raw state in a warn style and no ✔, so a dead @@ -275,49 +353,56 @@ function Overview({ state }) { function NodeStats({ state }) { const hr = state.hashrate, st = state.stratum; - return html` -
-

My P2Pool Node Stats

-
+ // Headline = the figures an operator actually checks first (mode, total hashrate, routed + // averages, share health); stratum windows, connections, effort and the rest are drill-down + // detail behind the "show more" toggle (progressive disclosure). + const headline = html` <${StatCard} label="Mining Mode" value=${hr.mode_name} cls=${cVar(hr.mode_variant)} /> <${StatCard} label="Total Hashrate" value=${hr.total} cls="text-accent" /> <${StatCard} label="P2Pool 1h Avg" value=${hr.p2p_1h} cls=${cVar(hr.p2p_variant)} /> <${StatCard} label="P2Pool 24h Avg" value=${hr.p2p_24h} cls=${cVar(hr.p2p_variant)} /> + <${StatCard} label="Shares (OK/Err)" value=${st.shares} />`; + const detail = html`
Stratum (15m / 1h / 24h)

${st.h15} / ${st.h1h} / ${st.h24h}

- <${StatCard} label="Shares (OK/Err)" value=${st.shares} /> - <${StatCard} label="Effort" value=${st.effort} /> <${StatCard} label="Connections" value=${st.conns} /> + <${StatCard} label="Effort" value=${st.effort} /> <${StatCard} label="Reward Share" value=${st.reward_pct} /> <${StatCard} label="Total Shares" value=${st.total_shares} /> <${StatCard} label="Last Share" value=${st.last_share} /> - <${StatCard} label="Total Hashes" value=${st.total_hashes} span=${true} /> -
+ <${StatCard} label="Total Hashes (Node)" value=${st.total_hashes} span=${true} />`; + return html` +
+

My P2Pool Node Stats

+ <${MoreStats} prefKey="dashboardCardNode" headline=${headline} detail=${detail} count=${12} />
Wallet: ${st.wallet}
`; } function GlobalStats({ state }) { const p = state.pool; - return html` -
-

Global P2Pool Stats

-
- <${StatCard} label="Pool Hashrate" value=${p.hr} /> + // Headline = the pool's own money/health figures (hashrate, whether it's finding blocks, when + // it last did); sidechain internals, peers and uptime are reference detail, not a glance figure. + const headline = html` + <${StatCard} label="Pool Hashrate" value=${p.hr} cls="text-accent" /> + <${StatCard} label="Blocks Found" value=${p.blocks} /> +
Last Block

${p.last_blk}

`; + const detail = html` <${StatCard} label="Miners" value=${p.miners} /> <${StatCard} label="Sidechain Height" value=${p.sidechain_height} /> <${StatCard} label="Difficulty" value=${p.diff} /> - <${StatCard} label="Blocks Found" value=${p.blocks} /> <${StatCard} label="PPLNS Window" value=${p.pplns_win} /> <${StatCard} label="PPLNS Weight" value=${p.pplns_wgt} /> <${SharesStat} sw=${state.shares_window} /> -
Last Block

${p.last_blk}

<${StatCard} label="Peers" value=${p.peers} />
Uptime

${p.uptime}

- <${StatCard} label="Total Hashes" value=${p.total_hashes} /> -
+ <${StatCard} label="Total Hashes (Pool)" value=${p.total_hashes} />`; + return html` +
+

Global P2Pool Stats

+ <${MoreStats} prefKey="dashboardCardGlobal" headline=${headline} detail=${detail} count=${12} />
`; } @@ -364,18 +449,21 @@ function XvBStats({ state }) { function NetworkCard({ state }) { const n = state.network, m = state.monero; - return html` -
-

XMR Network

-
+ // Headline = the chain's own money/health figures (height, difficulty, block reward); node + // internals (mode, DB size, hash, network time) are reference detail. + const headline = html` <${StatCard} label="Block Height" value=${n.height} /> - <${StatCard} label="Reward" value=${n.reward} /> + <${StatCard} label="Difficulty" value=${n.diff} /> + <${StatCard} label="Reward" value=${n.reward} />`; + const detail = html` <${StatCard} label="Node Mode" value=${m.mode} /> <${StatCard} label="DB Size" value=${m.db_size} /> - <${StatCard} label="Difficulty" value=${n.diff} span=${true} />
Current Block Hash

${n.hash}

- <${StatCard} label="Network Time" value=${n.ts} span=${true} /> -
+ <${StatCard} label="Network Time" value=${n.ts} span=${true} />`; + return html` +
+

XMR Network

+ <${MoreStats} prefKey="dashboardCardNetwork" headline=${headline} detail=${detail} count=${7} />
`; } @@ -423,6 +511,7 @@ class XvbComparison extends Component { <${StatCard} label="Cost / yr" value=${cmp.cost !== null ? formatXmr(cmp.cost) : "—"} title="P2Pool earnings foregone by donating the tier threshold for a year (threshold × the P2Pool daily rate × 365)." /> <${StatCard} label="Net / yr" value=${sustainable && cmp.net !== null ? formatXmr(cmp.net) : "—"} + cls=${sustainable && cmp.net !== null ? netCls(cmp.net) : ""} title=${ sustainable ? "Expected XvB reward minus the P2Pool earnings given up. Shown only when XvB's estimate is available." @@ -462,7 +551,7 @@ class XvbComparison extends Component { // raffle status, never a payout; deliberately no entry counts or win odds — the draw is random // above the threshold. Hidden entirely while XvB is disabled. `coeffDay` (earnings.coeff_day) // feeds the per-tier payout comparison dropdown below. -function XvbTierBlock({ calc, hr, coeffDay, energy }) { +function XvbTierBlock({ calc, hr, coeffDay, energy, est }) { if (!calc || !calc.enabled) return null; const t = computeXvbTier(hr, calc); return html` @@ -478,6 +567,19 @@ function XvbTierBlock({ calc, hr, coeffDay, energy }) { <${StatCard} label="Target Tier" value=${calc.target_tier} title=${"The tier the donation controller is configured to aim for" + (calc.sustainable ? "." : " — currently NOT sustainable at your hashrate.")} />
+ ${ + // Current-tier expected reward (#712's published per-day figure) in the same + // standardized Day/Month/Year table every other tab shows. Only when the server sent a + // fresh estimate — never fabricated; a fixed published figure, NOT scaled by the + // what-if hashrate, and the heading says so. + est && est.xvbDay !== null + ? html` +

+ Current Tier Expected Reward — published by XvB

+ <${EstTable} unit="XMR" day=${est.xvbDay} month=${est.xvbMonth} year=${est.xvbYear} + price=${energy ? energy.xmr_price : 0} currency=${energy ? energy.currency : "USD"} />` + : null + } <${XvbComparison} calc=${calc} coeffDay=${coeffDay} hr=${hr} energy=${energy} />

${calc.note}${calc.mode_note ? " " + calc.mode_note : ""}

`; @@ -584,18 +686,10 @@ class EarningsCard extends Component {
@@ -625,7 +721,7 @@ class EarningsCard extends Component { xvb && xvb.enabled ? html` ` : null } @@ -680,6 +776,15 @@ function EnergyPanel({ energy, est }) { : en.includesTari ? "P2Pool XMR + Tari (merge-mined) earnings at your set prices, minus power cost. Excludes XvB (raffle status, not a per-day income estimate)." : "P2Pool XMR earnings at your XMR price, minus power cost. Excludes Tari (set dashboard.energy.tari_price to include it) and XvB (raffle status, not a per-day income estimate)."; + // One standardized Day/Month/Year table for the whole money side: kWh always, then Revenue / + // Cost / Net columns as their inputs exist (same appearance gates as before — never a + // fabricated figure). Revenue is the gross the net starts from, so the estimate is no longer + // implicit; Net keeps the one judgment colour (green profit / red loss). + const rows = [ + ["Day", en.kwhDay, en.grossDay, en.costDay, en.netDay], + ["Month", en.kwhMonth, en.grossMonth, en.costMonth, en.netMonth], + ["Year", en.kwhYear, en.grossYear, en.costYear, en.netYear], + ]; return html`
<${StatCard} label="Fleet Power" value=${formatUnit(energy.total_watts, "W")} @@ -691,32 +796,36 @@ function EnergyPanel({ energy, est }) { } /> <${StatCard} label="Efficiency" value=${formatUnit(energy.hs_per_watt, "H/s·W", 2)} title="Fleet hashrate ÷ fleet watts." /> - <${StatCard} label="Energy / day" value=${formatUnit(en.kwhDay, "kWh")} /> - <${StatCard} label="Energy / month" value=${formatUnit(en.kwhMonth, "kWh")} /> - <${StatCard} label="Energy / year" value=${formatUnit(en.kwhYear, "kWh")} /> +
+

Energy${haveCost ? ` — Cost${haveNet ? ` & Net Profit, ${netLabel}` : ""} (${cur})` : ""}

+
+ + + + + ${haveNet ? html`` : null} + ${haveCost ? html`` : null} + ${haveNet ? html`` : null} + + + ${rows.map( + ([label, kwh, gross, cost, net]) => html` + + + + ${haveNet ? html`` : null} + ${haveCost ? html`` : null} + ${haveNet ? html`` : null} + `, + )} + +
kWhRevenue (est.)Power CostNet
${label}${formatUnit(kwh, "")}${formatFiatAmount(gross)}${formatFiatAmount(cost)}${formatFiatAmount(net)}
${ haveCost - ? html` -

Cost${haveNet ? ` & Net Profit — ${netLabel}` : ""} (${cur})

-
- <${StatCard} label="Power cost / day" value=${formatFiat(en.costDay, cur)} /> - <${StatCard} label="Power cost / month" value=${formatFiat(en.costMonth, cur)} /> - <${StatCard} label="Power cost / year" value=${formatFiat(en.costYear, cur)} /> - ${ - haveNet - ? html` - <${StatCard} label="Net / day" value=${formatFiat(en.netDay, cur)} - cls=${en.netDay !== null && en.netDay < 0 ? "c-bad" : "text-accent"} - title=${netTitle} /> - <${StatCard} label="Net / month" value=${formatFiat(en.netMonth, cur)} - cls=${en.netMonth !== null && en.netMonth < 0 ? "c-bad" : "text-accent"} /> - <${StatCard} label="Net / year" value=${formatFiat(en.netYear, cur)} - cls=${en.netYear !== null && en.netYear < 0 ? "c-bad" : "text-accent"} />` - : html`<${StatCard} label="Net Profit" value="set xmr_price" - title="Set dashboard.energy.xmr_price (in your currency), or dashboard.energy.price_feed: true to fetch live prices from CoinGecko over Tor (opt-in — off by default, no clearnet egress)." />` - } -
` + ? haveNet + ? null + : html`

Set dashboard.energy.xmr_price (in your currency), or dashboard.energy.price_feed: true to fetch live prices from CoinGecko over Tor (opt-in — off by default, no clearnet egress), to see revenue and net profit.

` : html`

Set dashboard.energy.cost_per_kwh to see energy cost and net profit after power.

` }`; } @@ -998,12 +1107,14 @@ function DashboardView({ onInspect=${state.control_enabled ? onInspect : null} />
+
Your Stack
<${Overview} state=${state} /> <${NodeStats} state=${state} /> <${XvBStats} state=${state} /> <${EarningsCard} earnings=${state.earnings} xvb=${state.xvb_calc} energy=${state.energy} /> <${CadenceCard} cadence=${state.cadence} /> <${TariCard} tari=${state.tari} /> +
The Wider Pool
<${GlobalStats} state=${state} /> <${NetworkCard} state=${state} /> <${ComponentHealth} topology=${state.topology} egress=${state.egress} /> @@ -1053,6 +1164,7 @@ export function App({ ? html`<${SyncView} sync=${state.sync} />` : html`<${Fragment}> <${HeroBand} state=${state} /> + <${MineCartTrain} chart=${state.chart} blocks=${state.blocks} payouts=${state.payouts} /> <${DashboardView} state=${state} ui=${ui} onRange=${onRange} onSort=${onSort} onView=${onView} onZoom=${onZoom} onResetZoom=${onResetZoom} onToggleSeries=${onToggleSeries} onAvgWindow=${onAvgWindow} diff --git a/build/dashboard/mining_dashboard/web/static/dashboard.css b/build/dashboard/mining_dashboard/web/static/dashboard.css index 74c0284d..c0131de4 100644 --- a/build/dashboard/mining_dashboard/web/static/dashboard.css +++ b/build/dashboard/mining_dashboard/web/static/dashboard.css @@ -96,6 +96,18 @@ body { gap: 20px; margin-bottom: 20px; } +/* Section label inside the Advanced card grid (#159): the grid itself has no visual grouping, so + * these mark the "mine" -> "the world" transition the layout comment describes. Spans the full + * grid row; typography matches .est-heading (the other muted section marker in this stylesheet). */ +.grid-section-label { + grid-column: 1 / -1; + margin-top: 6px; + font-size: 0.7rem; + font-weight: 600; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; +} .card { background: var(--card); border: 1px solid var(--border); @@ -164,6 +176,11 @@ body { padding: 16px 18px; text-align: center; } +/* Total Hashrate (always first — heroKpis, logic.mjs) gets an accent cue so the strip reads with + * a clear headline instead of six equally-weighted numbers. */ +.hero-band .hero-kpi:first-child { + border-top: 2px solid var(--accent); +} .hero-value { font-size: 1.6rem; font-weight: 700; @@ -178,6 +195,18 @@ body { color: var(--text-muted); } +/* Mine cart train (pixel art) under the hero band: purely decorative — the chart above shows + * the same data — so it stays short and quiet. The canvas draws at a 2× logical scale; height + * matches the renderer's 44-logical-px strip. */ +.minecart-strip { + margin: -4px 0 16px 0; +} +.minecart-strip canvas { + display: block; + width: 100%; + height: 88px; +} + /* Typography */ h2 { margin: 0; @@ -241,6 +270,26 @@ h3 { .col-span-2 { grid-column: span 2; } +/* Progressive disclosure ("show more") toggle for a stat-grid card: a real