dankgrinder 8.48.0 → 8.51.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.
Files changed (3) hide show
  1. package/lib/grinder.js +4 -2
  2. package/lib/ui.js +121 -104
  3. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -2932,7 +2932,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2932
2932
  ui.log(-1, `Starting ${activeWorkers.length} grind loops...`);
2933
2933
  ui.setLive(true);
2934
2934
  ui.draw();
2935
- ui.startRefresh(3000);
2935
+ ui.startRefresh();
2936
2936
  for (const w of activeWorkers) {
2937
2937
  if (!shutdownCalled) w.grindLoop();
2938
2938
  }
@@ -3050,10 +3050,12 @@ async function start(apiKey, apiUrl, opts = {}) {
3050
3050
  const webhookMsg = `+⏣ ${finalCoins.toLocaleString()} | ${finalCmds} cmds | ${formatUptime()}` +
3051
3051
  (totalRecoveries > 0 ? ` | ${totalRecoveries} auto-recoveries` : '') +
3052
3052
  (CLUSTER_ENABLED ? ` | node: ${NODE_ID.substring(0, 12)}` : '');
3053
- sendWebhook('Session Ended', webhookMsg, 0x8b5cf6);
3053
+ sendWebhook('Session Ended', webhookMsg, 0x8b5cf6).catch(() => {});
3054
3054
 
3055
3055
  if (redis) { redis.disconnect().catch(() => {}); }
3056
3056
  console.log(`${c.green}Goodbye!${c.reset}\n`);
3057
+ // Force exit after 5s so Ctrl+C always terminates even if cleanup hangs
3058
+ setTimeout(() => process.exit(0), 5000);
3057
3059
  process.exit(0);
3058
3060
  }
3059
3061
 
package/lib/ui.js CHANGED
@@ -1,26 +1,30 @@
1
1
  /**
2
- * CLI Live Dashboard — ASCII banner, per-account status table, event stream below.
2
+ * CLI Live Dashboard — cursor-positioned in-place row updates.
3
+ * Box drawn once at startup. Account rows updated in-place. Events below box.
3
4
  */
4
5
 
5
6
  let _startTime = Date.now();
6
7
  let _workers = [];
7
- let _isShuttingDown = () => false;
8
8
  let _version = '0.0.0';
9
9
  let _live = false;
10
- let _phase = 'init'; // 'init' | 'login' | 'inventory' | 'balance' | 'dms' | 'grinding'
11
- let _boxRow = 0; // which terminal row the box top starts on
12
- let _boxHeight = 0; // total rows consumed by the box
13
- let _eventCountAtDraw = 0; // events logged since last draw
14
- let _eventCount = 0; // total events ever logged
10
+ let _phase = 'init';
11
+
12
+ // Terminal dimensions at last draw
13
+ let _W = 100;
14
+ let _inner = 98;
15
+ let _maxAccounts = 4;
16
+
17
+ // Row map: which terminal row each account row starts at (1-indexed)
18
+ let _accountRows = []; // _accountRows[accountIdx] = terminalRow
15
19
 
16
20
  // ── Spinner frames ────────────────────────────────────────────
17
21
  const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
18
22
  function spinnerFrame() { return SPINNER[Math.floor(Date.now() / 150) % SPINNER.length]; }
19
23
 
