dankgrinder 8.49.0 → 8.53.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 +2 -0
  2. package/lib/ui.js +116 -73
  3. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -2932,6 +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();
2935
2936
  for (const w of activeWorkers) {
2936
2937
  if (!shutdownCalled) w.grindLoop();
2937
2938
  }
@@ -3002,6 +3003,7 @@ async function start(apiKey, apiUrl, opts = {}) {
3002
3003
  shutdownInProgress = true;
3003
3004
  shutdownCalled = true;
3004
3005
  setDashboardActive(false);
3006
+ ui.stopRefresh();
3005
3007
  ui.stop();
3006
3008
  process.stdout.write(c.show + '\n');
3007
3009
 
package/lib/ui.js CHANGED
@@ -1,20 +1,27 @@
1
1
  /**
2
- * CLI Live Dashboard — ASCII banner, per-account status table, event stream below.
3
- * Box fixed at top. Events append line-by-line below. Manual redraw only.
2
+ * CLI Live Dashboard — cursor-positioned in-place row updates.
3
+ * Box drawn once at startup. Account rows updated in-place. Events below box.
4
4
  */
5
5
 
6
6
  let _startTime = Date.now();
7
7
  let _workers = [];
8
- let _isShuttingDown = () => false;
9
8
  let _version = '0.0.0';
10
9
  let _live = false;
11
- let _phase = 'init'; // 'init' | 'login' | 'inventory' | 'balance' | 'dms' | 'grinding'
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
12
19
 
13
20
  // ── Spinner frames ────────────────────────────────────────────
14
21
  const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
15
22
  function spinnerFrame() { return SPINNER[Math.floor(Date.now() / 150) % SPINNER.length]; }
16
23
 
17
- // ── Compact ASCII art banner (fits in 80-col box) ─────────────
24
+ // ── ASCII banner ──────────────────────────────────────────────
18
25
  const BANNER_LINES = [
19
26
  ' ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗ DANKGRINDER',
20
27
  ' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║ vPLACEHOLDER',
@@ -40,7 +47,6 @@ const c = {
40
47
  red: '\x1b[38;2;255;80;100m',
41
48
  yellow: '\x1b[38;2;255;220;80m',
42
49
  cyan: '\x1b[38;2;80;220;255m',
43
- blue: '\x1b[38;2;100;160;255m',
44
50
  white: '\x1b[37m',
45
51
  };
46
52
  const DIM = c.dim;
@@ -49,6 +55,7 @@ function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0
49
55
  function padR(s, n) { return trunc(s, n).padEnd(n); }
50
56
  function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
51
57
  function stripAnsi(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, ''); }
58
+ function clearLine() { process.stdout.write('\x1b[2K'); } // clear current line
52
59
 
53
60
  function fmtUptime() {
54
61
  const s = Math.floor((Date.now() - _startTime) / 1000);
@@ -66,7 +73,7 @@ function statusColor(w) {
66
73
  if (!w.channel) return c.red;
67
74
  if (w.paused || w.dashboardPaused) return c.yellow;
68
75
  if (w.busy || w._invRunning || w._sellRunning) return c.yellow;
69
- if (Date.now() < w.globalCooldownUntil) return c.blue;
76
+ if (Date.now() < w.globalCooldownUntil) return c.cyan;
70
77
  return c.green;
71
78
  }
72
79
 
@@ -79,7 +86,7 @@ function statusText(w) {
79
86
  if (w.busy || w._invRunning || w._sellRunning) return spinnerFrame() + ' WORK';
80
87
  if (Date.now() < w.globalCooldownUntil) {
81
88
  const wait = Math.ceil((w.globalCooldownUntil - Date.now()) / 1000);
82
- return '⏳' + wait + 's';
89
+ return '⏳' + padL(wait, 3) + 's';
83
90
  }
84
91
  return '● READY';
85
92
  }
@@ -107,39 +114,56 @@ function fmtLevel(w) {
107
114
  return c.cyan + lv + c.reset;
108
115
  }
109
116
 
110
- // ── Layout ────────────────────────────────────────────────────
111
- function layout() {
112
- const W = Math.min(process.stdout.columns || 100, 120);
113
- const rows = process.stdout.rows || 40;
114
- const maxAccounts = Math.min(_workers.length, Math.max(3, rows - 14));
115
- 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
+ );
116
142
  }
117
143
 
118
- // ── Draw the full dashboard ───────────────────────────────────
144
+ // ── Draw the FULL box (called once at startup) ───────────────
119
145
  function draw() {
120
- const { W, maxAccounts } = layout();
121
- const inner = W - 2;
146
+ const rows = process.stdout.rows || 40;
147
+ _W = Math.min(process.stdout.columns || 100, 120);
148
+ _inner = _W - 2;
122
149
 
123
- // ── Clear screen + home (always from top — keeps box fixed) ──
150
+ // Clear entire screen
124
151
  process.stdout.write('\x1b[2J\x1b[H');
125
152
 
126
153
  // ── Top border ──
127
- 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`);
128
155
 
129
156
  // ── ASCII banner ──
130
157
  const bannerWithVer = BANNER_LINES[1].replace('PLACEHOLDER', _version);
131
158
  const bannerLines = [BANNER_LINES[0], bannerWithVer, ...BANNER_LINES.slice(2)];
132
-
133
159
  for (let i = 0; i < bannerLines.length; i++) {
134
160
  const line = bannerLines[i];
161
+ const inner = _inner - stripAnsi(line).length;
135
162
  if (i < 2) {
136
163
  const gradLine = gradientLine(line, 77, 212, 238, 255, 92, 147);
137
- const pad = ' '.repeat(Math.max(0, inner - stripAnsi(gradLine).length));
138
- 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`);
139
165
  } else {
140
- const dimLine = DIM + line + c.reset;
141
- const pad = ' '.repeat(Math.max(0, inner - stripAnsi(dimLine).length));
142
- 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`);
143
167
  }
144
168
  }
145
169
 
@@ -147,7 +171,7 @@ function draw() {
147
171
  const running = _workers.filter(w => w.channel && !w.paused && !w.dashboardPaused).length;
148
172
  const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
149
173
  const errors = _workers.filter(w => !w.channel).length;
150
- const phaseLabel = _phase === 'grinding' ? '' : ` ${c.dim}[${_phase}]${c.reset}`;
174
+ const phaseLabel = _phase === 'grinding' ? '' : ` ${DIM}[${_phase}]${c.reset}`;
151
175
  const statusParts = [
152
176
  `${c.green}●${c.reset} ${running} online`,
153
177
  paused > 0 ? `${c.yellow}~${c.reset} ${paused} paused` : null,
@@ -155,16 +179,15 @@ function draw() {
155
179
  `${DIM}Ctrl+C${c.reset}`,
156
180
  ].filter(Boolean);
157
181
  const statusStr = statusParts.join(' ') + phaseLabel;
158
- const statusPad = ' '.repeat(Math.max(0, inner - stripAnsi(statusStr)));
159
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${statusStr}${statusPad}\x1b[38;2;77;212;238m│\x1b[0m\n`);
160
- 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`);
161
184
 
162
185
  // ── Table header ──
163
186
  const col = { st: 9, name: 18, cmd: 16, bal: 8, ls: 3, lv: 3, earned: 8 };
164
- 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);
165
188
  col.name += nameExtra;
166
189
 
167
- 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 `);
168
191
  process.stdout.write(`${c.bold}#${c.reset} `);
