dankgrinder 7.63.0 → 7.66.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,287 @@
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 || ctx.shutdownCalled) 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(ctx.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
+ // Clear entire screen buffer first so old startup logs don't bleed through
273
+ process.stdout.write('\x1b[2J\x1b[H');
274
+ process.stdout.write(_c.hide);
275
+ for (const row of rows) {
276
+ process.stdout.write(`${_c.clearLine}\r${row}\n`);
277
+ }
278
+ process.stdout.write(_c.show);
279
+ const leftover = ctx.dashboardLines - rows.length;
280
+ if (leftover > 0) {
281
+ process.stdout.write(`\x1b[${leftover}B\x1b[2K`);
282
+ }
283
+
284
+ return rows.length;
285
+ }
286
+
287
+ 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,209 +376,40 @@ function scheduleRender() {
375
376
  }
376
377
  }
377
378
 
378
- // ══════════════════════════════════════════════════════════════
379
- // ═ CLI Dashboard clean, modern, stable rendering
380
- // ═ Structure: [3-line header] [accounts table] [live feed] [footer]
381
- // ║ Uses full-width box borders, ANSI cursor positioning,
382
- // ═ per-row ANSI-aware padding for stable display.
383
- // ══════════════════════════════════════════════════════════════
379
+ // ── Dashboard ──────────────────────────────────────────────────────────────────
380
+ // Thin wrapper: aggregates stats then delegates to ./dashboard.js
384
381
  function renderDashboard() {
385
382
  if (!dashboardStarted || workers.length === 0 || dashboardRendering || shutdownCalled) return;
386
383
  dashboardRendering = true;
387
384
  lastRenderTime = Date.now();
388
385
 
389
- // ── Aggregate session stats ─────────────────────────────────────
386
+ // Aggregate session totals
390
387
  totalBalance = 0; totalCoins = 0; totalCommands = 0;
391
388
  let totalErrors = 0;
392
- let totalLosses = 0;
393
389
  for (const w of workers) {
394
390
  totalBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
395
- totalCoins += w.stats.coins || 0;
391
+ totalCoins += w.stats.coins || 0;
396
392
  totalCommands += w.stats.commands || 0;
397
- totalErrors += w.stats.errors || 0;
398
- totalLosses += w.stats.losses || 0;
393
+ totalErrors += w.stats.errors || 0;
399
394
  }
400
- const successRate = totalCommands > 0 ? Math.round(((totalCommands - totalErrors) / totalCommands) * 100) : 100;
401
395
  if (totalCoins > sessionPeakCoins) {
402
396
  sessionPeakCoins = totalCoins;
403
397
  isNewHigh = true;
404
398
  setTimeout(() => { isNewHigh = false; }, 3000);
405
399
  }
406
400
 
407
- const tw = Math.max(process.stdout.columns || 80, 60);
408
- const RE = /\x1b\[[0-9;]*m/g;
409
- const vis = (s) => String(s).replace(RE, '').length;
410
- const pad = (content, width) => content + ' '.repeat(Math.max(0, width - vis(content)));
411
-
412
- // Color shortcuts
413
- const A = rgb(139, 92, 246); // purple accent
414
- const G = rgb(52, 211, 153); // green
415
- const B = rgb(96, 165, 250); // blue
416
- const Au = rgb(255, 215, 0); // gold
417
- const O = rgb(251, 146, 60); // orange
418
- const Cy = rgb(34, 211, 238); // cyan
419
- const R = rgb(239, 68, 68); // red
420
- const Y = rgb(251, 191, 36); // yellow
421
- const W = c.white;
422
- const D = c.dim;
423
- const _ = c.reset;
424
-
425
- // ── Build rows array ──────────────────────────────────────────
426
- const rows = [];
427
-
428
- // ── HEADER ────────────────────────────────────────────────────
429
- const spin = getSpinner('braille');
430
- const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
431
- const invalidCount = workers.filter(w => w._tokenInvalid).length;
432
- const pausedCount = workers.filter(w => w.paused || w.dashboardPaused).length;
433
- const recovCount = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
434
- const netQ = workers.length > 0
435
- ? workers.reduce((s, w) => s + (w._lastPing < 200 ? 1 : w._lastPing < 500 ? 0.5 : 0), 0) / workers.length : 1;
436
- const netDot = netQ > 0.8 ? `${G}●${_}` : netQ > 0.5 ? `${Y}●${_}` : `${R}●${_}`;
437
- const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
438
- const elapsedHrs = (Date.now() - startTime) / 3_600_000;
439
- const perHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
440
- const cpmVal = globalCmdRate.getRate().toFixed(1);
441
- const now = Date.now();
442
- if (now - lastEarningsSample > 8000) { earningsHistory.push(totalCoins); lastEarningsSample = now; }
443
-
444
- const topLine = `${A}┌${'─'.repeat(tw - 2)}┐${_}`;
445
- rows.push(topLine);
446
-
447
- const title = `${c.bold}${gradientText(' DANK GRINDER ', [139, 92, 246], [52, 211, 153])}${_}`;
448
- const modeLabel = CLOUD_MODE ? `${Cy}CLOUD${_}` : (CLUSTER_ENABLED ? `${Cy}CLUSTER${_}` : `${D}local${_}`);
449
- const row1Content = `${title} ${D}v${PKG_VERSION}${_} ${G}${spin}${_}` +
450
- ` ${D}up${_} ${Y}${formatUptime()}${_}` +
451
- ` ${netDot} ${D}net${_}` +
452
- ` ${Au}⏣${_}${W}${formatCoins(totalBalance)}${_} ${D}bal${_}` +
453
- ` ${G}${perHr >= 0 ? '+' : ''}⏣${formatCoins(perHr)}${_}/h${_}` +
454
- ` ${G}${activeCount}${_}/${W}${workers.length}${_} ${D}active${_}` +
455
- (invalidCount > 0 ? ` ${R}${invalidCount} invalid${_}` : '') +
456
- (pausedCount > 0 ? ` ${Y}${pausedCount} paused${_}` : '') +
457
- (recovCount > 0 ? ` ${O}${recovCount} recov${_}` : '') +
458
- ` ${D}${totalCommands}${_} cmds` +
459
- ` ${D}mem ${memMB}MB${_}`;
460
- rows.push(`${A}│${_} ${pad(row1Content, tw - 4)} ${A}│${_}`);
461
- rows.push(`${A}└${'─'.repeat(tw - 2)}┘${_}`);
462
-
463
- // ── ACCOUNTS TABLE ────────────────────────────────────────────
464
- // Columns: #, S, Name, Balance, Lvl, LS, Earned, Activity
465
- const colNum = 3; // "#1"
466
- const colSts = 2; // "●"
467
- const colName = Math.max(14, Math.min(20, Math.floor(tw * 0.22)));
468
- const colBal = 9; // "⏣ 1.2M"
469
- const colLvl = 5; // "L273"
470
- const colLs = 4; // "♥37"
471
- const colEarn = 8; // "+500K"
472
- const colAct = Math.max(6, tw - colNum - colSts - colName - colBal - colLvl - colLs - colEarn - 16);
473
- const gap = 2;
474
- const colGap = ' '.repeat(gap);
475
-
476
- const hNum = `${D}${pad('#', colNum)}${_}`;
477
- const hSts = `${D}${pad('S', colSts)}${_}`;
478
- const hName = `${gradientText(pad('Account', colName), [139, 92, 246], [96, 165, 250])}${_}`;
479
- const hBal = `${D}${pad('Balance', colBal)}${_}`;
480
- const hLvl = `${D}${pad('Lvl', colLvl)}${_}`;
481
- const hLs = `${D}${pad('LS', colLs)}${_}`;
482
- const hEarn = `${D}${pad('Earned', colEarn)}${_}`;
483
- const hAct = `${D}${pad('Activity', colAct)}${_}`;
484
-
485
- rows.push(`${A}┌${'─'.repeat(tw - 2)}┐${_}`);
486
- rows.push(`${A}│${_} ${hNum}${colGap}${hSts}${colGap}${hName}${colGap}${hBal}${colGap}${hLvl}${colGap}${hLs}${colGap}${hEarn}${colGap}${hAct} ${A}│${_}`);
487
- rows.push(`${A}├${'─'.repeat(tw - 2)}┤${_}`);
488
-
489
- const sortedWorkers = [...workers].sort((a, b) => a.idx - b.idx);
490
- const topEarners = [...workers]
491
- .filter(w => (w.stats.coins || 0) > 0)
492
- .sort((a, b) => (b.stats.coins || 0) - (a.stats.coins || 0))
493
- .slice(0, 3);
494
- const topIds = new Map();
495
- topEarners.forEach((w, i) => topIds.set(w.account.id, i));
496
-
497
- // Adapt row count to available terminal height
498
- const maxRows = Math.max(4, Math.min(sortedWorkers.length, Math.floor((process.stdout.rows || 24) - 14)));
499
- const visibleWorkers = sortedWorkers.slice(0, maxRows);
500
-
501
- for (const wk of visibleWorkers) {
502
- const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
503
- let stsIcon;
504
- if (wk._tokenInvalid) stsIcon = `${R}✗${_}`;
505
- else if (!wk.running) stsIcon = `${D}○${_}`;
506
- else if (isRecov) stsIcon = `${O}${getSpinner('braille').substring(0, 1)}${_}`;
507
- else if (wk.paused) stsIcon = `${R}⏸${_}`;
508
- else if (wk.dashboardPaused) stsIcon = `${Y}⏸${_}`;
509
- else if (wk.busy) stsIcon = `${G}${getSpinner('pulse').substring(0, 1)}${_}`;
510
- else stsIcon = `${G}●${_}`;
511
-
512
- const rawName = (wk.username || wk.account.label || '?').substring(0, colName);
513
- const nameStr = `${wk.color}${rawName}${_}`;
514
- const balStr = wk.stats.balance > 0 ? `${Au}⏣${_}${W}${formatCoins(wk.stats.balance)}${_}` : `${D}⏣-${_}`;
515
- const lvl = wk._level || 0;
516
- const lvlStr = lvl > 0 ? `${Cy}L${lvl}${_}` : `${D}L???${_}`;
517
- const ls = wk._lifesavers;
518
- let lsStr;
519
- if (ls === 0) lsStr = `${R}♥${ls}${_}`;
520
- else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${_}`;
521
- else if (ls != null) lsStr = `${G}♥${ls}${_}`;
522
- else {
523
- const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
524
- lsStr = `${D}${pulse}♥?${_}`;
525
- }
526
- const earnNum = wk.stats.coins || 0;
527
- const earnStr = earnNum > 0 ? `${G}+${formatCoins(earnNum)}${_}` : `${D}────${_}`;
528
- const rawAct = (wk.lastStatus || 'ready').replace(RE, '').substring(0, colAct);
529
- const actStr = `${D}${pad(rawAct, colAct)}${_}`;
530
- const numStr = `${D}${pad(String(wk.idx + 1), colNum)}${_}`;
531
-
532
- rows.push(`${A}│${_} ${numStr}${colGap}${pad(stsIcon, colSts)}${colGap}${pad(nameStr, colName)}${colGap}${pad(balStr, colBal)}${colGap}${pad(lvlStr, colLvl)}${colGap}${pad(lsStr, colLs)}${colGap}${pad(earnStr, colEarn)}${colGap}${actStr} ${A}│${_}`);
533
- }
534
-
535
- if (sortedWorkers.length > maxRows) {
536
- const rest = sortedWorkers.length - maxRows;
537
- rows.push(`${A}│${_} ${D}+${rest} more accounts${_}${' '.repeat(Math.max(0, tw - 22 - rest.toString().length))}${A}│${_}`);
538
- }
539
- rows.push(`${A}└${'─'.repeat(tw - 2)}┘${_}`);
540
-
541
- // ── LIVE FEED ─────────────────────────────────────────────────
542
- const logEntries = recentLogs.toArray();
543
- if (logEntries.length > 0) {
544
- rows.push(`${A}┌${'─'.repeat(tw - 2)}┐${_}`);
545
- rows.push(`${A}│${_} ${gradientText(' LIVE FEED ', [139, 92, 246], [52, 211, 153])}${_} ${G}${getSpinner('pulse')}${_}${' '.repeat(Math.max(0, tw - 22))}${A}│${_}`);
546
- rows.push(`${A}├${'─'.repeat(tw - 2)}┤${_}`);
547
- for (const entry of logEntries) {
548
- let lineText;
549
- if (typeof entry === 'string') {
550
- lineText = entry;
551
- } else if (entry && typeof entry === 'object') {
552
- const ts = entry.ts ? new Date(entry.ts).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
553
- const user = entry.username ? String(entry.username) : '';
554
- const cmd = entry.command ? `[${entry.command}]` : '';
555
- const resp = entry.response || '';
556
- lineText = `${D}${ts}${_} ${entry.color || D}${user}${_} ${D}${cmd}${_} ${resp}`;
557
- } else {
558
- lineText = String(entry);
559
- }
560
- rows.push(`${A}│${_} ${D}${pad(lineText, tw - 4)}${_} ${A}│${_}`);
561
- }
562
- rows.push(`${A}└${'─'.repeat(tw - 2)}┘${_}`);
563
- }
564
-
565
- // ── FOOTER ────────────────────────────────────────────────────
566
- rows.push(`${A}┌${'─'.repeat(tw - 2)}┐${_}`);
567
- rows.push(`${A}│${_} ${modeLabel} ${D}P=pause R=resume S=status Q=quit${_}${' '.repeat(Math.max(0, tw - 48))}${A}│${_}`);
568
- rows.push(`${A}└${'─'.repeat(tw - 2)}┘${_}`);
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
+ });
569
411
 
570
- // ── Flush ─────────────────────────────────────────────────────
571
- process.stdout.write('\x1b[H');
572
- for (const row of rows) {
573
- process.stdout.write(c.clearLine + '\r' + row + '\n');
574
- }
575
- // Erase any leftover lines from previous render
576
- const clearDown = Math.max(0, dashboardLines - rows.length);
577
- if (clearDown > 0) {
578
- process.stdout.write(`\x1b[${clearDown}B\x1b[2K`);
579
- }
580
- dashboardLines = rows.length;
412
+ if (newLines != null) dashboardLines = Math.max(dashboardLines, newLines);
581
413
  dashboardRendering = false;
582
414
  }
583
415
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "7.63.0",
3
+ "version": "7.66.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"