dankgrinder 7.62.0 → 7.64.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,284 @@
1
+ /**
2
+ * DankGrinder CLI Dashboard
3
+ *
4
+ * Modern terminal UI with box-drawing characters, gradient accents, and real-time stats.
5
+ *
6
+ * Usage:
7
+ * const { renderDashboard } = require('./dashboard');
8
+ * renderDashboard(context); // called from grinder.js render loop
9
+ *
10
+ * Context shape:
11
+ * {
12
+ * workers, dashboardStarted, dashboardRendering, dashboardLines,
13
+ * totalBalance, totalCoins, totalCommands, startTime,
14
+ * sessionPeakCoins, isNewHigh, recentLogs, globalCmdRate,
15
+ * earningsHistory, lastEarningsSample,
16
+ * CLOUD_MODE, CLUSTER_ENABLED, PKG_VERSION,
17
+ * AccountWorker, PULSE_CHARS, getSpinner, gradientText,
18
+ * rgb, c, BOX,
19
+ * }
20
+ */
21
+
22
+ 'use strict';
23
+
24
+ // ── Color shortcuts (resolved from context) ────────────────────
25
+ let _c, _rgb, _BOX, _getSpinner, _gradientText;
26
+
27
+ function initColors(ctx) {
28
+ _c = ctx.c;
29
+ _rgb = ctx.rgb;
30
+ _BOX = ctx.BOX;
31
+ _getSpinner = ctx.getSpinner;
32
+ _gradientText = ctx.gradientText;
33
+ }
34
+
35
+ function pad(str, width) {
36
+ const raw = str.replace(/\x1b\[[0-9;]*m/g, '');
37
+ return str + ' '.repeat(Math.max(0, width - raw.length));
38
+ }
39
+
40
+ // ── Formatters ─────────────────────────────────────────────────
41
+
42
+ function fmtCoins(n) {
43
+ if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
44
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
45
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
46
+ return n.toLocaleString();
47
+ }
48
+
49
+ function fmtUptime(startTime) {
50
+ const s = Math.floor((Date.now() - startTime) / 1000);
51
+ const h = Math.floor(s / 3600);
52
+ const m = Math.floor((s % 3600) / 60);
53
+ const sec = s % 60;
54
+ if (h > 0) return `${h}h ${m}m ${sec}s`;
55
+ if (m > 0) return `${m}m ${sec}s`;
56
+ return `${sec}s`;
57
+ }
58
+
59
+ // ── Render ──────────────────────────────────────────────────────
60
+
61
+ function renderDashboard(ctx) {
62
+ if (!ctx.dashboardStarted || ctx.workers.length === 0 || ctx.dashboardRendering) return;
63
+
64
+ initColors(ctx);
65
+
66
+ const tw = Math.max(process.stdout.columns || 80, 60);
67
+
68
+ // Color palette (resolved)
69
+ const A = _rgb(139, 92, 246); // purple
70
+ const G = _rgb(52, 211, 153); // green
71
+ const B = _rgb(96, 165, 250); // blue
72
+ const Au = _rgb(255, 215, 0); // gold
73
+ const O = _rgb(251, 146, 60); // orange
74
+ const Cy = _rgb(34, 211, 238); // cyan
75
+ const R = _rgb(239, 68, 68); // red
76
+ const Y = _rgb(251, 191, 36); // yellow
77
+ const W = _c.white;
78
+ const D = _c.dim;
79
+ const _ = _c.reset;
80
+ const BOX = _BOX;
81
+
82
+ const rows = [];
83
+
84
+ // ── Aggregate stats ──────────────────────────────────────────
85
+ let aggBalance = 0, aggCoins = 0, aggCommands = 0, aggErrors = 0;
86
+ for (const w of ctx.workers) {
87
+ aggBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
88
+ aggCoins += w.stats.coins || 0;
89
+ aggCommands += w.stats.commands || 0;
90
+ aggErrors += w.stats.errors || 0;
91
+ }
92
+ const successRate = aggCommands > 0 ? Math.round(((aggCommands - aggErrors) / aggCommands) * 100) : 100;
93
+
94
+ const elapsedHrs = (Date.now() - ctx.startTime) / 3_600_000;
95
+ const coinsPerHr = elapsedHrs > 0.01 ? Math.round(aggCoins / elapsedHrs) : 0;
96
+ const cpmVal = ctx.globalCmdRate.getRate().toFixed(1);
97
+
98
+ const activeCount = ctx.workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
99
+ const invalidCount = ctx.workers.filter(w => w._tokenInvalid).length;
100
+ const pausedCount = ctx.workers.filter(w => w.paused || w.dashboardPaused).length;
101
+ const recovCount = ctx.workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
102
+
103
+ const netQ = ctx.workers.length > 0
104
+ ? ctx.workers.reduce((s, w) => s + (w._lastPing < 200 ? 1 : w._lastPing < 500 ? 0.5 : 0), 0) / ctx.workers.length : 1;
105
+ const netDot = netQ > 0.8 ? `${G}●${_}` : netQ > 0.5 ? `${Y}●${_}` : `${R}●${_}`;
106
+
107
+ const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
108
+ const spin = _getSpinner('braille');
109
+
110
+ // ── HEADER BOX ───────────────────────────────────────────────
111
+ const titleBar = `${_c.bold}${_gradientText(' DANK GRINDER ', [139, 92, 246], [52, 211, 153])}${_}`;
112
+ const versionTag = `${D}v${ctx.PKG_VERSION}${_}`;
113
+ const modeTag = ctx.CLOUD_MODE ? `${Cy}CLOUD${_}` : (ctx.CLUSTER_ENABLED ? `${Cy}CLUSTER${_}` : `${D}local${_}`);
114
+
115
+ // Top border
116
+ rows.push(`${A}${BOX.dtl}${BOX.dh.repeat(tw - 2)}${BOX.dtr}${_}`);
117
+
118
+ // Main header line
119
+ const h1 = [
120
+ titleBar, versionTag, spin,
121
+ `${D}up${_} ${W}${fmtUptime(ctx.startTime)}${_}`,
122
+ netDot, `${D}net${_}`,
123
+ `${Au}⏣${_}${W}${fmtCoins(aggBalance)}${_} ${D}bal${_}`,
124
+ `${G}${coinsPerHr >= 0 ? '+' : ''}⏣${fmtCoins(coinsPerHr)}${_}/h`,
125
+ `${G}${activeCount}${_}/${W}${ctx.workers.length}${_} ${D}active${_}`,
126
+ invalidCount > 0 ? ` ${R}${invalidCount} inv${_}` : '',
127
+ pausedCount > 0 ? ` ${Y}${pausedCount} pause${_}` : '',
128
+ recovCount > 0 ? ` ${O}${recovCount} recov${_}` : '',
129
+ ` ${D}${aggCommands}${_} cmds`,
130
+ ` ${D}${cpmVal}${_}/min`,
131
+ ` ${D}mem${_} ${memMB}MB`,
132
+ modeTag,
133
+ ].filter(Boolean).join(' ');
134
+
135
+ rows.push(`${A}${BOX.dv}${_} ${pad(h1, tw - 4)} ${A}${BOX.dv}${_}`);
136
+
137
+ // Divider
138
+ rows.push(`${A}${BOX.teeD}${BOX.dh.repeat(tw - 2)}${BOX.teeU}${_}`);
139
+
140
+ // Stats row
141
+ const statsRow = [
142
+ `${G}${fmtCoins(aggCoins)}${_} ${D}coins${_}`,
143
+ `${G}${successRate}%${_} ${D}success${_}`,
144
+ `${W}${fmtCoins(sessionPeakCoins)}${_} ${D}peak${_}`,
145
+ ctx.isNewHigh ? `${Au}★ NEW HIGH${_}` : '',
146
+ ].filter(Boolean).join(` ${D}│${_} `);
147
+
148
+ const statLine = ` ${D}session${_} ${statsRow}`;
149
+ rows.push(`${A}${BOX.dv}${_}${pad(statLine, tw - 4)}${A}${BOX.dv}${_}`);
150
+
151
+ // Bottom header border
152
+ rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
153
+
154
+ // ── ACCOUNTS TABLE ──────────────────────────────────────────
155
+ const colNum = 4;
156
+ const colSts = 3;
157
+ const colName = Math.max(14, Math.min(22, Math.floor(tw * 0.20)));
158
+ const colBal = 10;
159
+ const colLvl = 5;
160
+ const colLs = 4;
161
+ const colEarn = 9;
162
+ const colAct = Math.max(6, tw - colNum - colSts - colName - colBal - colLvl - colLs - colEarn - 18);
163
+ const gap = 2;
164
+ const colGap = ' '.repeat(gap);
165
+
166
+ // Column header
167
+ const headers = [
168
+ `${D}${pad('#', colNum)}${_}`,
169
+ `${D}${pad('S', colSts)}${_}`,
170
+ `${_gradientText(pad('Account', colName), [139, 92, 246], [96, 165, 250])}${_}`,
171
+ `${D}${pad('Balance', colBal)}${_}`,
172
+ `${D}${pad('Lvl', colLvl)}${_}`,
173
+ `${D}${pad('LS', colLs)}${_}`,
174
+ `${D}${pad('Earned', colEarn)}${_}`,
175
+ `${D}${pad('Activity', colAct)}${_}`,
176
+ ].join(colGap);
177
+
178
+ rows.push(`${A}${BOX.dtl}${BOX.dh.repeat(tw - 2)}${BOX.dtr}${_}`);
179
+ rows.push(`${A}${BOX.dv}${_} ${headers} ${A}${BOX.dv}${_}`);
180
+ rows.push(`${A}${BOX.teeD}${BOX.h.repeat(tw - 2)}${BOX.teeU}${_}`);
181
+
182
+ const sorted = [...ctx.workers].sort((a, b) => a.idx - b.idx);
183
+ const maxRows = Math.max(4, Math.min(sorted.length, Math.floor((process.stdout.rows || 24) - 14)));
184
+ const visible = sorted.slice(0, maxRows);
185
+
186
+ for (const wk of visible) {
187
+ const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
188
+
189
+ let stsIcon;
190
+ if (wk._tokenInvalid) stsIcon = `${R}✗${_}`;
191
+ else if (!wk.running) stsIcon = `${D}○${_}`;
192
+ else if (isRecov) stsIcon = `${O}${_getSpinner('braille').substring(0, 1)}${_}`;
193
+ else if (wk.paused) stsIcon = `${R}⏸${_}`;
194
+ else if (wk.dashboardPaused) stsIcon = `${Y}⏸${_}`;
195
+ else if (wk.busy) stsIcon = `${G}${_getSpinner('pulse').substring(0, 1)}${_}`;
196
+ else stsIcon = `${G}●${_}`;
197
+
198
+ const name = (wk.username || wk.account.label || '?').substring(0, colName);
199
+ const nameStr = `${wk.color}${name}${_}`;
200
+ const balStr = wk.stats.balance > 0 ? `${Au}⏣${_}${W}${fmtCoins(wk.stats.balance)}${_}` : `${D}⏣-${_}`;
201
+ const lvl = wk._level || 0;
202
+ const lvlStr = lvl > 0 ? `${Cy}L${lvl}${_}` : `${D}L???${_}`;
203
+ const ls = wk._lifesavers;
204
+ let lsStr;
205
+ if (ls === 0) lsStr = `${R}♥${ls}${_}`;
206
+ else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${_}`;
207
+ else if (ls != null) lsStr = `${G}♥${ls}${_}`;
208
+ else {
209
+ const p = ctx.PULSE_CHARS[Math.floor(Date.now() / 400) % ctx.PULSE_CHARS.length];
210
+ lsStr = `${D}${p}♥?${_}`;
211
+ }
212
+
213
+ const earn = wk.stats.coins || 0;
214
+ const earnStr = earn > 0 ? `${G}+${fmtCoins(earn)}${_}` : `${D}────${_}`;
215
+ const actRaw = (wk.lastStatus || 'ready').replace(/\x1b\[[0-9;]*m/g, '').substring(0, colAct);
216
+ const actStr = `${D}${pad(actRaw, colAct)}${_}`;
217
+ const numStr = `${D}${pad(String(wk.idx + 1), colNum)}${_}`;
218
+
219
+ const row = [
220
+ numStr, pad(stsIcon, colSts), pad(nameStr, colName),
221
+ pad(balStr, colBal), pad(lvlStr, colLvl), pad(lsStr, colLs),
222
+ pad(earnStr, colEarn), actStr,
223
+ ].join(colGap);
224
+
225
+ rows.push(`${A}${BOX.dv}${_} ${pad(row, tw - 4)} ${A}${BOX.dv}${_}`);
226
+ }
227
+
228
+ if (sorted.length > maxRows) {
229
+ const rest = sorted.length - maxRows;
230
+ rows.push(`${A}${BOX.dv}${_} ${D}+${rest} more${_}${' '.repeat(Math.max(0, tw - 18))}${A}${BOX.dv}${_}`);
231
+ }
232
+
233
+ rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
234
+
235
+ // ── LIVE FEED ────────────────────────────────────────────────
236
+ const logs = ctx.recentLogs.toArray();
237
+ if (logs.length > 0) {
238
+ const feedTitle = `${_gradientText(' LIVE FEED ', [139, 92, 246], [52, 211, 153])}${_} ${G}${_getSpinner('pulse')}${_}`;
239
+
240
+ rows.push(`${A}${BOX.dtl}${BOX.dh.repeat(tw - 2)}${BOX.dtr}${_}`);
241
+ rows.push(`${A}${BOX.dv}${_} ${pad(feedTitle, tw - 4)} ${A}${BOX.dv}${_}`);
242
+ rows.push(`${A}${BOX.teeD}${BOX.h.repeat(tw - 2)}${BOX.teeU}${_}`);
243
+
244
+ for (const entry of logs) {
245
+ let lineText;
246
+ if (typeof entry === 'string') {
247
+ lineText = entry;
248
+ } else if (entry && typeof entry === 'object') {
249
+ const ts = entry.ts ? new Date(entry.ts).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
250
+ const user = entry.username ? String(entry.username) : '';
251
+ const cmd = entry.command ? `[${entry.command}]` : '';
252
+ const resp = entry.response || '';
253
+ lineText = `${D}${ts}${_} ${entry.color || D}${user}${_} ${D}${cmd}${_} ${resp}`;
254
+ } else {
255
+ lineText = String(entry);
256
+ }
257
+ rows.push(`${A}${BOX.dv}${_} ${D}${pad(lineText, tw - 4)}${_} ${A}${BOX.dv}${_}`);
258
+ }
259
+ rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
260
+ }
261
+
262
+ // ── FOOTER ───────────────────────────────────────────────────
263
+ const footerLine = `${_c.dim}P=pause R=resume S=status Q=quit${_}`;
264
+ const modeLabel = ctx.CLOUD_MODE ? `${Cy}☁ CLOUD${_}` : (ctx.CLUSTER_ENABLED ? `${Cy}⎔ CLUSTER${_}` : `${D}○ local${_}`);
265
+ const footerFull = `${modeLabel} ${footerLine}`;
266
+
267
+ rows.push(`${A}${BOX.dtl}${BOX.dh.repeat(tw - 2)}${BOX.dtr}${_}`);
268
+ rows.push(`${A}${BOX.dv}${_} ${pad(footerFull, tw - 4)} ${A}${BOX.dv}${_}`);
269
+ rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
270
+
271
+ // ── Flush ────────────────────────────────────────────────────
272
+ process.stdout.write('\x1b[H');
273
+ for (const row of rows) {
274
+ process.stdout.write(`${_c.clearLine}\r${row}\n`);
275
+ }
276
+ const leftover = ctx.dashboardLines - rows.length;
277
+ if (leftover > 0) {
278
+ process.stdout.write(`\x1b[${leftover}B\x1b[2K`);
279
+ }
280
+
281
+ return rows.length;
282
+ }
283
+
284
+ module.exports = { renderDashboard };
package/lib/grinder.js CHANGED
@@ -3,6 +3,7 @@ const Redis = require('ioredis');
3
3
  const commands = require('./commands');
4
4
  const { setDashboardActive, isCV2, ensureCV2, stripAnsi } = require('./commands/utils');
5
5
  const rawLogger = require('./rawLogger');
6
+ const { renderDashboard: renderDashboardImpl } = require('./dashboard');
6
7
  const {
7
8
  BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
8
9
  AhoCorasick, LRUCache, StringPool, AsyncBatchQueue, JitterBackoff,
@@ -375,372 +376,40 @@ function scheduleRender() {
375
376
  }
376
377
  }
377
378
 
379
+ // ── Dashboard ──────────────────────────────────────────────────────────────────
380
+ // Thin wrapper: aggregates stats then delegates to ./dashboard.js
378
381
  function renderDashboard() {
379
382
  if (!dashboardStarted || workers.length === 0 || dashboardRendering || shutdownCalled) return;
380
383
  dashboardRendering = true;
381
384
  lastRenderTime = Date.now();
382
385
 
386
+ // Aggregate session totals
383
387
  totalBalance = 0; totalCoins = 0; totalCommands = 0;
384
388
  let totalErrors = 0;
385
- let totalLosses = 0;
386
389
  for (const w of workers) {
387
390
  totalBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
388
- totalCoins += w.stats.coins || 0;
391
+ totalCoins += w.stats.coins || 0;
389
392
  totalCommands += w.stats.commands || 0;
390
- totalErrors += w.stats.errors || 0;
391
- totalLosses += w.stats.losses || 0;
393
+ totalErrors += w.stats.errors || 0;
392
394
  }
393
- const successRate = totalCommands > 0 ? Math.round(((totalCommands - totalErrors) / totalCommands) * 100) : 100;
394
395
  if (totalCoins > sessionPeakCoins) {
395
396
  sessionPeakCoins = totalCoins;
396
397
  isNewHigh = true;
397
398
  setTimeout(() => { isNewHigh = false; }, 3000);
398
399
  }
399
400
 
400
- // ── Layout: use FULL terminal width ──
401
- const lines = [];
402
- const tw = Math.max(process.stdout.columns || 80, 60);
403
- const iw = tw - 4; // inner width (inside box borders)
404
-
405
- // Color shortcuts (local to this function)
406
- const A = rgb(139, 92, 246); // accent purple
407
- const G = rgb(52, 211, 153); // green
408
- const B = rgb(96, 165, 250); // blue
409
- const P = rgb(236, 72, 153); // pink
410
- const Au = rgb(255, 215, 0); // gold
411
- const O = rgb(251, 146, 60); // orange
412
- const Cy = rgb(34, 211, 238); // cyan
413
- const R = rgb(239, 68, 68); // red
414
- const Y = rgb(251, 191, 36); // yellow
415
- const W = c.white;
416
- const D = c.dim;
417
- const RE = /\x1b\[[0-9;]*m/g;
418
-
419
- // ── Box drawing (scales to tw) ──
420
- const bTop = A + '╔' + '═'.repeat(tw - 2) + '╗' + c.reset;
421
- const bMid = A + '╟' + '─'.repeat(tw - 2) + '╢' + c.reset;
422
- const bSep = A + '╠' + '═'.repeat(tw - 2) + '╣' + c.reset;
423
- const bBot = A + '╚' + '═'.repeat(tw - 2) + '╝' + c.reset;
424
- const bRow = (content) => {
425
- const vis = content.replace(RE, '').length;
426
- const pad = Math.max(0, iw - vis);
427
- return A + '║' + c.reset + ' ' + content + ' '.repeat(pad) + ' ' + A + '║' + c.reset;
428
- };
429
- const bEmpty = bRow('');
430
-
431
- // ═══════════════════════════════════════════════════════════════
432
- // HEADER
433
- // ═══════════════════════════════════════════════════════════════
434
- lines.push(bTop);
435
- lines.push(bEmpty);
436
-
437
- const spin = getSpinner('braille');
438
-
439
- // Title — big gradient banner
440
- const titleLines = [
441
- '██████╗ █████╗ ███╗ ██╗██╗ ██╗ ██████╗ ██████╗ ██╗███╗ ██╗██████╗ ███████╗██████╗',
442
- '██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝ ██╔════╝ ██╔══██╗██║████╗ ██║██╔══██╗██╔════╝██╔══██╗',
443
- '██║ ██║███████║██╔██╗ ██║█████╔╝ ██║ ███╗██████╔╝██║██╔██╗ ██║██║ ██║█████╗ ██████╔╝',
444
- '██║ ██║██╔══██║██║╚██╗██║██╔═██╗ ██║ ██║██╔══██╗██║██║╚██╗██║██║ ██║██╔══╝ ██╔══██╗',
445
- '██████╔╝██║ ██║██║ ╚████║██║ ██╗ ╚██████╔╝██║ ██║██║██║ ╚████║██████╔╝███████╗██║ ██║',
446
- '╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝',
447
- ];
448
- // Check terminal width — fall back to compact title if too narrow
449
- const termW = (process.stdout.columns || 120) - 6; // account for box borders
450
- const useBigTitle = termW >= 92;
451
- if (useBigTitle) {
452
- for (let i = 0; i < titleLines.length; i++) {
453
- const t = i / (titleLines.length - 1);
454
- const from = t < 0.5
455
- ? [lerp(192, 139, t * 2), lerp(132, 92, t * 2), lerp(252, 246, t * 2)]
456
- : [lerp(139, 34, (t - 0.5) * 2), lerp(92, 211, (t - 0.5) * 2), lerp(246, 238, (t - 0.5) * 2)];
457
- lines.push(bRow(` ${c.bold}${gradientLine(titleLines[i], from, [52, 211, 153])}${c.reset}`));
458
- }
459
- } else {
460
- const titleGrad = gradientText(' D A N K G R I N D E R ', [192, 132, 252], [52, 211, 153]);
461
- lines.push(bRow(` ${c.bold}${titleGrad}${c.reset} ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset}`));
462
- }
463
-
464
- lines.push(bRow(` ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset} ${Y}◷${c.reset} ${D}UP${c.reset} ${c.bold}${Y}${formatUptime()}${c.reset}`));
465
-
466
- // Subtitle info
467
- const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
468
- const invalidCount = workers.filter(w => w._tokenInvalid).length;
469
- const pausedCount = workers.filter(w => w.paused || w.dashboardPaused).length;
470
- const recovCount = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
471
- const mode = CLOUD_MODE ? `${Cy}CLOUD${c.reset}` : (CLUSTER_ENABLED ? `${Cy}CLUSTER${c.reset}` : `${D}Standalone${c.reset}`);
472
- const cmdCount = AccountWorker.COMMAND_MAP.length;
473
-
474
- const netQ = workers.length > 0
475
- ? workers.reduce((s, w) => s + (w._lastPing < 200 ? 1 : w._lastPing < 500 ? 0.5 : 0), 0) / workers.length : 1;
476
- const netDot = netQ > 0.8 ? `${G}●${c.reset}` : netQ > 0.5 ? `${Y}●${c.reset}` : `${R}●${c.reset}`;
477
- const netLbl = netQ > 0.8 ? `${G}Good${c.reset}` : netQ > 0.5 ? `${Y}Fair${c.reset}` : `${R}Poor${c.reset}`;
478
-
479
- const infoParts = [
480
- mode,
481
- `${netDot} ${netLbl}`,
482
- `${B}${cmdCount}${c.reset} ${D}commands${c.reset}`,
483
- `${G}${activeCount}${c.reset}${D}/${c.reset}${W}${workers.length}${c.reset} ${D}active${c.reset}`,
484
- ];
485
- if (invalidCount > 0) infoParts.push(`${R}${invalidCount} invalid${c.reset}`);
486
- if (pausedCount > 0) infoParts.push(`${Y}${pausedCount} paused${c.reset}`);
487
- if (recovCount > 0) infoParts.push(`${O}${recovCount} recovering${c.reset}`);
488
- lines.push(bRow(` ${infoParts.join(` ${D}·${c.reset} `)}`));
489
-
490
- lines.push(bEmpty);
491
-
492
- // ═══════════════════════════════════════════════════════════════
493
- // STATS PANEL — left: all metrics | right: big trend + rate
494
- // ═══════════════════════════════════════════════════════════════
495
- lines.push(bSep);
496
- lines.push(bEmpty);
497
-
498
- const now = Date.now();
499
- if (now - lastEarningsSample > 8000) { earningsHistory.push(totalCoins); lastEarningsSample = now; }
500
- const elapsedHrs = (Date.now() - startTime) / 3_600_000;
501
- const perHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
502
-
503
- // ── Compute metric values ─────────────────────────────────────
504
- const cpmVal = globalCmdRate.getRate().toFixed(1);
505
- const srColor = successRate >= 95 ? G : successRate >= 80 ? Y : R;
506
- const srBar = progressBar(successRate, 100, 10, successRate >= 95 ? [52, 211, 153] : successRate >= 80 ? [251, 191, 36] : [239, 68, 68]);
507
- const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
508
- const memCol = memMB > 900 ? [239, 68, 68] : memMB > 600 ? [251, 191, 36] : [52, 211, 153];
509
- const memBar = progressBar(memMB, 1024, 10, memCol, [40, 40, 55]);
510
- const perHrColor = perHr >= 0 ? G : R;
511
- const perHrSign = perHr >= 0 ? '+' : '';
512
- const newHighFlag = isNewHigh ? ` ${R}${c.bold}★ NEW HIGH ★${c.reset}` : '';
513
-
514
- // ── Big trend sparkline (takes ~40% of inner width) ─────────
515
- const sparkW = Math.max(28, Math.floor(iw * 0.4));
516
- const spark = drawSparkline(earningsHistory.toArray(), sparkW);
517
- const sparkLabel = `${A}~${c.reset} ${D}TREND${c.reset}`;
518
-
519
- // ── Left metric rows (each left-aligned, ANSI-aware padding) ─
520
- // Helper: ANSI-strip-aware pad — strip ANSI then pad the visible content
521
- const padRow = (content, totalVis) => {
522
- const vis = content.replace(RE, '').length;
523
- return content + ' '.repeat(Math.max(0, totalVis - vis));
524
- };
525
-
526
- const leftTotal = iw - sparkW - 10; // reserve space for spark + gap
527
- const lRow1 = `${Au}⟐${c.reset} ${D}BALANCE${c.reset} ${c.bold}${Au}⏣${c.reset} ${formatCoins(totalBalance)}`;
528
- const lRow2 = `${G}▲${c.reset} ${D}EARNED${c.reset} ${c.bold}${G}${perHrSign}⏣${c.reset} ${formatCoins(totalCoins)}${newHighFlag}`;
529
- const lRow3 = `${O}★${c.reset} ${D}PEAK${c.reset} ${c.bold}${O}⏣${c.reset} ${formatCoins(sessionPeakCoins)}`;
530
- const lRow4 = `${B}◆${c.reset} ${D}CMDS${c.reset} ${c.bold}${B}${totalCommands}${c.reset} ${srColor}${successRate}%${c.reset} ${srBar} ${Cy}${cpmVal}${c.reset}${D}/min${c.reset}`;
531
- const lRow5 = `${D}≡${c.reset} ${D}MEM${c.reset} ${rgb(memCol[0], memCol[1], memCol[2])}${c.bold}${memMB}MB${c.reset} ${memBar}`;
532
-
533
- // Build right column label
534
- const rRate = `${perHrColor}${perHrSign}⏣${c.reset} ${formatCoins(Math.abs(perHr))}/h`;
535
-
536
- lines.push(bRow(` ${padRow(lRow1, leftTotal)} ${sparkLabel} ${spark}`));
537
- lines.push(bRow(` ${padRow(lRow2, leftTotal)} ${D}────────${c.reset} ${c.dim}earned${c.reset}`));
538
- lines.push(bRow(` ${padRow(lRow3, leftTotal)} ${D} ${c.reset} ${rRate}`));
539
- lines.push(bRow(` ${padRow(lRow4, leftTotal)} ${D} ${c.reset}`));
540
- lines.push(bRow(` ${padRow(lRow5, leftTotal)} ${D} ${c.reset}`));
541
-
542
- lines.push(bEmpty);
543
-
544
- // ═══════════════════════════════════════════════════════════════
545
- // ACCOUNTS TABLE (sorted by original index, proper alignment)
546
- // ═══════════════════════════════════════════════════════════════
547
- lines.push(bSep);
548
-
549
- // Column widths scale with terminal
550
- const colNum = 4; // " 1 "
551
- const colSts = 3; // "● "
552
- const colMedal = 4; // " 1st" or " "
553
- const colBal = 12; // "⏣ 999.9M "
554
- const colEarn = 10; // "+999.9K "
555
- const colBar = 8; // "████░░░░"
556
- const fixedW = colNum + colSts + colMedal + colBal + colEarn + colBar + 16; // 16 for spacing
557
- const colName = Math.max(10, Math.min(22, Math.floor((iw - fixedW) * 0.45)));
558
- const colActivity = Math.max(10, iw - fixedW - colName);
559
-
560
- // Header
561
- const hNum = `${D}${'#'.padStart(colNum)}${c.reset}`;
562
- const hSts = `${D}${'ST'.padEnd(colSts)}${c.reset}`;
563
- const hMedal = `${D}${'RNK'.padEnd(colMedal)}${c.reset}`;
564
- const hName = `${gradientText('Account', [139, 92, 246], [96, 165, 250])}${''.padEnd(Math.max(0, colName - 7))}`;
565
- const hBal = `${D}${'Balance'.padEnd(colBal)}${c.reset}`;
566
- const hEarn = `${D}${'Earned'.padEnd(colEarn)}${c.reset}`;
567
- const hBar = `${D}${''.padEnd(colBar)}${c.reset}`;
568
- const hAct = `${D}Activity${c.reset}`;
569
-
570
- lines.push(bRow(` ${hNum} ${hSts} ${hMedal} ${hName} ${hBal} ${hEarn} ${hBar} ${hAct}`));
571
- lines.push(bRow(` ${D}${'─'.repeat(iw - 2)}${c.reset}`));
572
-
573
- // Sort workers by original index for consistent display
574
- const sortedWorkers = [...workers].sort((a, b) => a.idx - b.idx);
575
-
576
- // Top 3 earners
577
- const topEarners = [...workers]
578
- .filter(w => (w.stats.coins || 0) > 0)
579
- .sort((a, b) => (b.stats.coins || 0) - (a.stats.coins || 0))
580
- .slice(0, 3);
581
- const topIds = new Map();
582
- topEarners.forEach((w, i) => topIds.set(w.account.id, i));
583
-
584
- const MAX_VIS = 30;
585
- const visibleWorkers = sortedWorkers.slice(0, MAX_VIS);
586
-
587
- for (const wk of visibleWorkers) {
588
- const origNum = (wk.idx + 1).toString().padStart(colNum);
589
- const rawStat = (wk.lastStatus || 'ready').replace(RE, '');
590
- const activityText = rawStat.substring(0, colActivity);
591
-
592
- // ── Status icon ──
593
- let stsIcon, actLabel;
594
- const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
595
- if (wk._tokenInvalid) {
596
- stsIcon = `${R}✗${c.reset}`;
597
- actLabel = `${R}TOKEN INVALID${c.reset}`;
598
- } else if (!wk.running) {
599
- stsIcon = `${D}○${c.reset}`;
600
- actLabel = `${D}offline${c.reset}`;
601
- } else if (isRecov) {
602
- const sL = Math.ceil((wk._errorCooldownUntil - Date.now()) / 1000);
603
- stsIcon = `${O}${getSpinner('braille')}${c.reset}`;
604
- actLabel = `${O}recovering (${sL}s)${c.reset}`;
605
- } else if (wk.paused) {
606
- stsIcon = `${R}⏸${c.reset}`;
607
- actLabel = `${R}PAUSED${c.reset}`;
608
- } else if (wk.dashboardPaused) {
609
- stsIcon = `${Y}⏸${c.reset}`;
610
- actLabel = `${Y}paused${c.reset}`;
611
- } else if (wk.busy) {
612
- stsIcon = `${G}${getSpinner('pulse')}${c.reset}`;
613
- actLabel = `${D}${activityText}${c.reset}`;
614
- } else {
615
- stsIcon = `${G}●${c.reset}`;
616
- actLabel = `${D}${activityText}${c.reset}`;
617
- }
618
-
619
- // ── Medal (fixed 3-char visible width + 1 space) ──
620
- let medalStr;
621
- if (topIds.has(wk.account.id)) {
622
- const rank = topIds.get(wk.account.id);
623
- if (rank === 0) medalStr = `${Au}1st${c.reset} `;
624
- else if (rank === 1) medalStr = `${rgb(192, 192, 192)}2nd${c.reset} `;
625
- else medalStr = `${rgb(205, 127, 50)}3rd${c.reset} `;
626
- } else {
627
- medalStr = ' '; // 4 chars: 3 empty + 1 space
628
- }
629
-
630
- // ── Name (fixed visible width, padded) ──
631
- const rawName = (wk.username || wk.account.label || '?').substring(0, colName);
632
- const nameStr = `${wk.color}${c.bold}${rawName.padEnd(colName)}${c.reset}`;
633
-
634
- // ── Balance (fixed visible width) ──
635
- let balStr;
636
- if (wk.stats.balance > 0) {
637
- balStr = `${Au}⏣${c.reset}${W}${formatCoins(wk.stats.balance).padStart(colBal - 2)}${c.reset}`;
638
- } else {
639
- balStr = `${D}⏣${' '.repeat(colBal - 3)}-${c.reset}`;
640
- }
641
-
642
- // ── Lifesaver indicator ──
643
- const ls = wk._lifesavers;
644
- let lsStr;
645
- if (ls === 0) lsStr = `${R}♥0${c.reset}`;
646
- else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${c.reset}`;
647
- else if (ls != null) lsStr = `${G}♥${ls}${c.reset}`;
648
- else {
649
- // Unknown — pulse to show it's still being determined
650
- const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
651
- lsStr = `${D}${pulse}♥?${c.reset}`;
652
- }
653
-
654
- // ── Level indicator (fixed width so value changes don't jitter) ──
655
- const lvl = wk._level || 0;
656
- const lvlStr = lvl > 0 ? `${Cy}L${String(lvl).padStart(3)}${c.reset}` : `${D}L???${c.reset}`;
657
-
658
- // ── Earned (fixed visible width) ──
659
- const earnNum = wk.stats.coins || 0;
660
- let earnStr;
661
- if (earnNum > 0) {
662
- earnStr = `${G}+${formatCoins(earnNum).padEnd(colEarn - 1)}${c.reset}`;
663
- } else if (earnNum < 0) {
664
- earnStr = `${R}${formatCoins(earnNum).padEnd(colEarn)}${c.reset}`;
665
- } else {
666
- earnStr = `${D}${'-'.padEnd(colEarn)}${c.reset}`;
667
- }
668
-
669
- // ── Progress bar (fixed width) ──
670
- const earnBarFill = earnNum > 0 ? Math.min(colBar, Math.max(1, Math.floor(Math.log10(earnNum + 1)))) : 0;
671
- const earnBar = progressBar(earnBarFill, colBar, colBar, [52, 211, 153], [40, 40, 55]);
672
-
673
- lines.push(bRow(` ${D}${origNum}${c.reset} ${stsIcon} ${medalStr}${nameStr} ${balStr} ${lvlStr} ${lsStr} ${earnStr} ${earnBar} ${actLabel}`));
674
- }
675
-
676
- // Overflow summary
677
- if (sortedWorkers.length > MAX_VIS) {
678
- const rest = sortedWorkers.length - MAX_VIS;
679
- let ha = 0, hp = 0, hr = 0, ho = 0, hi = 0;
680
- for (let i = MAX_VIS; i < sortedWorkers.length; i++) {
681
- const w = sortedWorkers[i];
682
- if (w._tokenInvalid) hi++;
683
- else if (!w.running) ho++;
684
- else if (w.paused || w.dashboardPaused) hp++;
685
- else if (w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()) hr++;
686
- else ha++;
687
- }
688
- const parts = [`${D}+${rest} more${c.reset}`];
689
- if (ha > 0) parts.push(`${G}${ha} active${c.reset}`);
690
- if (hp > 0) parts.push(`${Y}${hp} paused${c.reset}`);
691
- if (hr > 0) parts.push(`${O}${hr} recovering${c.reset}`);
692
- if (hi > 0) parts.push(`${R}${hi} invalid${c.reset}`);
693
- if (ho > 0) parts.push(`${D}${ho} offline${c.reset}`);
694
- lines.push(bRow(` ${parts.join(` ${D}·${c.reset} `)}`));
695
- }
696
-
697
- // ═══════════════════════════════════════════════════════════════
698
- // HEALTH BAR
699
- // ═══════════════════════════════════════════════════════════════
700
- const totalRecoveries = workers.reduce((s, w) => s + (w._totalRecoveries || 0), 0);
701
- const totalDisconnects = workers.reduce((s, w) => s + (w._disconnectCount || 0), 0);
702
- if (recovCount > 0 || pausedCount > 0 || invalidCount > 0 || totalRecoveries > 0 || totalDisconnects > 0) {
703
- lines.push(bMid);
704
- const hParts = [];
705
- if (invalidCount > 0) hParts.push(`${R}✗ ${invalidCount} invalid${c.reset}`);
706
- if (recovCount > 0) hParts.push(`${O}${getSpinner('braille')} ${recovCount} recovering${c.reset}`);
707
- if (pausedCount > 0) hParts.push(`${Y}⏸ ${pausedCount} paused${c.reset}`);
708
- if (totalRecoveries > 0) hParts.push(`${D}${totalRecoveries} auto-healed${c.reset}`);
709
- if (totalDisconnects > 0) hParts.push(`${D}${totalDisconnects} reconnects${c.reset}`);
710
- lines.push(bRow(` ${D}HEALTH${c.reset} ${hParts.join(` ${D}·${c.reset} `)}`));
711
- }
712
-
713
- // Cluster
714
- if (CLUSTER_ENABLED) {
715
- const nodeShort = NODE_ID.substring(0, 12);
716
- const claimedCount = workers.filter(w => w.running).length;
717
- lines.push(bRow(` ${Cy}CLUSTER${c.reset} ${D}node:${c.reset} ${Cy}${nodeShort}${c.reset} ${D}claimed:${c.reset} ${W}${claimedCount}${c.reset}`));
718
- }
719
-
720
- // ═══════════════════════════════════════════════════════════════
721
- // LIVE LOG FEED
722
- // ═══════════════════════════════════════════════════════════════
723
- const logEntries = recentLogs.toArray();
724
- if (logEntries.length > 0) {
725
- lines.push(bSep);
726
- lines.push(bRow(` ${gradientText('LIVE FEED', [139, 92, 246], [52, 211, 153])} ${G}${getSpinner('pulse')}${c.reset}`));
727
- lines.push(bMid);
728
- for (const entry of logEntries) {
729
- lines.push(bRow(` ${D}${entry}${c.reset}`));
730
- }
731
- }
732
-
733
- // ═══════════════════════════════════════════════════════════════
734
- lines.push(bEmpty);
735
- lines.push(bBot);
401
+ // Pass all state into the dashboard module
402
+ const newLines = renderDashboardImpl({
403
+ workers, dashboardStarted, dashboardRendering, dashboardLines,
404
+ totalBalance, totalCoins, totalCommands, startTime,
405
+ sessionPeakCoins, isNewHigh, recentLogs, globalCmdRate,
406
+ earningsHistory, lastEarningsSample,
407
+ CLOUD_MODE, CLUSTER_ENABLED, PKG_VERSION,
408
+ AccountWorker, PULSE_CHARS, getSpinner, gradientText,
409
+ rgb, c, BOX,
410
+ });
736
411
 
737
- // ── Flush to terminal ──
738
- process.stdout.write('\x1b[H');
739
- for (const line of lines) {
740
- process.stdout.write(c.clearLine + '\r' + line + '\n');
741
- }
742
- process.stdout.write('\x1b[J');
743
- dashboardLines = lines.length;
412
+ if (newLines != null) dashboardLines = newLines;
744
413
  dashboardRendering = false;
745
414
  }
746
415
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "7.62.0",
3
+ "version": "7.64.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"