|
| 1 | +<% |
| 2 | +const fmtN = (n) => new Intl.NumberFormat('en-US').format(n || 0); |
| 3 | +const fmtF = (n, d = 4) => (typeof n === 'number' ? n.toFixed(d) : '—'); |
| 4 | +const fmtTs = (t) => t ? new Date(t * 1000).toISOString().replace('T', ' ').slice(0, 19) + 'Z' : '—'; |
| 5 | +const ago = (t) => { |
| 6 | + if (!t) return '—'; |
| 7 | + const s = Math.max(0, Math.floor(Date.now() / 1000) - t); |
| 8 | + if (s < 60) return s + 's ago'; |
| 9 | + if (s < 3600) return Math.floor(s / 60) + 'm ago'; |
| 10 | + if (s < 86400) return Math.floor(s / 3600) + 'h ago'; |
| 11 | + return Math.floor(s / 86400) + 'd ago'; |
| 12 | +}; |
| 13 | +const fmtSats = (sats) => { |
| 14 | + if (!sats) return '0 sats'; |
| 15 | + const btc = sats / 1e8; |
| 16 | + if (btc >= 0.01) return fmtN(sats) + ' sats (' + btc.toFixed(8) + ' BTC)'; |
| 17 | + return fmtN(sats) + ' sats'; |
| 18 | +}; |
| 19 | +/* Precompute a running_accrued column for the recent-shares table. |
| 20 | + * The rows come newest-first, so we walk in reverse to accumulate. */ |
| 21 | +const recentAsc = [...audit.recent].reverse(); |
| 22 | +let running = 0; |
| 23 | +const withRunning = recentAsc.map(r => { |
| 24 | + running += r.credit_sats; |
| 25 | + return { ...r, running_accrued: running }; |
| 26 | +}).reverse(); |
| 27 | +const naiveAccrued = Math.floor(audit.totals.sum_difficulty * audit.rate); |
| 28 | +const drift = audit.totals.accrued_computed - naiveAccrued; |
| 29 | +%> |
| 30 | +<!doctype html> |
| 31 | +<html lang="en"> |
| 32 | +<head> |
| 33 | + <meta charset="utf-8"> |
| 34 | + <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 35 | + <meta http-equiv="refresh" content="30"> |
| 36 | + <title>audit · <%= audit.worker.name %> · simplepool</title> |
| 37 | + <link rel="stylesheet" href="/static/style.css"> |
| 38 | +</head> |
| 39 | +<body> |
| 40 | + <header class="top"> |
| 41 | + <a href="/" class="brand">simplepool</a> |
| 42 | + <span class="tag">worker audit</span> |
| 43 | + <span style="margin-left:1em"><a href="/admin">← back to admin</a></span> |
| 44 | + </header> |
| 45 | + <main class="wrap"> |
| 46 | + <section class="card info"> |
| 47 | + <h2>Why is this balance what it is?</h2> |
| 48 | + <p class="muted small"> |
| 49 | + Answers a miner's "where does <em>my</em> number come from" |
| 50 | + question at the byte level. Every accepted share credits |
| 51 | + <code>FLOOR(difficulty × rate)</code> sats, where <em>rate</em> |
| 52 | + comes from <code>proxy.conf</code>'s |
| 53 | + <code>pps_sats_per_diff</code>. Truncation is per-share; that's |
| 54 | + the authoritative sum shown below. |
| 55 | + </p> |
| 56 | + </section> |
| 57 | + |
| 58 | + <section class="card"> |
| 59 | + <h2><%= audit.worker.name %></h2> |
| 60 | + <div class="grid"> |
| 61 | + <div><label>Thunder address</label><span class="mono small"><%= audit.worker.thunder_address || '—' %></span></div> |
| 62 | + <div><label>First seen</label><span title="<%= fmtTs(audit.worker.first_seen) %>"><%= ago(audit.worker.first_seen) %></span></div> |
| 63 | + <div><label>Last seen</label><span title="<%= fmtTs(audit.worker.last_seen) %>"><%= ago(audit.worker.last_seen) %></span></div> |
| 64 | + </div> |
| 65 | + </section> |
| 66 | + |
| 67 | + <section class="card"> |
| 68 | + <h2>Ledger</h2> |
| 69 | + <div class="grid"> |
| 70 | + <div><label>Owed (needs payout)</label><strong><%= fmtSats(audit.ledger.owed) %></strong></div> |
| 71 | + <div><label>Accrued (all-time, from pps_credits)</label><strong><%= fmtSats(audit.ledger.accrued) %></strong></div> |
| 72 | + <div><label>Paid (all-time)</label><strong><%= fmtSats(audit.ledger.paid) %></strong></div> |
| 73 | + <div><label>Last updated</label><span title="<%= fmtTs(audit.ledger.last_updated) %>"><%= ago(audit.ledger.last_updated) %></span></div> |
| 74 | + </div> |
| 75 | + </section> |
| 76 | + |
| 77 | + <section class="card"> |
| 78 | + <h2>Cross-check</h2> |
| 79 | + <p class="muted small" style="margin-top:0"> |
| 80 | + The pool credits <code>FLOOR(difficulty × <%= fmtN(audit.rate) %>)</code> |
| 81 | + sats per accepted share. If the numbers below don't match, the |
| 82 | + DB has drifted from the C proxy's log — worth investigating. |
| 83 | + </p> |
| 84 | + <div class="grid"> |
| 85 | + <div><label>Accepted shares</label><strong><%= fmtN(audit.totals.share_count) %></strong></div> |
| 86 | + <div><label>Blocks found</label><strong><%= fmtN(audit.totals.blocks_found) %></strong></div> |
| 87 | + <div><label>Σ difficulty</label><strong><%= fmtF(audit.totals.sum_difficulty, 6) %></strong></div> |
| 88 | + <div><label>Rate (sats × diff)</label><strong><%= fmtN(audit.rate) %></strong></div> |
| 89 | + <div><label>Σ FLOOR(diff × rate) — authoritative</label><strong><%= fmtSats(audit.totals.accrued_computed) %></strong></div> |
| 90 | + <div><label>pps_credits.accrued (stored)</label><strong><%= fmtSats(audit.ledger.accrued) %></strong></div> |
| 91 | + </div> |
| 92 | + <% if (audit.ledger.accrued === audit.totals.accrued_computed) { %> |
| 93 | + <p class="muted small" style="margin-top:0.75em;color:#5b8"> |
| 94 | + ✓ Stored accrued matches the per-share truncated sum. |
| 95 | + </p> |
| 96 | + <% } else { %> |
| 97 | + <p class="muted small" style="margin-top:0.75em;color:#c66"> |
| 98 | + ⚠ Stored accrued differs by |
| 99 | + <%= fmtSats(audit.ledger.accrued - audit.totals.accrued_computed) %>. |
| 100 | + This usually means the DB was populated across a rate change, |
| 101 | + OR someone edited pps_credits by hand. Look at the C |
| 102 | + proxy log for the WARN line at the last mode/rate change. |
| 103 | + </p> |
| 104 | + <% } %> |
| 105 | + <p class="muted small" style="margin-top:0.5em"> |
| 106 | + (Naive sum FLOOR(Σ diff × rate) would be <%= fmtSats(naiveAccrued) %>; |
| 107 | + per-share truncation costs |
| 108 | + <%= drift === 0 ? '0 sats' : Math.abs(drift) + ' sats' %> |
| 109 | + over the window — the difference is the number of shares whose |
| 110 | + fractional-sat contribution was rounded down.) |
| 111 | + </p> |
| 112 | + </section> |
| 113 | + |
| 114 | + <section class="card"> |
| 115 | + <h2>Daily breakdown</h2> |
| 116 | + <% if (audit.days.length === 0) { %> |
| 117 | + <p class="muted">No activity yet.</p> |
| 118 | + <% } else { %> |
| 119 | + <table> |
| 120 | + <thead> |
| 121 | + <tr> |
| 122 | + <th>Day (UTC)</th> |
| 123 | + <th>Shares</th> |
| 124 | + <th>Σ difficulty</th> |
| 125 | + <th>Accrued this day</th> |
| 126 | + <th>Blocks</th> |
| 127 | + </tr> |
| 128 | + </thead> |
| 129 | + <tbody> |
| 130 | + <% audit.days.forEach(d => { %> |
| 131 | + <tr> |
| 132 | + <td class="mono small"><%= d.day %></td> |
| 133 | + <td><%= fmtN(d.shares) %></td> |
| 134 | + <td><%= fmtF(d.sum_diff, 6) %></td> |
| 135 | + <td><strong><%= fmtSats(d.accrued_delta) %></strong></td> |
| 136 | + <td><%= d.blocks > 0 ? d.blocks : '' %></td> |
| 137 | + </tr> |
| 138 | + <% }) %> |
| 139 | + </tbody> |
| 140 | + </table> |
| 141 | + <% } %> |
| 142 | + </section> |
| 143 | + |
| 144 | + <section class="card"> |
| 145 | + <h2>Recent shares (last <%= withRunning.length %>, newest first)</h2> |
| 146 | + <% if (withRunning.length === 0) { %> |
| 147 | + <p class="muted">No shares recorded.</p> |
| 148 | + <% } else { %> |
| 149 | + <p class="muted small" style="margin-top:0"> |
| 150 | + The <em>credit</em> column is what this share added to your |
| 151 | + balance. <em>Running accrued</em> is the sum from the oldest |
| 152 | + visible share up to and including this one. |
| 153 | + </p> |
| 154 | + <table> |
| 155 | + <thead> |
| 156 | + <tr> |
| 157 | + <th>#</th> |
| 158 | + <th>When</th> |
| 159 | + <th>Difficulty</th> |
| 160 | + <th>Credit</th> |
| 161 | + <th>Block?</th> |
| 162 | + <th>Running accrued</th> |
| 163 | + </tr> |
| 164 | + </thead> |
| 165 | + <tbody> |
| 166 | + <% withRunning.forEach(r => { %> |
| 167 | + <tr<%= r.is_block ? ' style="font-weight:600"' : '' %>> |
| 168 | + <td class="mono small"><%= r.id %></td> |
| 169 | + <td class="mono small" title="<%= fmtTs(r.ts) %>"><%= ago(r.ts) %></td> |
| 170 | + <td><%= fmtF(r.difficulty, 6) %></td> |
| 171 | + <td><%= fmtSats(r.credit_sats) %></td> |
| 172 | + <td><%= r.is_block ? '✓ ' + (r.block_hash ? r.block_hash.slice(0, 10) + '…' : '') : '' %></td> |
| 173 | + <td><%= fmtSats(r.running_accrued) %></td> |
| 174 | + </tr> |
| 175 | + <% }) %> |
| 176 | + </tbody> |
| 177 | + </table> |
| 178 | + <% } %> |
| 179 | + </section> |
| 180 | + </main> |
| 181 | + <footer class="foot"> |
| 182 | + <span>read-only · auto-refresh 30s</span> |
| 183 | + <span class="sep">·</span> |
| 184 | + <span>worker audit</span> |
| 185 | + </footer> |
| 186 | +</body> |
| 187 | +</html> |
0 commit comments