20
- // ── Compact ASCII art banner (fits in 80-col box) ─────────────
24
+ // ── ASCII banner ──────────────────────────────────────────────
21
25
  const BANNER_LINES = [
22
26
  ' ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗ DANKGRINDER',
23
- ' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║ v' + 'PLACEHOLDER',
27
+ ' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║ vPLACEHOLDER',
24
28
  ' ██║ ██║██║ ██║██╔██╗ ██║██║ ███╗█████╗ ██║ ██║██╔██╗ ██║',
25
29
  ' ██║ ██║██║ ██║██║╚██╗██║██║ ██║██╔══╝ ██║ ██║██║╚██╗██║',
26
30
  ' ██████╔╝╚██████╔╝██║ ╚████║╚██████╔╝███████╗╚██████╔╝██║ ╚████║',
@@ -43,7 +47,6 @@ const c = {
43
47
  red: '\x1b[38;2;255;80;100m',
44
48
  yellow: '\x1b[38;2;255;220;80m',
45
49
  cyan: '\x1b[38;2;80;220;255m',
46
- blue: '\x1b[38;2;100;160;255m',
47
50
  white: '\x1b[37m',
48
51
  };
49
52
  const DIM = c.dim;
@@ -52,6 +55,7 @@ function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0
52
55
  function padR(s, n) { return trunc(s, n).padEnd(n); }
53
56
  function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
54
57
  function stripAnsi(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, ''); }
58
+ function clearLine() { process.stdout.write('\x1b[2K'); } // clear current line
55
59
 
