apow-cli 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,321 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getDashboardHtml = getDashboardHtml;
4
+ function getDashboardHtml() {
5
+ return `<!DOCTYPE html>
6
+ <html lang="en">
7
+ <head>
8
+ <meta charset="utf-8">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1">
10
+ <title>APoW Dashboard</title>
11
+ <style>
12
+ *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
13
+ :root{
14
+ --bg:#0a0a0a;--card:#141414;--card-border:#161616;
15
+ --text:#e5e5e5;--text-dim:#737373;--accent:#0052FF;
16
+ --rarity-common:#a1a1aa;--rarity-uncommon:#4ade80;
17
+ --rarity-rare:#60a5fa;--rarity-epic:#a78bfa;--rarity-mythic:#fbbf24;
18
+ }
19
+ body{background:var(--bg);color:var(--text);font-family:SFMono-Regular,'SF Mono',Menlo,Consolas,'Liberation Mono',monospace;font-size:13px;line-height:1.4}
20
+ a{color:inherit;text-decoration:none}
21
+ .container{min-height:100vh;padding:12px}
22
+ /* Header */
23
+ .header{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
24
+ .header h1{font-size:14px;font-weight:700;letter-spacing:.05em}
25
+ .status{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--text-dim)}
26
+ .pulse{display:inline-block;height:6px;width:6px;border-radius:50%;background:var(--accent);animation:pulse 1.5s infinite}
27
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}
28
+ /* Banners */
29
+ .rpc-warning{border-radius:6px;border:1px solid #7c2d12;background:rgba(69,26,3,.3);padding:8px 12px;font-size:12px;color:#fdba74;margin-bottom:12px}
30
+ .rpc-warning a{text-decoration:underline}
31
+ .rpc-warning a:hover{color:#fed7aa}
32
+ .error-banner{border-radius:6px;border:1px solid #7f1d1d;background:rgba(69,10,10,.3);padding:8px 12px;font-size:12px;color:#f87171;margin-bottom:12px}
33
+ /* Fleet tabs */
34
+ .fleet-tabs{display:flex;align-items:center;gap:4px;margin-bottom:12px;overflow-x:auto}
35
+ .fleet-tab{padding:4px 12px;font-size:12px;border-radius:4px;cursor:pointer;white-space:nowrap;border:1px solid transparent;background:var(--card);color:var(--text-dim);transition:all .15s}
36
+ .fleet-tab:hover{color:var(--text)}
37
+ .fleet-tab.active{background:var(--accent);color:#fff;border-color:var(--accent)}
38
+ /* Stats grid */
39
+ .stats-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:1px;background:var(--card-border);border-radius:8px;overflow:hidden;margin-bottom:12px}
40
+ @media(min-width:640px){.stats-grid{grid-template-columns:repeat(5,1fr)}}
41
+ @media(min-width:1024px){.stats-grid{grid-template-columns:repeat(12,1fr)}}
42
+ .stat-cell{background:var(--card);padding:8px 12px}
43
+ .stat-label{font-size:10px;color:var(--text-dim);text-transform:uppercase;letter-spacing:.05em}
44
+ .stat-value{font-size:14px;font-weight:600}
45
+ .stat-value.highlight{color:var(--accent)}
46
+ .stat-sub{font-size:10px;color:var(--text-dim)}
47
+ /* Wallet grid */
48
+ .wallet-grid{display:grid;grid-template-columns:1fr;gap:8px}
49
+ @media(min-width:640px){.wallet-grid{grid-template-columns:repeat(2,1fr)}}
50
+ @media(min-width:1024px){.wallet-grid{grid-template-columns:repeat(3,1fr)}}
51
+ @media(min-width:1280px){.wallet-grid{grid-template-columns:repeat(4,1fr)}}
52
+ .wallet-card{border-radius:6px;border:1px solid var(--card-border);background:var(--card);overflow:hidden}
53
+ .wallet-card.active{border-color:var(--accent)}
54
+ .wallet-header{display:flex;align-items:center;justify-content:space-between;padding:6px 10px;border-bottom:1px solid var(--card-border)}
55
+ .wallet-addr{font-size:11px;font-weight:500;color:var(--text-dim);transition:color .15s}
56
+ .wallet-addr:hover{color:var(--accent)}
57
+ .wallet-stats{display:flex;align-items:center;gap:12px;font-size:11px}
58
+ .wallet-stats .hp{color:var(--rarity-uncommon)}
59
+ .wallet-stats .dim{color:var(--text-dim)}
60
+ .wallet-stats .agent-hl{color:var(--accent)}
61
+ .miners-wrap{display:flex;flex-wrap:wrap;gap:8px;padding:8px 10px}
62
+ .miner-thumb{display:flex;align-items:center;gap:4px;text-decoration:none}
63
+ .miner-thumb:hover .miner-img{box-shadow:0 0 0 1px var(--accent)}
64
+ .miner-thumb:hover .miner-id{color:var(--accent)}
65
+ .miner-img{height:40px;width:40px;border-radius:4px;transition:box-shadow .15s}
66
+ .miner-placeholder{height:40px;width:40px;border-radius:4px;background:rgba(255,255,255,.05);animation:pulse 1.5s infinite}
67
+ .miner-id{font-size:9px;color:var(--text-dim);transition:color .15s}
68
+ .no-miners{padding:8px 10px;font-size:10px;color:var(--text-dim)}
69
+ /* Empty state */
70
+ .empty-state{border-radius:6px;border:1px solid var(--card-border);background:var(--card);padding:16px 20px;font-size:12px;color:var(--text-dim)}
71
+ .empty-state h2{font-size:14px;color:var(--text);text-align:center;margin-bottom:12px}
72
+ .empty-state code{color:var(--accent)}
73
+ .empty-state .cmds{margin:8px 0 0 8px;line-height:2}
74
+ .empty-state .hint{margin-top:12px;font-size:10px}
75
+ /* Loading */
76
+ .loading{text-align:center;padding:80px 0;color:var(--text-dim)}
77
+ </style>
78
+ </head>
79
+ <body>
80
+ <div class="container">
81
+ <div class="header">
82
+ <h1>APoW DASHBOARD</h1>
83
+ <div class="status"><span class="pulse" id="statusDot"></span><span id="statusText">Loading...</span></div>
84
+ </div>
85
+ <div id="rpcWarning" class="rpc-warning" style="display:none">
86
+ Using public Base RPC (mainnet.base.org) — this is unreliable for dashboards with many wallets.
87
+ Get a free dedicated endpoint at <a href="https://www.alchemy.com/base" target="_blank" rel="noopener noreferrer">alchemy.com</a> for reliable data.
88
+ </div>
89
+ <div id="errorBanner" class="error-banner" style="display:none">Failed to fetch data. Check RPC connection.</div>
90
+ <div id="fleetTabs" class="fleet-tabs" style="display:none"></div>
91
+ <div id="statsGrid" class="stats-grid" style="display:none"></div>
92
+ <div id="walletGrid" class="wallet-grid"></div>
93
+ <div id="emptyState" class="empty-state" style="display:none">
94
+ <h2>No wallets detected.</h2>
95
+ <p>To add wallets:</p>
96
+ <div class="cmds">
97
+ <div><code>apow dashboard add &lt;address&gt;</code> <span>— add a specific address</span></div>
98
+ <div><code>apow dashboard scan</code> <span>— auto-detect from wallet files in current dir</span></div>
99
+ </div>
100
+ <p class="hint">Wallets are also auto-detected from your .env PRIVATE_KEY on dashboard start.</p>
101
+ </div>
102
+ <div id="loading" class="loading">Loading...</div>
103
+ </div>
104
+ <script>
105
+ (function(){
106
+ var activeFleet = 'All';
107
+ var balanceHistory = [];
108
+ var prevMines = {};
109
+ var activeWallets = {};
110
+ var lastSeen = {};
111
+
112
+ function fetchJson(url) {
113
+ return fetch(url).then(function(r) {
114
+ if (!r.ok) throw new Error(r.status + ' ' + r.statusText);
115
+ return r.json();
116
+ });
117
+ }
118
+
119
+ function fmt(n, d) { return Number(n).toLocaleString(undefined, { maximumFractionDigits: d !== undefined ? d : 1 }); }
120
+ function fmtFixed(n, d) { return Number(n).toFixed(d); }
121
+
122
+ function statCellHtml(label, value, opts) {
123
+ opts = opts || {};
124
+ var cls = 'stat-value' + (opts.highlight ? ' highlight' : '');
125
+ var sub = opts.sub ? '<div class="stat-sub">' + opts.sub + '</div>' : '';
126
+ return '<div class="stat-cell"><div class="stat-label">' + label + '</div><div class="' + cls + '">' + value + '</div>' + sub + '</div>';
127
+ }
128
+
129
+ function shortAddr(addr) { return addr.slice(0, 6) + '...' + addr.slice(-4); }
130
+
131
+ function escapeHtml(s) {
132
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
133
+ }
134
+
135
+ function updateMiningRate(totalAgent) {
136
+ if (totalAgent <= 0) return { perMin: null, perHour: null, sessionGain: null, sessionMinutes: null };
137
+ var now = Date.now();
138
+ var h = balanceHistory;
139
+ if (h.length === 0 || h[h.length - 1].agent !== totalAgent) {
140
+ h.push({ agent: totalAgent, timestamp: now });
141
+ }
142
+ if (h.length > 120) h.splice(0, h.length - 120);
143
+ if (h.length < 2) return { perMin: null, perHour: null, sessionGain: null, sessionMinutes: null };
144
+ var first = h[0], last = h[h.length - 1];
145
+ var elapsedMin = (last.timestamp - first.timestamp) / 60000;
146
+ if (elapsedMin < 0.5) return { perMin: null, perHour: null, sessionGain: null, sessionMinutes: null };
147
+ var gained = last.agent - first.agent;
148
+ var perMin = gained / elapsedMin;
149
+ return { perMin: perMin, perHour: perMin * 60, sessionGain: gained, sessionMinutes: Math.floor(elapsedMin) };
150
+ }
151
+
152
+ function updateActiveWallets(wallets) {
153
+ if (!wallets) return;
154
+ var now = Date.now();
155
+ for (var i = 0; i < wallets.length; i++) {
156
+ var w = wallets[i];
157
+ var totalMines = 0;
158
+ for (var j = 0; j < w.miners.length; j++) totalMines += Number(w.miners[j].mineCount);
159
+ var prev = prevMines[w.address];
160
+ if (prev !== undefined && totalMines > prev) {
161
+ activeWallets[w.address] = true;
162
+ lastSeen[w.address] = now;
163
+ }
164
+ if (lastSeen[w.address] && now - lastSeen[w.address] > 300000) {
165
+ delete activeWallets[w.address];
166
+ }
167
+ prevMines[w.address] = totalMines;
168
+ }
169
+ }
170
+
171
+ function renderFleetTabs(fleets) {
172
+ var el = document.getElementById('fleetTabs');
173
+ if (!fleets || fleets.length <= 1) { el.style.display = 'none'; return; }
174
+ el.style.display = 'flex';
175
+ var totalCount = 0;
176
+ for (var i = 0; i < fleets.length; i++) totalCount += fleets[i].walletCount;
177
+ var html = '<div class="fleet-tab' + (activeFleet === 'All' ? ' active' : '') + '" data-fleet="All">All (' + totalCount + ')</div>';
178
+ for (var i = 0; i < fleets.length; i++) {
179
+ var f = fleets[i];
180
+ html += '<div class="fleet-tab' + (activeFleet === f.name ? ' active' : '') + '" data-fleet="' + escapeHtml(f.name) + '">' + escapeHtml(f.name) + ' (' + f.walletCount + ')</div>';
181
+ }
182
+ el.innerHTML = html;
183
+ var tabs = el.querySelectorAll('.fleet-tab');
184
+ for (var t = 0; t < tabs.length; t++) {
185
+ tabs[t].addEventListener('click', function() {
186
+ activeFleet = this.getAttribute('data-fleet');
187
+ refresh();
188
+ });
189
+ }
190
+ }
191
+
192
+ function renderStats(wallets, network) {
193
+ var el = document.getElementById('statsGrid');
194
+ if (!wallets && !network) { el.style.display = 'none'; return; }
195
+ el.style.display = 'grid';
196
+ var totalAgent = 0, totalEth = 0, totalMiners = 0, totalHashpower = 0;
197
+ if (wallets) {
198
+ for (var i = 0; i < wallets.length; i++) {
199
+ totalAgent += Number(wallets[i].agentBalance);
200
+ totalEth += Number(wallets[i].ethBalance);
201
+ totalMiners += wallets[i].miners.length;
202
+ for (var j = 0; j < wallets[i].miners.length; j++) totalHashpower += wallets[i].miners[j].hashpower;
203
+ }
204
+ }
205
+ var rate = updateMiningRate(totalAgent);
206
+ var html = '';
207
+ html += statCellHtml('TOTAL AGENT', fmt(totalAgent, 1), { highlight: true });
208
+ html += statCellHtml('TOTAL ETH', fmtFixed(totalEth, 4));
209
+ html += statCellHtml('WALLETS', wallets ? String(wallets.length) : '\\u2014');
210
+ html += statCellHtml('MINERS', String(totalMiners));
211
+ html += statCellHtml('HASHPOWER', fmtFixed(totalHashpower / 100, 1) + 'x');
212
+ html += statCellHtml('AGENT/MIN', rate.perMin !== null ? fmtFixed(rate.perMin, 2) : '\\u2014', { highlight: rate.perMin !== null && rate.perMin > 0 });
213
+ html += statCellHtml('AGENT/HR', rate.perHour !== null ? fmtFixed(rate.perHour, 1) : '\\u2014', {
214
+ highlight: rate.perHour !== null && rate.perHour > 0,
215
+ sub: rate.sessionGain !== null ? '+' + fmtFixed(rate.sessionGain, 1) + ' in ' + rate.sessionMinutes + 'm' : undefined
216
+ });
217
+ if (network) {
218
+ html += statCellHtml('ERA', String(network.era), { sub: fmt(network.minesUntilNextEra, 0) + ' to next' });
219
+ html += statCellHtml('BASE REWARD', fmtFixed(network.baseReward, 2) + ' AGENT/mine');
220
+ html += statCellHtml('SUPPLY', fmtFixed(network.supplyPct, 2) + '%');
221
+ html += statCellHtml('DIFFICULTY', String(network.difficulty));
222
+ html += statCellHtml('NETWORK MINES', fmt(network.totalMines, 0));
223
+ }
224
+ el.innerHTML = html;
225
+ }
226
+
227
+ function renderWallets(wallets) {
228
+ var grid = document.getElementById('walletGrid');
229
+ var empty = document.getElementById('emptyState');
230
+ if (!wallets || wallets.length === 0) {
231
+ grid.innerHTML = '';
232
+ if (wallets) empty.style.display = 'block';
233
+ return;
234
+ }
235
+ empty.style.display = 'none';
236
+ wallets = wallets.slice().sort(function(a, b) { return Number(b.agentBalance) - Number(a.agentBalance); });
237
+ var html = '';
238
+ for (var i = 0; i < wallets.length; i++) {
239
+ var w = wallets[i];
240
+ var isActive = !!activeWallets[w.address];
241
+ var hasAgent = Number(w.agentBalance) > 0;
242
+ var walletHp = 0;
243
+ for (var j = 0; j < w.miners.length; j++) walletHp += w.miners[j].hashpower;
244
+ html += '<div class="wallet-card' + (isActive ? ' active' : '') + '">';
245
+ html += '<div class="wallet-header">';
246
+ html += '<a class="wallet-addr" href="https://basescan.org/address/' + w.address + '" target="_blank" rel="noopener noreferrer">' + shortAddr(w.address) + '</a>';
247
+ html += '<div class="wallet-stats">';
248
+ if (w.miners.length > 0) html += '<span class="hp">' + fmtFixed(walletHp / 100, 1) + 'x</span>';
249
+ html += '<span><span class="dim">E </span>' + fmtFixed(Number(w.ethBalance), 4) + '</span>';
250
+ html += '<span' + (hasAgent ? ' class="agent-hl"' : '') + '><span class="dim">A </span>' + fmt(Number(w.agentBalance), 1) + '</span>';
251
+ html += '</div></div>';
252
+ if (w.miners.length > 0) {
253
+ html += '<div class="miners-wrap">';
254
+ for (var j = 0; j < w.miners.length; j++) {
255
+ var m = w.miners[j];
256
+ var osUrl = 'https://opensea.io/item/base/0xb7cad3ca5f2bd8aec2eb67d6e8d448099b3bc03d/' + m.tokenId;
257
+ html += '<a class="miner-thumb" href="' + osUrl + '" target="_blank" rel="noopener noreferrer">';
258
+ if (m.imageUri) {
259
+ html += '<img class="miner-img" src="' + escapeHtml(m.imageUri) + '" alt="#' + m.tokenId + '">';
260
+ } else {
261
+ html += '<div class="miner-placeholder"></div>';
262
+ }
263
+ html += '<span class="miner-id">#' + m.tokenId + '</span></a>';
264
+ }
265
+ html += '</div>';
266
+ } else {
267
+ html += '<div class="no-miners">No miners</div>';
268
+ }
269
+ html += '</div>';
270
+ }
271
+ grid.innerHTML = html;
272
+ }
273
+
274
+ var networkData = null;
275
+ var walletsData = null;
276
+ var hasError = false;
277
+
278
+ function setStatus(refreshing) {
279
+ document.getElementById('statusDot').style.display = refreshing ? 'inline-block' : 'none';
280
+ document.getElementById('statusText').textContent = refreshing ? 'Refreshing...' : 'Live';
281
+ }
282
+
283
+ function refresh() {
284
+ setStatus(true);
285
+ var fleetParam = encodeURIComponent(activeFleet);
286
+ Promise.all([
287
+ fetchJson('/api/network').catch(function(e) { return null; }),
288
+ fetchJson('/api/wallets?fleet=' + fleetParam).catch(function(e) { return null; }),
289
+ fetchJson('/api/fleets').catch(function(e) { return null; }),
290
+ fetchJson('/api/config').catch(function(e) { return null; })
291
+ ]).then(function(results) {
292
+ document.getElementById('loading').style.display = 'none';
293
+ var net = results[0], wal = results[1], fleets = results[2], cfg = results[3];
294
+ hasError = !net && !wal;
295
+ document.getElementById('errorBanner').style.display = hasError ? 'block' : 'none';
296
+ if (net) networkData = net;
297
+ if (wal && !wal.error) {
298
+ walletsData = wal;
299
+ updateActiveWallets(wal);
300
+ }
301
+ if (cfg) {
302
+ document.getElementById('rpcWarning').style.display = cfg.rpcIsDefault ? 'block' : 'none';
303
+ }
304
+ renderFleetTabs(fleets);
305
+ renderStats(walletsData, networkData);
306
+ renderWallets(walletsData);
307
+ setStatus(false);
308
+ }).catch(function() {
309
+ document.getElementById('loading').style.display = 'none';
310
+ document.getElementById('errorBanner').style.display = 'block';
311
+ setStatus(false);
312
+ });
313
+ }
314
+
315
+ refresh();
316
+ setInterval(refresh, 30000);
317
+ })();
318
+ </script>
319
+ </body>
320
+ </html>`;
321
+ }