dankgrinder 8.53.0 → 8.56.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 +62 -24
  2. package/lib/ui.js +108 -95
  3. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -2002,11 +2002,25 @@ class AccountWorker {
2002
2002
 
2003
2003
  // Set up error/disconnect handlers for auto-recovery
2004
2004
  this._attachRecoveryListeners();
2005
- rawLogger.attachRawLogger(this.client, { channelId: this.account.channel_id });
2005
+ rawLogger.attachRawLogger(this.client, { channelId: this.channel?.id || this.account.channel_id || '' });
2006
2006
  rawLogger.attachDmLogger(this.client);
2007
2007
 
2008
- await this.client.login(this.account.discord_token);
2009
- this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
2008
+ // DM mode: resolve Dank Memer DM after login
2009
+ if (isDMMode) {
2010
+ const recoveryLoginToken = this.account.access_token || this.account.discord_token;
2011
+ await this.client.login(recoveryLoginToken);
2012
+ try {
2013
+ const dankUser = await this.client.users.fetch(DANK_MEMER_ID);
2014
+ this.channel = await dankUser.createDM();
2015
+ this._dmChannelId = this.channel.id;
2016
+ this.log('info', `DM re-opened with Dank Memer`);
2017
+ } catch (err) {
2018
+ this.log('error', `Failed to re-open DM: ${err?.message || err}`);
2019
+ }
2020
+ } else {
2021
+ await this.client.login(this.account.discord_token);
2022
+ this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
2023
+ }
2010
2024
 
2011
2025
  if (this.channel) {
2012
2026
  this._recoveryAttempts = 0;
@@ -2486,8 +2500,13 @@ class AccountWorker {
2486
2500
  } else {
2487
2501
  process.stdout.write(`[WORKER START] ${this.account.label || this.account.id}\n`);
2488
2502
  }
2489
- if (!this.account.discord_token) { this.log('error', 'No token'); return; }
2490
- if (!this.account.channel_id) { this.log('error', 'No channel'); return; }
2503
+ if (!this.account.discord_token && !this.account.access_token) { this.log('error', 'No token or access token'); return; }
2504
+ if (!this.account.channel_id && this.account.grind_mode !== 'dm') { this.log('error', 'No channel'); return; }
2505
+
2506
+ // Determine login token: OAuth access_token takes priority over discord_token
2507
+ const loginToken = this.account.access_token || this.account.discord_token;
2508
+ const grindMode = this.account.grind_mode || 'channel';
2509
+ const isDMMode = grindMode === 'dm';
2491
2510
 
2492
2511
  const LOGIN_TIMEOUT_MS = 30_000; // 30s max per account login
2493
2512
 
@@ -2512,8 +2531,8 @@ class AccountWorker {
2512
2531
  this.loggedIn = true; // so statusColor/statusText show correct state
2513
2532
 
2514
2533
  // Attach raw gateway logger for CV2 component capture
2515
- rawLogger.attachRawLogger(this.client, { channelId: this.account.channel_id });
2516
- rawLogger.attachDmLogger(this.client);
2534
+ rawLogger.attachRawLogger(this.client, { channelId: this.channel?.id || '' });
2535
+ rawLogger.attachDmLogger(this.client);
2517
2536
 
2518
2537
  // Report status non-blocking
2519
2538
  fetch(`${API_URL}/api/grinder/status`, {
@@ -2522,10 +2541,23 @@ class AccountWorker {
2522
2541
  body: JSON.stringify({ account_id: this.account.id, discord_username: this.username, avatar_url: this.avatarUrl, userId: this.account.userId }),
2523
2542
  }).catch(() => {});
2524
2543
 
2525
- this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
2526
- if (!this.channel) {
2527
- this.log('error', `Channel not found`);
2528
- done(); return;
2544
+ // Resolve grinding channel: DM mode opens DM with Dank Memer, otherwise use configured channel
2545
+ if (isDMMode) {
2546
+ try {
2547
+ const dankUser = await this.client.users.fetch(DANK_MEMER_ID);
2548
+ this.channel = await dankUser.createDM();
2549
+ this._dmChannelId = this.channel.id;
2550
+ this.log('info', `DM opened with Dank Memer`);
2551
+ } catch (err) {
2552
+ this.log('error', `Failed to open DM: ${err?.message || err}`);
2553
+ done(); return;
2554
+ }
2555
+ } else {
2556
+ this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
2557
+ if (!this.channel) {
2558
+ this.log('error', `Channel not found`);
2559
+ done(); return;
2560
+ }
2529
2561
  }
2530
2562
 
2531
2563
  const enabledCmds = [
@@ -2574,7 +2606,7 @@ class AccountWorker {
2574
2606
 
2575
2607
  // Attach auto-recovery event listeners before login
2576
2608
  this._attachRecoveryListeners();
2577
- this.client.login(this.account.discord_token).catch((err) => {
2609
+ this.client.login(loginToken).catch((err) => {
2578
2610
  clearTimeout(timeoutId);
2579
2611
  const msg = (err?.message || '').toLowerCase();
2580
2612
  if (msg.includes('token_invalid') || msg.includes('invalid token') || msg.includes('401') || msg.includes('incorrect login')) {
@@ -2638,11 +2670,17 @@ class AccountWorker {
2638
2670
  this.client.destroy();
2639
2671
  this.client = createLeanClient();
2640
2672
  this._attachRecoveryListeners();
2641
- await this.client.login(this.account.discord_token);
2642
- this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
2643
- if (this.channel) {
2644
- this.username = this.client.user?.tag || this.username;
2645
- this.log('success', `Background login OK`);
2673
+ const retryToken = this.account.access_token || this.account.discord_token;
2674
+ await this.client.login(retryToken);
2675
+ // DM mode: re-open DM with Dank Memer
2676
+ if (this.account.grind_mode === 'dm') {
2677
+ const dankUser = await this.client.users.fetch(DANK_MEMER_ID);
2678
+ this.channel = await dankUser.createDM();
2679
+ this._dmChannelId = this.channel.id;
2680
+ this.log('success', `Background login OK (DM)`);
2681
+ } else {
2682
+ this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
2683
+ if (this.channel) this.log('success', `Background login OK`);
2646
2684
  }
2647
2685
  } catch (err) {
2648
2686
  this.log('error', `Background login failed: ${err?.message || err}`);
@@ -3024,11 +3062,10 @@ async function start(apiKey, apiUrl, opts = {}) {
3024
3062
  const cpm = globalCmdRate.getRate().toFixed(1);
3025
3063
  console.log(`${c.bold}Total:${c.reset} +⏣${finalCoins.toLocaleString()} in ${formatUptime()} | ${finalCmds}cmds | ~${cpm}cmd/m | ${memFinal}MB`);
3026
3064
 
3027
- // Stop workers max 1s per worker so one hung client doesn't block shutdown
3028
- await Promise.all(workers.map(wk => Promise.race([
3029
- new Promise(resolve => { wk.stop(); resolve(true); }),
3030
- new Promise(resolve => setTimeout(() => resolve(false), 1000)),
3031
- ])));
3065
+ // Stop workers immediately (don't wait) instant shutdown
3066
+ for (const wk of workers) {
3067
+ try { wk.stop(); } catch {}
3068
+ }
3032
3069
  workerMap.clear();
3033
3070
 
3034
3071
  // Release cluster claims
@@ -3054,13 +3091,14 @@ async function start(apiKey, apiUrl, opts = {}) {
3054
3091
 
3055
3092
  if (redis) { redis.disconnect().catch(() => {}); }
3056
3093
  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);
3094
+ // Force exit so Ctrl+C always terminates immediately
3095
+ setTimeout(() => process.exit(0), 2000);
3059
3096
  process.exit(0);
3060
3097
  }
3061
3098
 
3062
3099
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));
3063
3100
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
3101
+ process.on('SIGHUP', () => gracefulShutdown('SIGHUP'));
3064
3102
  }
3065
3103
 
3066
3104
  // ══════════════════════════════════════════════════════════════
package/lib/ui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
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
+ * Box drawn once. Rows updated in-place. Events below box.
4
4
  */
5
5
 
6
6
  let _startTime = Date.now();
@@ -9,13 +9,15 @@ let _version = '0.0.0';
9
9
  let _live = false;
10
10
  let _phase = 'init';
11
11
 
12
- // Terminal dimensions at last draw
12
+ // Terminal dimensions
13
13
  let _W = 100;
14
14
  let _inner = 98;
15
15
  let _maxAccounts = 4;
16
16
 
17
- // Row map: which terminal row each account row starts at (1-indexed)
18
- let _accountRows = []; // _accountRows[accountIdx] = terminalRow
17
+ // Row map: which terminal row each account starts at (1-indexed)
18
+ let _accountRows = []; // _accountRows[accountIdx] = row
19
+ let _totalsRow = 0;
20
+ let _bottomRow = 0;
19
21
 
20
22
  // ── Spinner frames ────────────────────────────────────────────
21
23
  const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
@@ -50,20 +52,21 @@ const c = {
50
52
  white: '\x1b[37m',
51
53
  };
52
54
  const DIM = c.dim;
55
+ const B = '\x1b[38;2;77;212;238m'; // box border color
53
56
 
57
+ function row(n) { process.stdout.write(`\x1b[${n};1H`); }
58
+ function clrLine() { process.stdout.write('\x1b[2K'); }
54
59
  function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
55
60
  function padR(s, n) { return trunc(s, n).padEnd(n); }
56
61
  function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
57
62
  function stripAnsi(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, ''); }
58
- function clearLine() { process.stdout.write('\x1b[2K'); } // clear current line
63
+ function ln() { process.stdout.write('\n'); }
59
64
 
60
65
  function fmtUptime() {
61
66
  const s = Math.floor((Date.now() - _startTime) / 1000);
62
67
  if (s < 60) return `${s}s`;
63
68
  const m = Math.floor(s / 60);
64
69
  const h = Math.floor(m / 60);
65
- const d = Math.floor(h / 24);
66
- if (d > 0) return `${d}d${h % 24}h`;
67
70
  if (h > 0) return `${h}h${m % 60}m`;
68
71
  return `${m}m`;
69
72
  }
@@ -114,8 +117,24 @@ function fmtLevel(w) {
114
117
  return c.cyan + lv + c.reset;
115
118
  }
116
119
 
117
- // ── Build a full account row string ──────────────────────────
118
- function buildAccountRow(w, wi, col) {
120
+ // ── Layout ────────────────────────────────────────────────────
121
+ function layout() {
122
+ _W = Math.min(process.stdout.columns || 100, 120);
123
+ _inner = _W - 2;
124
+ const rows = process.stdout.rows || 40;
125
+ _maxAccounts = Math.min(_workers.length, Math.max(3, rows - 16));
126
+ }
127
+
128
+ // ── Column layout ─────────────────────────────────────────────
129
+ function getCol() {
130
+ const col = { st: 9, name: 18, cmd: 16, bal: 8, ls: 3, lv: 3, earned: 8 };
131
+ const nameExtra = Math.max(0, _inner - col.st - col.name - col.cmd - col.bal - col.ls - col.lv - col.earned - 14);
132
+ col.name += nameExtra;
133
+ return col;
134
+ }
135
+
136
+ // ── Build account row string ─────────────────────────────────
137
+ function accountRow(w, wi, col) {
119
138
  const col2 = wc(wi);
120
139
  const stCol = statusColor(w);
121
140
  const stTxt = statusText(w);
@@ -125,46 +144,54 @@ function buildAccountRow(w, wi, col) {
125
144
  const lv = fmtLevel(w);
126
145
  const name = padR(trunc(w.username || '?', col.name), col.name);
127
146
  const doing = padR(w.lastStatus || 'idle', col.cmd);
147
+ const earnedStr = stripAnsi(earned);
148
+ const balStr = stripAnsi(bal);
149
+
150
+ // Pad earned and bal to column widths
151
+ const earnedPadded = earned.padEnd(col.earned + (earned.length - earnedStr.length));
152
+ const balPadded = bal.padEnd(col.bal + (bal.length - balStr.length));
128
153
 
129
154
  return (
130
- '\x1b[2K' + // clear the line first
131
- `\x1b[38;2;77;212;238m│\x1b[0m ` +
155
+ `${B}│\x1b[0m ` +
132
156
  `${DIM}${padL(wi + 1, 2)}${c.reset} ` +
133
157
  `${stCol}${padR(stTxt, col.st)}${c.reset} ` +
134
158
  `${col2}${name}${c.reset} ` +
135
159
  `${DIM}${doing}${c.reset} ` +
136
- `${padL(bal, col.bal)} ` +
160
+ `${balPadded} ` +
137
161
  `${padL(ls, col.ls)} ` +
138
162
  `${padL(lv, col.lv)} ` +
139
- `${padL(earned, col.earned)} ` +
140
- `\x1b[38;2;77;212;238m│\x1b[0m\n`
163
+ `${earnedPadded} ` +
164
+ `${B}│\x1b[0m`
141
165
  );
142
166
  }
143
167
 
144
- // ── Draw the FULL box (called once at startup) ───────────────
168
+ // ── Draw the FULL box (called once) ─────────────────────────
145
169
  function draw() {
146
- const rows = process.stdout.rows || 40;
147
- _W = Math.min(process.stdout.columns || 100, 120);
148
- _inner = _W - 2;
170
+ layout();
171
+ const col = getCol();
172
+ const bannerH = BANNER_LINES.length;
149
173
 
150
- // Clear entire screen
174
+ // Clear entire screen + home
151
175
  process.stdout.write('\x1b[2J\x1b[H');
152
176
 
177
+ let r = 1;
178
+
153
179
  // ── Top border ──
154
- process.stdout.write(`\x1b[38;2;77;212;238m┌${'─'.repeat(_inner)}┐\x1b[0m\n`);
155
-
156
- // ── ASCII banner ──
157
- const bannerWithVer = BANNER_LINES[1].replace('PLACEHOLDER', _version);
158
- const bannerLines = [BANNER_LINES[0], bannerWithVer, ...BANNER_LINES.slice(2)];
159
- for (let i = 0; i < bannerLines.length; i++) {
160
- const line = bannerLines[i];
161
- const inner = _inner - stripAnsi(line).length;
180
+ row(r++); clrLine();
181
+ process.stdout.write(`${B}┌${'─'.repeat(_inner)}┐`);
182
+ ln();
183
+
184
+ // ── Banner ──
185
+ for (let i = 0; i < bannerH; i++) {
186
+ row(r++); clrLine();
187
+ const line = i === 1 ? BANNER_LINES[1].replace('PLACEHOLDER', _version) : BANNER_LINES[i];
162
188
  if (i < 2) {
163
189
  const gradLine = gradientLine(line, 77, 212, 238, 255, 92, 147);
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`);
190
+ process.stdout.write(`${B}│\x1b[0m ${gradLine}${' '.repeat(_inner - 2 - stripAnsi(gradLine) - 2)}${B}│`);
165
191
  } else {
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`);
192
+ process.stdout.write(`${B}│\x1b[0m ${DIM}${line}${c.reset}${' '.repeat(_inner - 2 - stripAnsi(line) - 2)}${B}│`);
167
193
  }
194
+ ln();
168
195
  }
169
196
 
170
197
  // ── Status bar ──
@@ -179,34 +206,25 @@ function draw() {
179
206
  `${DIM}Ctrl+C${c.reset}`,
180
207
  ].filter(Boolean);
181
208
  const statusStr = statusParts.join(' ') + phaseLabel;
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`);
184
209
 
185
- // ── Table header ──
186
- const col = { st: 9, name: 18, cmd: 16, bal: 8, ls: 3, lv: 3, earned: 8 };
187
- const nameExtra = Math.max(0, _inner - col.st - col.name - col.cmd - col.bal - col.ls - col.lv - col.earned - 14);
188
- col.name += nameExtra;
210
+ row(r++); clrLine();
211
+ process.stdout.write(`${B}│\x1b[0m ${statusStr}${' '.repeat(_inner - 1 - stripAnsi(statusStr))}${B}│`);
212
+ ln();
189
213
 
190
- process.stdout.write(`\x1b[2K\x1b[38;2;77;212;238m│\x1b[0m `);
191
- process.stdout.write(`${c.bold}#${c.reset} `);
192
- process.stdout.write(`${c.bold}${padR('STATUS', col.st)}${c.reset} `);
193
- process.stdout.write(`${c.bold}${padR('ACCOUNT', col.name)}${c.reset} `);
194
- process.stdout.write(`${c.bold}${padR('DOING', col.cmd)}${c.reset} `);
195
- process.stdout.write(`${c.bold}${padL('BAL', col.bal)}${c.reset} `);
196
- process.stdout.write(`${c.bold}${padL('LS', col.ls)}${c.reset} `);
197
- process.stdout.write(`${c.bold}${padL('LV', col.lv)}${c.reset} `);
198
- process.stdout.write(`${c.bold}${padL('EARNED', col.earned)}${c.reset} `);
199
- process.stdout.write(`\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 = [];
214
+ // ── Divider ──
215
+ row(r++); clrLine();
216
+ process.stdout.write(`${B}├${''.repeat(_inner)}┤`);
217
+ ln();
218
+
219
+ // ── Header ──
220
+ row(r++); clrLine();
221
+ process.stdout.write(`${B}│\x1b[0m ${c.bold}#${c.reset} ${c.bold}${padR('STATUS', col.st)}${c.reset} ${c.bold}${padR('ACCOUNT', col.name)}${c.reset} ${c.bold}${padR('DOING', col.cmd)}${c.reset} ${c.bold}${padL('BAL', col.bal)}${c.reset} ${c.bold}${padL('LS', col.ls)}${c.reset} ${c.bold}${padL('LV', col.lv)}${c.reset} ${c.bold}${padL('EARNED', col.earned)}${c.reset} ${B}│`);
222
+ ln();
223
+
224
+ // ── HR ──
225
+ row(r++); clrLine();
226
+ process.stdout.write(`${B}│\x1b[0m ${'─'.repeat(_inner)} ${B}│`);
227
+ ln();
210
228
 
211
229
  // ── Account rows ──
212
230
  const sorted = [..._workers].sort((a, b) => {
@@ -218,14 +236,22 @@ function draw() {
218
236
  });
219
237
  const shown = sorted.slice(0, _maxAccounts);
220
238
 
239
+ _accountRows = [];
221
240
  for (let si = 0; si < shown.length; si++) {
222
241
  const w = shown[si];
223
242
  const wi = _workers.indexOf(w);
224
- const rowNum = hrRow + 1 + si;
225
- _accountRows[wi] = rowNum;
226
- process.stdout.write(buildAccountRow(w, wi, col));
243
+ _accountRows[wi] = r;
244
+ row(r++); clrLine();
245
+ process.stdout.write(accountRow(w, wi, col));
246
+ ln();
227
247
  }
228
248
 
249
+ // ── Totals divider ──
250
+ _totalsRow = r;
251
+ row(r++); clrLine();
252
+ process.stdout.write(`${B}│\x1b[0m ${'─'.repeat(_inner)} ${B}│`);
253
+ ln();
254
+
229
255
  // ── Totals ──
230
256
  let totalCoins = 0, totalBal = 0;
231
257
  for (const w of _workers) {
@@ -233,40 +259,35 @@ function draw() {
233
259
  totalBal += w.stats.balance || 0;
234
260
  }
235
261
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
262
+ const balStr = totalBal > 0 ? '⏣' + totalBal.toLocaleString() : '—';
263
+ const balPadded = balStr.padEnd(col.bal);
264
+ const earnedStr = stripAnsi(fmtCoins(totalCoins));
236
265
 
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 `);
240
- process.stdout.write(`${c.bold}Σ${c.reset} `);
241
- process.stdout.write(`${DIM}${padL(_workers.length, 2)} acc${c.reset} `);
242
- process.stdout.write(`${' '.repeat(col.name)} `);
243
- process.stdout.write(`${' '.repeat(col.cmd)} `);
244
- process.stdout.write(`${padL(totalBal > 0 ? '⏣' + totalBal.toLocaleString() : '—', col.bal)} `);
245
- process.stdout.write(`${' '.repeat(col.ls)} `);
246
- process.stdout.write(`${' '.repeat(col.lv)} `);
247
- process.stdout.write(`${fmtCoins(totalCoins)} `);
248
- process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(_inner));
249
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
266
+ row(r++); clrLine();
267
+ process.stdout.write(
268
+ `${B}│\x1b[0m ${c.bold}Σ${c.reset} ${DIM}${padL(_workers.length, 2)} acc${c.reset} ${' '.repeat(col.name + col.cmd)} ${balPadded} ${' '.repeat(col.ls + col.lv)} ${fmtCoins(totalCoins)}${' '.repeat(_inner - 2 - stripAnsi(` ${c.bold}Σ${c.reset} ${DIM}${padL(_workers.length, 2)} acc${c.reset} ${' '.repeat(col.name + col.cmd)} ${balPadded} ${' '.repeat(col.ls + col.lv)} ${fmtCoins(totalCoins)}`))}${B}│`
269
+ );
270
+ ln();
250
271
 
251
272
  // ── Bottom ──
252
- process.stdout.write(`\x1b[38;2;77;212;238m└${'─'.repeat(_inner)}┘\x1b[0m\n`);
273
+ _bottomRow = r;
274
+ row(r++); clrLine();
275
+ process.stdout.write(`${B}└${'─'.repeat(_inner)}┘`);
276
+ ln();
253
277
  }
254
278
 
255
- // ── Update ONE account row in place ─────────────────────────
279
+ // ── Update ONE account row in-place ─────────────────────────
256
280
  function updateAccountRow(accountIdx) {
257
- const row = _accountRows[accountIdx];
258
- if (!row) return; // not visible
259
-
281
+ if (!_live) return;
282
+ const rowNum = _accountRows[accountIdx];
283
+ if (!rowNum) return; // not visible
260
284
  const w = _workers[accountIdx];
261
285
  if (!w) return;
286
+ const col = getCol();
262
287
 
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));
288
+ row(rowNum); clrLine();
289
+ process.stdout.write(accountRow(w, accountIdx, col));
290
+ ln();
270
291
  }
271
292
 
272
293
  // ── Gradient line ─────────────────────────────────────────────
@@ -280,7 +301,7 @@ function gradientLine(text, r1, g1, b1, r2, g2, b2) {
280
301
  }
281
302
 
282
303
  // ── Event tracking ────────────────────────────────────────────
283
- let _eventLines = []; // [accountIdx] = [{text, ts}]
304
+ let _eventLines = [];
284
305
  const MAX_EVENTS = 15;
285
306
 
286
307
  // ── Public API ────────────────────────────────────────────────
@@ -307,7 +328,6 @@ function stop() {
307
328
  function setLive(val) { _live = val; }
308
329
  function setPhase(phase) { _phase = phase; }
309
330
 
310
- // log: update account row in-place + append event below box
311
331
  function log(accountIdx, msg) {
312
332
  const now = new Date();
313
333
  const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
@@ -316,13 +336,11 @@ function log(accountIdx, msg) {
316
336
  if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
317
337
  _eventLines[accountIdx].push({ text: msg, ts });
318
338
  if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
319
- // Update the row in place
320
339
  if (_live) updateAccountRow(accountIdx);
321
340
  }
322
341
 
323
342
  if (!_live) return;
324
343
 
325
- // Append event below the box (after the bottom border)
326
344
  const col2 = accountIdx >= 0 ? wc(accountIdx) : c.cyan;
327
345
  const name = accountIdx >= 0 ? trunc(_workers[accountIdx]?.username || '?', 14) : 'GLOBAL';
328
346
  process.stdout.write(`${col2}${name}${c.reset} ${DIM}[${ts}]${c.reset} ${msg}\n`);
@@ -330,22 +348,17 @@ function log(accountIdx, msg) {
330
348
 
331
349
  function logGlobal(msg) { log(-1, msg); }
332
350
 
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 ──
351
+ // ── Refresh ──────────────────────────────────────────────────
337
352
  let _refreshTimer = null;
338
353
  function startRefresh() {
339
354
  if (_refreshTimer) return;
340
355
  _refreshTimer = setInterval(() => {
341
356
  if (!_live) return;
342
- for (let i = 0; i < _workers.length; i++) {
343
- updateAccountRow(i);
344
- }
357
+ for (let i = 0; i < _workers.length; i++) updateAccountRow(i);
345
358
  }, 1500);
346
359
  }
347
360
  function stopRefresh() {
348
361
  if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
349
362
  }
350
363
 
351
- module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase, updateAccountRow, fullRedraw, startRefresh, stopRefresh };
364
+ module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase, updateAccountRow, startRefresh, stopRefresh };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.53.0",
3
+ "version": "8.56.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"