56
60
  function fmtUptime() {
57
61
  const s = Math.floor((Date.now() - _startTime) / 1000);
@@ -69,7 +73,7 @@ function statusColor(w) {
69
73
  if (!w.channel) return c.red;
70
74
  if (w.paused || w.dashboardPaused) return c.yellow;
71
75
  if (w.busy || w._invRunning || w._sellRunning) return c.yellow;
72
- if (Date.now() < w.globalCooldownUntil) return c.blue;
76
+ if (Date.now() < w.globalCooldownUntil) return c.cyan;
73
77
  return c.green;
74
78
  }
75
79
 
@@ -82,7 +86,7 @@ function statusText(w) {
82
86
  if (w.busy || w._invRunning || w._sellRunning) return spinnerFrame() + ' WORK';
83
87
  if (Date.now() < w.globalCooldownUntil) {
84
88
  const wait = Math.ceil((w.globalCooldownUntil - Date.now()) / 1000);
85
- return '⏳' + wait + 's';
89
+ return '⏳' + padL(wait, 3) + 's';
86
90
  }
87
91
  return '● READY';
88
92
  }
@@ -110,48 +114,56 @@ function fmtLevel(w) {
110
114
  return c.cyan + lv + c.reset;
111
115
  }
112
116
 
113
- // ── Layout ────────────────────────────────────────────────────
114
- function layout() {
115
- const W = Math.min(process.stdout.columns || 100, 120);
116
- const rows = process.stdout.rows || 40;
117
- const maxAccounts = Math.min(_workers.length, Math.max(3, rows - 14));
118
- return { W, maxAccounts };
117
+ // ── Build a full account row string ──────────────────────────
118
+ function buildAccountRow(w, wi, col) {
119
+ const col2 = wc(wi);
120
+ const stCol = statusColor(w);
121
+ const stTxt = statusText(w);
122
+ const earned = fmtCoins(w.stats.coins);
123
+ const bal = w.stats.balance !== undefined ? fmtCoins(w.stats.balance) : DIM + '?' + c.reset;
124
+ const ls = fmtLifesavers(w);
125
+ const lv = fmtLevel(w);
126
+ const name = padR(trunc(w.username || '?', col.name), col.name);
127
+ const doing = padR(w.lastStatus || 'idle', col.cmd);
128
+
129
+ return (
130
+ '\x1b[2K' + // clear the line first
131
+ `\x1b[38;2;77;212;238m│\x1b[0m ` +
132
+ `${DIM}${padL(wi + 1, 2)}${c.reset} ` +
133
+ `${stCol}${padR(stTxt, col.st)}${c.reset} ` +
134
+ `${col2}${name}${c.reset} ` +
135
+ `${DIM}${doing}${c.reset} ` +
136
+ `${padL(bal, col.bal)} ` +
137
+ `${padL(ls, col.ls)} ` +
138
+ `${padL(lv, col.lv)} ` +
139
+ `${padL(earned, col.earned)} ` +
140
+ `\x1b[38;2;77;212;238m│\x1b[0m\n`
141
+ );
119
142
  }
120
143
 
121
- // ── Draw the full dashboard ───────────────────────────────────
144
+ // ── Draw the FULL box (called once at startup) ───────────────
122
145
  function draw() {
123
- const { W, maxAccounts } = layout();
124
- const inner = W - 2;
125
-
126
- // ── Clear screen + home ──
127
- const isFirst = _boxRow === 0;
128
- if (isFirst) {
129
- process.stdout.write('\x1b[2J\x1b[H');
130
- } else {
131
- // Move cursor up to box start: box rows + events since last draw
132
- const eventsSinceDraw = _eventCount - _eventCountAtDraw;
133
- const up = _boxHeight + eventsSinceDraw;
134
- process.stdout.write(`\x1b[${up}A`);
135
- }
146
+ const rows = process.stdout.rows || 40;
147
+ _W = Math.min(process.stdout.columns || 100, 120);
148
+ _inner = _W - 2;
149
+
150
+ // Clear entire screen
151
+ process.stdout.write('\x1b[2J\x1b[H');
136
152
 
137
153
  // ── Top border ──
138
- process.stdout.write(`\x1b[38;2;77;212;238m┌${'─'.repeat(inner)}┐\x1b[0m\n`);
154
+ process.stdout.write(`\x1b[38;2;77;212;238m┌${'─'.repeat(_inner)}┐\x1b[0m\n`);
139
155
 
140
- // ── ASCII banner (dynamic — includes version) ──
156
+ // ── ASCII banner ──
141
157
  const bannerWithVer = BANNER_LINES[1].replace('PLACEHOLDER', _version);
142
158
  const bannerLines = [BANNER_LINES[0], bannerWithVer, ...BANNER_LINES.slice(2)];
143
-
144
159
  for (let i = 0; i < bannerLines.length; i++) {
145
160
  const line = bannerLines[i];
146
- // Gradient: cyan pink for row 0 and 1, dim for others
161
+ const inner = _inner - stripAnsi(line).length;
147
162
  if (i < 2) {
148
163
  const gradLine = gradientLine(line, 77, 212, 238, 255, 92, 147);
149
- const pad = ' '.repeat(Math.max(0, inner - stripAnsi(gradLine).length));
150
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m${gradLine}${pad}\x1b[38;2;77;212;238m│\x1b[0m\n`);
164
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m${gradLine}${' '.repeat(_inner - stripAnsi(gradLine).length)}\x1b[38;2;77;212;238m│\x1b[0m\n`);
151
165
  } else {
152
- const dimLine = DIM + line + c.reset;
153
- const pad = ' '.repeat(Math.max(0, inner - stripAnsi(dimLine).length));
154
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m${dimLine}${pad}\x1b[38;2;77;212;238m│\x1b[0m\n`);
166
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m${DIM}${line}${c.reset}${' '.repeat(_inner - stripAnsi(line).length)}\x1b[38;2;77;212;238m│\x1b[0m\n`);
155
167
  }
156
168
  }
157
169
 
@@ -159,7 +171,7 @@ function draw() {
159
171
  const running = _workers.filter(w => w.channel && !w.paused && !w.dashboardPaused).length;
160
172
  const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
161
173
  const errors = _workers.filter(w => !w.channel).length;
162
- const phaseLabel = _phase === 'grinding' ? '' : ` ${c.dim}[${_phase}]${c.reset}`;
174
+ const phaseLabel = _phase === 'grinding' ? '' : ` ${DIM}[${_phase}]${c.reset}`;
163
175
  const statusParts = [
164
176
  `${c.green}●${c.reset} ${running} online`,
165
177
  paused > 0 ? `${c.yellow}~${c.reset} ${paused} paused` : null,
@@ -167,16 +179,15 @@ function draw() {
167
179
  `${DIM}Ctrl+C${c.reset}`,
168
180
  ].filter(Boolean);
169
181
  const statusStr = statusParts.join(' ') + phaseLabel;
170
- const statusPad = ' '.repeat(Math.max(0, inner - stripAnsi(statusStr)));
171
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${statusStr}${statusPad}\x1b[38;2;77;212;238m│\x1b[0m\n`);
172
- process.stdout.write(`\x1b[38;2;77;212;238m├${'─'.repeat(inner)}┤\x1b[0m\n`);
182
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${statusStr}${' '.repeat(_inner - stripAnsi(statusStr))}\x1b[38;2;77;212;238m│\x1b[0m\n`);
183
+ process.stdout.write(`\x1b[38;2;77;212;238m├${'─'.repeat(_inner)}┤\x1b[0m\n`);
173
184
 
174
185
  // ── Table header ──
175
186
  const col = { st: 9, name: 18, cmd: 16, bal: 8, ls: 3, lv: 3, earned: 8 };
176
- const nameExtra = Math.max(0, inner - col.st - col.name - col.cmd - col.bal - col.ls - col.lv - col.earned - 14);
187
+ const nameExtra = Math.max(0, _inner - col.st - col.name - col.cmd - col.bal - col.ls - col.lv - col.earned - 14);
177
188
  col.name += nameExtra;
178
189
 
179
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
190
+ process.stdout.write(`\x1b[2K\x1b[38;2;77;212;238m│\x1b[0m `);
180
191
  process.stdout.write(`${c.bold}#${c.reset} `);
181
192
  process.stdout.write(`${c.bold}${padR('STATUS', col.st)}${c.reset} `);
182
193
  process.stdout.write(`${c.bold}${padR('ACCOUNT', col.name)}${c.reset} `);
@@ -186,7 +197,16 @@ function draw() {
186
197
  process.stdout.write(`${c.bold}${padL('LV', col.lv)}${c.reset} `);
187
198
  process.stdout.write(`${c.bold}${padL('EARNED', col.earned)}${c.reset} `);
188
199
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
189
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(inner)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
200
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(_inner)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
201
+
202
+ // Track row positions
203
+ const BOX_TOP = 1;
204
+ const bannerH = bannerLines.length; // 6
205
+ const statusBarRow = BOX_TOP + bannerH + 1; // row 8
206
+ const headerRow = statusBarRow + 1; // row 9
207
+ const hrRow = headerRow + 1; // row 10
208
+ _maxAccounts = Math.min(_workers.length, Math.max(3, rows - 14));
209
+ _accountRows = [];
190
210
 
191
211
  // ── Account rows ──
192
212
  const sorted = [..._workers].sort((a, b) => {
@@ -196,49 +216,27 @@ function draw() {
196
216
  if (aA !== bA) return bA ? 1 : -1;
197
217
  return (b.stats.commands || 0) - (a.stats.commands || 0);
198
218
  });
199
- const shown = sorted.slice(0, maxAccounts);
219
+ const shown = sorted.slice(0, _maxAccounts);
200
220
 
201
221
  for (let si = 0; si < shown.length; si++) {
202
222
  const w = shown[si];
203
223
  const wi = _workers.indexOf(w);
204
- const col2 = wc(wi);
205
- const stCol = statusColor(w);
206
- const stTxt = statusText(w);
207
- const earned = fmtCoins(w.stats.coins);
208
- const bal = w.stats.balance !== undefined ? fmtCoins(w.stats.balance) : DIM + '?' + c.reset;
209
- const ls = fmtLifesavers(w);
210
- const lv = fmtLevel(w);
211
-
212
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
213
- process.stdout.write(`${DIM}${padL(wi + 1, 2)}${c.reset} `);
214
- process.stdout.write(`${stCol}${padR(stTxt, col.st)}${c.reset} `);
215
- process.stdout.write(`${col2}${padR(trunc(w.username || '?', col.name), col.name)}${c.reset} `);
216
- process.stdout.write(`${DIM}${padR(w.lastStatus || 'idle', col.cmd)}${c.reset} `);
217
- process.stdout.write(`${padL(bal, col.bal)} `);
218
- process.stdout.write(`${padL(ls, col.ls)} `);
219
- process.stdout.write(`${padL(lv, col.lv)} `);
220
- process.stdout.write(`${padL(earned, col.earned)} `);
221
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
222
- }
223
-
224
- if (sorted.length > maxAccounts) {
225
- const extra = sorted.length - maxAccounts;
226
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${DIM}+ ${extra} more accounts${' '.repeat(Math.max(0, inner - 20 - String(extra).length))}${c.reset} \x1b[38;2;77;212;238m│\x1b[0m\n`);
224
+ const rowNum = hrRow + 1 + si;
225
+ _accountRows[wi] = rowNum;
226
+ process.stdout.write(buildAccountRow(w, wi, col));
227
227
  }
228
228
 
229
229
  // ── Totals ──
230
- let totalCoins = 0, totalBal = 0, totalCmds = 0, totalOk = 0;
230
+ let totalCoins = 0, totalBal = 0;
231
231
  for (const w of _workers) {
232
232
  totalCoins += w.stats.coins || 0;
233
233
  totalBal += w.stats.balance || 0;
234
- totalCmds += w.stats.commands || 0;
235
- totalOk += w.stats.successes || 0;
236
234
  }
237
- const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
238
235
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
239
236
 
240
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(inner)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
241
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
237
+ const totalsRow = hrRow + 1 + shown.length;
238
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(_inner)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
239
+ process.stdout.write(`\x1b[2K\x1b[38;2;77;212;238m│\x1b[0m `);
242
240
  process.stdout.write(`${c.bold}Σ${c.reset} `);
243
241
  process.stdout.write(`${DIM}${padL(_workers.length, 2)} acc${c.reset} `);
244
242
  process.stdout.write(`${' '.repeat(col.name)} `);
@@ -247,15 +245,28 @@ function draw() {
247
245
  process.stdout.write(`${' '.repeat(col.ls)} `);
248
246
  process.stdout.write(`${' '.repeat(col.lv)} `);
249
247
  process.stdout.write(`${fmtCoins(totalCoins)} `);
250
- process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(inner));
248
+ process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(_inner));
251
249
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
252
250
 
253
251
  // ── Bottom ──
254
- process.stdout.write(`\x1b[38;2;77;212;238m└${'─'.repeat(inner)}┘\x1b[0m\n`);
252
+ process.stdout.write(`\x1b[38;2;77;212;238m└${'─'.repeat(_inner)}┘\x1b[0m\n`);
253
+ }
254
+
255
+ // ── Update ONE account row in place ─────────────────────────
256
+ function updateAccountRow(accountIdx) {
257
+ const row = _accountRows[accountIdx];
258
+ if (!row) return; // not visible
255
259
 
256
- // Record box height + current event count for next redraw
257
- _boxHeight = bannerLines.length + 1 + 1 + 2 + shown.length + (sorted.length > maxAccounts ? 1 : 0) + 3;
258
- _eventCountAtDraw = _eventCount;
260
+ const w = _workers[accountIdx];
261
+ if (!w) return;
262
+
263
+ const col = { st: 9, name: 18, cmd: 16, bal: 8, ls: 3, lv: 3, earned: 8 };
264
+ const nameExtra = Math.max(0, _inner - col.st - col.name - col.cmd - col.bal - col.ls - col.lv - col.earned - 14);
265
+ col.name += nameExtra;
266
+
267
+ // Move cursor to this row, column 1
268
+ process.stdout.write(`\x1b[${row};1H`);
269
+ process.stdout.write(buildAccountRow(w, accountIdx, col));
259
270
  }
260
271
 
261
272
  // ── Gradient line ─────────────────────────────────────────────
@@ -270,34 +281,33 @@ function gradientLine(text, r1, g1, b1, r2, g2, b2) {
270
281
 
271
282
  // ── Event tracking ────────────────────────────────────────────
272
283
  let _eventLines = []; // [accountIdx] = [{text, ts}]
273
- const MAX_EVENTS = 4;
284
+ const MAX_EVENTS = 15;
274
285
 
275
286
  // ── Public API ────────────────────────────────────────────────
276
287
 
277
288
  function init({ workers, isShuttingDown }) {
278
289
  _startTime = Date.now();
279
290
  _workers = workers;
280
- _isShuttingDown = isShuttingDown || (() => false);
281
291
  _version = '0.0.0';
282
292
  _eventLines = [];
283
293
  _live = false;
284
294
  _phase = 'init';
285
- _boxRow = 0;
286
- _boxHeight = 0;
287
- _eventCount = 0;
288
- _eventCountAtDraw = 0;
295
+ _accountRows = [];
289
296
  }
290
297
 
291
298
  function drawBanner(version) { _version = version || '0.0.0'; }
292
-
293
299
  function start() {}
294
- function stop() { _live = false; _phase = 'init'; process.stdout.write(c.reset + '\n'); }
295
300
 
296
- function setLive(val) { _live = val; }
301
+ function stop() {
302
+ _live = false;
303
+ _phase = 'init';
304
+ process.stdout.write('\x1b[2J\x1b[H' + c.reset + '\n');
305
+ }
297
306
 
307
+ function setLive(val) { _live = val; }
298
308
  function setPhase(phase) { _phase = phase; }
299
309
 
300
- // log: append event below the box (no redraw of box)
310
+ // log: update account row in-place + append event below box
301
311
  function log(accountIdx, msg) {
302
312
  const now = new Date();
303
313
  const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
@@ -306,12 +316,13 @@ function log(accountIdx, msg) {
306
316
  if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
307
317
  _eventLines[accountIdx].push({ text: msg, ts });
308
318
  if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
319
+ // Update the row in place
320
+ if (_live) updateAccountRow(accountIdx);
309
321
  }
310
- _eventCount++;
311
322
 
312
323
  if (!_live) return;
313
324
 
314
- // Clean event line: colored name + timestamp + message
325
+ // Append event below the box (after the bottom border)
315
326
  const col2 = accountIdx >= 0 ? wc(accountIdx) : c.cyan;
316
327
  const name = accountIdx >= 0 ? trunc(_workers[accountIdx]?.username || '?', 14) : 'GLOBAL';
317
328
  process.stdout.write(`${col2}${name}${c.reset} ${DIM}[${ts}]${c.reset} ${msg}\n`);
@@ -319,16 +330,22 @@ function log(accountIdx, msg) {
319
330
 
320
331
  function logGlobal(msg) { log(-1, msg); }
321
332
 
322
- // Start periodic box refresh (every 3s for spinner/countdown updates)
323
- let _refreshInterval = null;
324
- function startRefresh(ms = 3000) {
325
- if (_refreshInterval) clearInterval(_refreshInterval);
326
- _refreshInterval = setInterval(() => {
327
- if (_live) draw();
328
- }, ms);
333
+ // Force full redraw (call this when window resizes or box needs rebuild)
334
+ function fullRedraw() { if (_live) draw(); }
335
+
336
+ // ── Periodic refresh: animate spinners & countdowns every 3s ──
337
+ let _refreshTimer = null;
338
+ function startRefresh() {
339
+ if (_refreshTimer) return;
340
+ _refreshTimer = setInterval(() => {
341
+ if (!_live) return;
342
+ for (let i = 0; i < _workers.length; i++) {
343
+ updateAccountRow(i);
344
+ }
345
+ }, 1500);
329
346
  }
330
347
  function stopRefresh() {
331
- if (_refreshInterval) { clearInterval(_refreshInterval); _refreshInterval = null; }
348
+ if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
332
349
  }
333
350
 
334
- module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase, startRefresh, stopRefresh };
351
+ module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase, updateAccountRow, fullRedraw, startRefresh, stopRefresh };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.48.0",
3
+ "version": "8.51.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"