169
192
  process.stdout.write(`${c.bold}${padR('STATUS', col.st)}${c.reset} `);
170
193
  process.stdout.write(`${c.bold}${padR('ACCOUNT', col.name)}${c.reset} `);
@@ -174,7 +197,16 @@ function draw() {
174
197
  process.stdout.write(`${c.bold}${padL('LV', col.lv)}${c.reset} `);
175
198
  process.stdout.write(`${c.bold}${padL('EARNED', col.earned)}${c.reset} `);
176
199
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
177
- 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 = [];
178
210
 
179
211
  // ── Account rows ──
180
212
  const sorted = [..._workers].sort((a, b) => {
@@ -184,49 +216,27 @@ function draw() {
184
216
  if (aA !== bA) return bA ? 1 : -1;
185
217
  return (b.stats.commands || 0) - (a.stats.commands || 0);
186
218
  });
187
- const shown = sorted.slice(0, maxAccounts);
219
+ const shown = sorted.slice(0, _maxAccounts);
188
220
 
189
221
  for (let si = 0; si < shown.length; si++) {
190
222
  const w = shown[si];
191
223
  const wi = _workers.indexOf(w);
192
- const col2 = wc(wi);
193
- const stCol = statusColor(w);
194
- const stTxt = statusText(w);
195
- const earned = fmtCoins(w.stats.coins);
196
- const bal = w.stats.balance !== undefined ? fmtCoins(w.stats.balance) : DIM + '?' + c.reset;
197
- const ls = fmtLifesavers(w);
198
- const lv = fmtLevel(w);
199
-
200
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
201
- process.stdout.write(`${DIM}${padL(wi + 1, 2)}${c.reset} `);
202
- process.stdout.write(`${stCol}${padR(stTxt, col.st)}${c.reset} `);
203
- process.stdout.write(`${col2}${padR(trunc(w.username || '?', col.name), col.name)}${c.reset} `);
204
- process.stdout.write(`${DIM}${padR(w.lastStatus || 'idle', col.cmd)}${c.reset} `);
205
- process.stdout.write(`${padL(bal, col.bal)} `);
206
- process.stdout.write(`${padL(ls, col.ls)} `);
207
- process.stdout.write(`${padL(lv, col.lv)} `);
208
- process.stdout.write(`${padL(earned, col.earned)} `);
209
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
210
- }
211
-
212
- if (sorted.length > maxAccounts) {
213
- const extra = sorted.length - maxAccounts;
214
- 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));
215
227
  }
216
228
 
217
229
  // ── Totals ──
218
- let totalCoins = 0, totalBal = 0, totalCmds = 0, totalOk = 0;
230
+ let totalCoins = 0, totalBal = 0;
219
231
  for (const w of _workers) {
220
232
  totalCoins += w.stats.coins || 0;
221
233
  totalBal += w.stats.balance || 0;
222
- totalCmds += w.stats.commands || 0;
223
- totalOk += w.stats.successes || 0;
224
234
  }
225
- const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
226
235
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
227
236
 
228
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(inner)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
229
- 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 `);
230
240
  process.stdout.write(`${c.bold}Σ${c.reset} `);
231
241
  process.stdout.write(`${DIM}${padL(_workers.length, 2)} acc${c.reset} `);
232
242
  process.stdout.write(`${' '.repeat(col.name)} `);
@@ -235,11 +245,28 @@ function draw() {
235
245
  process.stdout.write(`${' '.repeat(col.ls)} `);
236
246
  process.stdout.write(`${' '.repeat(col.lv)} `);
237
247
  process.stdout.write(`${fmtCoins(totalCoins)} `);
238
- process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(inner));
248
+ process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(_inner));
239
249
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
240
250
 
241
251
  // ── Bottom ──
242
- 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
259
+
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));
243
270
  }
244
271
 
245
272
  // ── Gradient line ─────────────────────────────────────────────
@@ -254,34 +281,33 @@ function gradientLine(text, r1, g1, b1, r2, g2, b2) {
254
281
 
255
282
  // ── Event tracking ────────────────────────────────────────────
256
283
  let _eventLines = []; // [accountIdx] = [{text, ts}]
257
- const MAX_EVENTS = 20; // keep last 20 events per account
284
+ const MAX_EVENTS = 15;
258
285
 
259
286
  // ── Public API ────────────────────────────────────────────────
260
287
 
261
288
  function init({ workers, isShuttingDown }) {
262
289
  _startTime = Date.now();
263
290
  _workers = workers;
264
- _isShuttingDown = isShuttingDown || (() => false);
265
291
  _version = '0.0.0';
266
292
  _eventLines = [];
267
293
  _live = false;
268
294
  _phase = 'init';
295
+ _accountRows = [];
269
296
  }
270
297
 
271
298
  function drawBanner(version) { _version = version || '0.0.0'; }
272
-
273
299
  function start() {}
300
+
274
301
  function stop() {
275
302
  _live = false;
276
303
  _phase = 'init';
277
- // Clear screen on shutdown so box disappears
278
304
  process.stdout.write('\x1b[2J\x1b[H' + c.reset + '\n');
279
305
  }
280
306
 
281
307
  function setLive(val) { _live = val; }
282
308
  function setPhase(phase) { _phase = phase; }
283
309
 
284
- // log: draw box + append event below it
310
+ // log: update account row in-place + append event below box
285
311
  function log(accountIdx, msg) {
286
312
  const now = new Date();
287
313
  const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
@@ -290,12 +316,13 @@ function log(accountIdx, msg) {
290
316
  if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
291
317
  _eventLines[accountIdx].push({ text: msg, ts });
292
318
  if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
319
+ // Update the row in place
320
+ if (_live) updateAccountRow(accountIdx);
293
321
  }
294
322
 
295
323
  if (!_live) return;
296
324
 
297
- // Redraw the whole box (clears screen + redraws at top) then append event
298
- draw();
325
+ // Append event below the box (after the bottom border)
299
326
  const col2 = accountIdx >= 0 ? wc(accountIdx) : c.cyan;
300
327
  const name = accountIdx >= 0 ? trunc(_workers[accountIdx]?.username || '?', 14) : 'GLOBAL';
301
328
  process.stdout.write(`${col2}${name}${c.reset} ${DIM}[${ts}]${c.reset} ${msg}\n`);
@@ -303,6 +330,22 @@ function log(accountIdx, msg) {
303
330
 
304
331
  function logGlobal(msg) { log(-1, msg); }
305
332
 
306
- // Periodic refresh removed — draw() is now only called explicitly
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);
346
+ }
347
+ function stopRefresh() {
348
+ if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
349
+ }
307
350
 
308
- module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase };
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.49.0",
3
+ "version": "8.53.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"