dankgrinder 8.41.0 → 8.43.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 +6 -8
  2. package/lib/ui.js +76 -127
  3. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -721,14 +721,11 @@ class AccountWorker {
721
721
  log(type, msg) {
722
722
  const stripped = msg.replace(/\x1b\[[0-9;]*m/g, '');
723
723
  if (type !== 'debug') this.lastStatus = stripped.substring(0, 28);
724
- // Route through UI
725
- const icons = { info: '.', success: '[OK]', error: '[X]', warn: '[!]', cmd: '>', coin: '$', buy: '#', bal: '*', debug: '.' };
726
- const icon = icons[type] || icons.info;
727
- const text = `${icon} ${this.username} ${msg}`;
724
+ // Route through UI — msg already has context, no need to prepend username
728
725
  if (typeof ui !== 'undefined') {
729
- try { ui.log(this.idx, text); } catch { process.stdout.write(text + '\n'); }
726
+ try { ui.log(this.idx, msg); } catch { process.stdout.write(msg + '\n'); }
730
727
  } else {
731
- process.stdout.write(text + '\n');
728
+ process.stdout.write(msg + '\n');
732
729
  }
733
730
  }
734
731
 
@@ -2512,6 +2509,7 @@ class AccountWorker {
2512
2509
  clearTimeout(timeoutId);
2513
2510
  this.username = this.client.user.tag || this.username;
2514
2511
  this.avatarUrl = this.client.user.displayAvatarURL?.({ format: 'png', dynamic: true, size: 128 }) || null;
2512
+ this.loggedIn = true; // so statusColor/statusText show correct state
2515
2513
 
2516
2514
  // Attach raw gateway logger for CV2 component capture
2517
2515
  rawLogger.attachRawLogger(this.client, { channelId: this.account.channel_id });
@@ -2872,7 +2870,6 @@ async function start(apiKey, apiUrl, opts = {}) {
2872
2870
  if (timedOutWorkers.length > 0) {
2873
2871
  ui.log(-1, `${timedOutWorkers.length} timed out (retrying in background)`);
2874
2872
  }
2875
- ui.draw();
2876
2873
 
2877
2874
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
2878
2875
 
@@ -2928,10 +2925,11 @@ async function start(apiKey, apiUrl, opts = {}) {
2928
2925
 
2929
2926
  // ── Phase 3: Start grind loops ───────────────────────────────────
2930
2927
  ui.log(-1, `Starting ${activeWorkers.length} grind loops...`);
2928
+ ui.setLive(true);
2929
+ ui.draw();
2931
2930
  for (const w of activeWorkers) {
2932
2931
  if (!shutdownCalled) w.grindLoop();
2933
2932
  }
2934
- ui.draw();
2935
2933
 
2936
2934
  // Cluster heartbeat — lets other nodes see this node is alive
2937
2935
  if (CLUSTER_ENABLED) {
package/lib/ui.js CHANGED
@@ -1,42 +1,18 @@
1
1
  /**
2
- * CLI Live Dashboard — box design, loading animations, per-account status.
3
- * Everything inside a single box. Events stream below. No duplicate output.
2
+ * CLI Live Dashboard — clean box at top, events stream below.
3
+ * Box drawn once. Events appended line-by-line. No flicker.
4
4
  */
5
5
 
6
6
  let _startTime = Date.now();
7
7
  let _workers = [];
8
8
  let _isShuttingDown = () => false;
9
9
  let _version = '0.0.0';
10
+ let _live = false;
10
11
 
11
12
  // ── Spinner frames ────────────────────────────────────────────
12
13
  const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
13
14
  function spinnerFrame() { return SPINNER[Math.floor(Date.now() / 150) % SPINNER.length]; }
14
15
 
15
- // ── Big ASCII art banner ──────────────────────────────────────
16
- const BANNER_LINES = [
17
- ' ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗',
18
- ' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║',
19
- ' ██║ ██║██║ ██║██╔██╗ ██║██║ ███╗█████╗ ██║ ██║██╔██╗ ██║',
20
- ' ██║ ██║██║ ██║██║╚██╗██║██║ ██║██╔══╝ ██║ ██║██║╚██╗██║',
21
- ' ██████╔╝╚██████╔╝██║ ╚████║╚██████╔╝███████╗╚██████╔╝██║ ╚████║',
22
- ' ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝',
23
- ' ██╗██╗ ██╗███╗ ██╗ ██████╗ ██╗ ███████╗██████╗ ',
24
- ' ██║██║ ██║████╗ ██║██╔═══██╗██║ ██╔════╝██╔══██╗',
25
- ' ███████╗███████╗ ██║██║ ██║██╔██╗ ██║██║ ██║██║ █████╗ ██████╔╝',
26
- ' ╚════██║╚════██║ ██║██║ ██║██║╚██╗██║██║ ██║██║ ██╔══╝ ██╔══██╗',
27
- ' ██║ ██║ ██║╚██████╔╝██║ ╚████║╚██████╔╝███████╗███████╗██║ ██║',
28
- ' ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝',
29
- ];
30
-
31
- function gradientLine(text, r1, g1, b1, r2, g2, b2) {
32
- let out = '';
33
- for (let i = 0; i < text.length; i++) {
34
- const t = text.length <= 1 ? 0 : i / (text.length - 1);
35
- out += `\x1b[38;2;${Math.round(r1 + (r2 - r1) * t)};${Math.round(g1 + (g2 - g1) * t)};${Math.round(b1 + (b2 - b1) * t)}m${text[i]}`;
36
- }
37
- return out + '\x1b[0m';
38
- }
39
-
40
16
  // ── Palette ───────────────────────────────────────────────────
41
17
  const PALETTE = [
42
18
  '\x1b[38;2;77;212;238m', '\x1b[38;2;255;194;77m', '\x1b[38;2;130;210;100m',
@@ -60,11 +36,6 @@ const DIM = c.dim;
60
36
  function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
61
37
  function padR(s, n) { return trunc(s, n).padEnd(n); }
62
38
  function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
63
- function padC(s, n) {
64
- s = String(s || '');
65
- const pad = Math.max(0, n - s.length);
66
- return ' '.repeat(Math.floor(pad / 2)) + s + ' '.repeat(Math.ceil(pad / 2));
67
- }
68
39
 
69
40
  function fmtUptime() {
70
41
  const s = Math.floor((Date.now() - _startTime) / 1000);
@@ -79,7 +50,7 @@ function fmtUptime() {
79
50
 
80
51
  // ── Status helpers ─────────────────────────────────────────────
81
52
  function statusColor(w) {
82
- if (!w.running || !w.channel) return c.red;
53
+ if (!w.loggedIn || !w.channel) return c.red;
83
54
  if (w.paused || w.dashboardPaused) return c.yellow;
84
55
  if (w.busy || w._invRunning || w._sellRunning) return c.yellow;
85
56
  if (Date.now() < w.globalCooldownUntil) return c.blue;
@@ -87,7 +58,7 @@ function statusColor(w) {
87
58
  }
88
59
 
89
60
  function statusText(w) {
90
- if (!w.running || !w.channel) return 'ERROR';
61
+ if (!w.loggedIn || !w.channel) return 'CONNECTING';
91
62
  if (w.paused) return 'PAUSED';
92
63
  if (w.dashboardPaused) return 'HELD';
93
64
  if (w._alert?.type === 'death') return 'DEAD';
@@ -102,62 +73,48 @@ function statusText(w) {
102
73
 
103
74
  // ── Layout ────────────────────────────────────────────────────
104
75
  function layout() {
105
- const W = Math.min(process.stdout.columns || 120, 120);
76
+ const W = Math.min(process.stdout.columns || 100, 120);
106
77
  const rows = process.stdout.rows || 40;
107
- const bannerH = BANNER_LINES.length + 1; // +1 for blank line
108
- const statusH = 1;
109
- const headerH = 3; // blank + headers + hr
110
- const totalsH = 2; // totals + hr
111
- const footerH = 1; // blank
112
- const maxAccounts = Math.min(_workers.length, Math.max(3, rows - bannerH - statusH - headerH - totalsH - footerH - 8));
113
- const eventH = Math.max(3, rows - bannerH - statusH - headerH - totalsH - footerH - maxAccounts);
114
- return { W, bannerH, statusH, headerH, totalsH, footerH, maxAccounts, eventH };
78
+ // Box: header(2) + title(1) + gap(1) + statusBar(1) + divider(1) + headerRow(1) + hr(1) + maxAccounts + totals(2) + divider(1)
79
+ const maxAccounts = Math.min(_workers.length, Math.max(3, rows - 16));
80
+ return { W, maxAccounts };
115
81
  }
116
82
 
117
- // ── Draw the full box ────────────────────────────────────────
83
+ // ── Draw the fixed box ────────────────────────────────────────
118
84
  function draw() {
119
- const { W, bannerH, statusH, headerH, totalsH, footerH, maxAccounts, eventH } = layout();
120
- const T = '─'.repeat(W - 2); // inner width
121
-
122
- // ── Top of box ──
123
- // Move cursor to top-left of box area and overwrite in place (no full clear)
124
- process.stdout.write(`\x1b[${1};1H`);
125
- process.stdout.write(`\x1b[0J`); // clear from cursor to end of screen
126
- process.stdout.write(`\x1b[38;2;77;212;238m┌─${T}─┐\x1b[0m\n`);
127
-
128
- // ── Banner inside box ──
129
- for (const line of BANNER_LINES) {
130
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${gradientLine(line, 77, 212, 238, 255, 92, 147)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
131
- }
132
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
133
-
134
- // ── Status bar inside box ──
135
- const running = _workers.filter(w => w.running && w.channel && !w.paused && !w.dashboardPaused).length;
85
+ const { W, maxAccounts } = layout();
86
+ const inner = W - 2;
87
+
88
+ // ── Save cursor + clear screen ──
89
+ process.stdout.write('\x1b[s'); // save cursor
90
+ process.stdout.write('\x1b[2J\x1b[H'); // clear screen + home
91
+
92
+ // ── Title ──
93
+ process.stdout.write(`\x1b[38;2;77;212;238m┌${'─'.repeat(inner)}┐\x1b[0m\n`);
94
+ const title = ` ${c.bold}${c.cyan}DANK${c.reset}${c.bold}${c.white}GRINDER${c.reset} ${c.dim}v${_version}${c.reset} `;
95
+ const dots = ' '.repeat(Math.max(0, inner - stripAnsi(title) - 10));
96
+ const upStr = `${DIM}${fmtUptime()}${c.reset}`;
97
+ const upPad = ' '.repeat(Math.max(0, inner - stripAnsi(title) - stripAnsi(dots) - stripAnsi(upStr) - 2));
98
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m${title}${dots}${upStr}${upPad}\x1b[38;2;77;212;238m│\x1b[0m\n`);
99
+
100
+ // ── Status bar ──
101
+ const running = _workers.filter(w => w.loggedIn && w.channel && !w.paused && !w.dashboardPaused).length;
136
102
  const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
137
- const errors = _workers.filter(w => !w.running || !w.channel).length;
138
- const statusLine = [
139
- `${c.bold}v${_version}${c.reset}`,
140
- `${c.green}●${c.reset} online`,
141
- `${fmtUptime()}`,
142
- `${c.green}·${c.reset}${running}`,
143
- `${c.yellow}~${c.reset}${paused}`,
144
- errors > 0 ? `${c.red}E${c.reset}${errors}` : null,
103
+ const errors = _workers.filter(w => !w.loggedIn || !w.channel).length;
104
+ const statusParts = [
105
+ `${c.green}●${c.reset} ${running} online`,
106
+ paused > 0 ? `${c.yellow}~${c.reset} ${paused} paused` : null,
107
+ errors > 0 ? `${c.red}E${c.reset} ${errors} error` : null,
145
108
  `${DIM}Ctrl+C${c.reset}`,
146
- ].filter(Boolean).join(' ');
147
- const statusPad = W - 4 - statusLine.length;
148
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${statusLine}${' '.repeat(Math.max(0, statusPad))} \x1b[38;2;77;212;238m│\x1b[0m\n`);
149
- process.stdout.write(`\x1b[38;2;77;212;238m├─${T}─┤\x1b[0m\n`);
109
+ ].filter(Boolean);
110
+ const statusStr = statusParts.join(' ');
111
+ const statusPad = ' '.repeat(Math.max(0, inner - stripAnsi(statusStr)));
112
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${statusStr}${statusPad}\x1b[38;2;77;212;238m│\x1b[0m\n`);
113
+ process.stdout.write(`\x1b[38;2;77;212;238m├${'─'.repeat(inner)}┤\x1b[0m\n`);
150
114
 
151
115
  // ── Table header ──
152
- const col = {
153
- st: 8, // Status
154
- name: 18, // Account name
155
- last: 20, // Last command
156
- cmds: 6, // Commands
157
- ok: 4, // OK%
158
- earned: 10, // Earned
159
- };
160
- const nameExtra = Math.max(0, W - 4 - col.st - col.name - col.last - col.cmds - col.ok - col.earned - 14);
116
+ const col = { st: 9, name: 18, last: 24, cmds: 6, ok: 5, earned: 10 };
117
+ const nameExtra = Math.max(0, inner - col.st - col.name - col.last - col.cmds - col.ok - col.earned - 8);
161
118
  col.name += nameExtra;
162
119
 
163
120
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
@@ -169,13 +126,13 @@ function draw() {
169
126
  process.stdout.write(`${c.bold}${padL('OK%', col.ok)}${c.reset} `);
170
127
  process.stdout.write(`${c.bold}${padL('EARNED', col.earned)}${c.reset} `);
171
128
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
172
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
129
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(inner)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
173
130
 
174
131
  // ── Account rows ──
175
132
  const sorted = [..._workers].sort((a, b) => {
176
- if (!a.channel !== !b.channel) return !a.channel ? 1 : -1;
177
- const aA = a.running && a.channel && !a.paused && !a.dashboardPaused;
178
- const bA = b.running && b.channel && !b.paused && !b.dashboardPaused;
133
+ if (!a.loggedIn !== !b.loggedIn) return !a.loggedIn ? 1 : -1;
134
+ const aA = a.loggedIn && a.channel && !a.paused && !a.dashboardPaused;
135
+ const bA = b.loggedIn && b.channel && !b.paused && !b.dashboardPaused;
179
136
  if (aA !== bA) return bA ? 1 : -1;
180
137
  return (b.stats.commands || 0) - (a.stats.commands || 0);
181
138
  });
@@ -186,14 +143,14 @@ function draw() {
186
143
  const wi = _workers.indexOf(w);
187
144
  const col2 = wc(wi);
188
145
  const stCol = statusColor(w);
189
- const stText = statusText(w);
146
+ const stTxt = statusText(w);
190
147
  const earned = w.stats.coins > 0 ? `${c.green}+${w.stats.coins.toLocaleString()}${c.reset}` : DIM + '—' + c.reset;
191
148
  const cmds = w.stats.commands || 0;
192
149
  const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
193
150
 
194
151
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
195
152
  process.stdout.write(`${DIM}${padL(wi + 1, 2)}${c.reset} `);
196
- process.stdout.write(`${stCol}${padR(stText, col.st)}${c.reset} `);
153
+ process.stdout.write(`${stCol}${padR(stTxt, col.st)}${c.reset} `);
197
154
  process.stdout.write(`${col2}${padR(w.username || '?', col.name)}${c.reset} `);
198
155
  process.stdout.write(`${DIM}${padR(w.lastStatus || 'idle', col.last)}${c.reset} `);
199
156
  process.stdout.write(`${padL(cmds, col.cmds)} `);
@@ -203,7 +160,8 @@ function draw() {
203
160
  }
204
161
 
205
162
  if (sorted.length > maxAccounts) {
206
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${DIM}+${sorted.length - maxAccounts} more accounts${' '.repeat(Math.max(0, W - 24 - String(sorted.length - maxAccounts).length))}${c.reset} \x1b[38;2;77;212;238m│\x1b[0m\n`);
163
+ const extra = sorted.length - maxAccounts;
164
+ 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`);
207
165
  }
208
166
 
209
167
  // ── Totals ──
@@ -216,50 +174,32 @@ function draw() {
216
174
  const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
217
175
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
218
176
 
219
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \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`);
220
178
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
221
179
  process.stdout.write(`${c.bold}Σ${c.reset} `);
222
180
  process.stdout.write(`${DIM}${padL(_workers.length, 2)} acc${c.reset} `);
223
181
  process.stdout.write(`${' '.repeat(col.name)} `);
224
- process.stdout.write(`${DIM}${' '.repeat(col.last)}${c.reset} `);
182
+ process.stdout.write(`${' '.repeat(col.last)} `);
225
183
  process.stdout.write(`${padL(totalCmds, col.cmds)} `);
226
184
  process.stdout.write(`${padL(rate, col.ok)}% `);
227
- process.stdout.write(`${totalCoins > 0 ? c.green + padL('+' + totalCoins.toLocaleString(), col.earned) + c.reset : DIM + padL('—', col.earned) + c.reset} `);
228
- process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(W - 4));
185
+ if (totalCoins > 0) {
186
+ process.stdout.write(`${c.green}${padL('+' + totalCoins.toLocaleString(), col.earned)}${c.reset} `);
187
+ } else {
188
+ process.stdout.write(`${DIM}${padL('—', col.earned)}${c.reset} `);
189
+ }
190
+ process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(inner));
229
191
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
230
192
 
231
- // ── Events section ──
232
- process.stdout.write(`\x1b[38;2;77;212;238m├─${T}─┤\x1b[0m\n`);
233
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${c.bold}EVENTS${c.reset}${' '.repeat(Math.max(0, W - 10))}\x1b[38;2;77;212;238m│\x1b[0m\n`);
234
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
235
-
236
- // Show recent events per account
237
- const evLines = [];
238
- for (let i = 0; i < _workers.length; i++) {
239
- if (_eventLines[i] && _eventLines[i].length > 0) {
240
- const latest = _eventLines[i][_eventLines[i].length - 1];
241
- const col2 = wc(i);
242
- const name = trunc(_workers[i]?.username || '?', 14);
243
- evLines.push({ i, text: latest.text, ts: latest.ts, col: col2, name });
244
- }
245
- }
193
+ // ── Bottom ──
194
+ process.stdout.write(`\x1b[38;2;77;212;238m└${'─'.repeat(inner)}┘\x1b[0m\n`);
246
195
 
247
- for (let i = 0; i < Math.min(eventH, evLines.length); i++) {
248
- const ev = evLines[evLines.length - 1 - i];
249
- const now = new Date();
250
- const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
251
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
252
- process.stdout.write(`${ev.col}${padR(ev.name, 14)}${c.reset} `);
253
- process.stdout.write(`${DIM}[${ts}]${c.reset} ${ev.text}${' '.repeat(Math.max(0, W - 20 - ev.name.length - ev.text.length))}`);
254
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
255
- }
256
-
257
- for (let i = 0; i < Math.max(0, eventH - evLines.length); i++) {
258
- process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${' '.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
259
- }
196
+ // Restore cursor to after the box
197
+ process.stdout.write('\x1b[u');
198
+ }
260
199
 
261
- // ── Bottom of box ──
262
- process.stdout.write(`\x1b[38;2;77;212;238m└─${T}─┘\x1b[0m\n`);
200
+ // ── Strip ANSI for length calc ───────────────────────────────
201
+ function stripAnsi(s) {
202
+ return String(s).replace(/\x1b\[[0-9;]*m/g, '');
263
203
  }
264
204
 
265
205
  // ── Event tracking ────────────────────────────────────────────
@@ -274,28 +214,37 @@ function init({ workers, isShuttingDown }) {
274
214
  _isShuttingDown = isShuttingDown || (() => false);
275
215
  _version = '0.0.0';
276
216
  _eventLines = [];
217
+ _live = false;
277
218
  }
278
219
 
279
220
  function drawBanner(version) { _version = version; }
280
221
  function start() {}
281
- function stop() { process.stdout.write(c.reset + '\n'); }
222
+ function stop() { _live = false; process.stdout.write(c.reset + '\n'); }
223
+ function setLive(val) { _live = val; }
282
224
 
225
+ // log: append event below the box (no redraw of box)
283
226
  function log(accountIdx, msg) {
284
227
  const now = new Date();
285
228
  const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
286
229
 
230
+ // Track event per account
287
231
  if (accountIdx >= 0) {
288
232
  if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
289
233
  _eventLines[accountIdx].push({ text: msg, ts });
290
234
  if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
291
235
  }
292
236
 
293
- draw();
237
+ if (!_live) return; // during login: just track, don't output yet
238
+
239
+ // ── Append event cleanly below the box ──
240
+ const col2 = accountIdx >= 0 ? wc(accountIdx) : c.cyan;
241
+ const name = accountIdx >= 0 ? trunc(_workers[accountIdx]?.username || '?', 14) : 'GLOBAL';
242
+ const text = DIM + name + c.reset + ' ' + DIM + ts + c.reset + ' ' + msg;
243
+ process.stdout.write(text + '\n');
294
244
  }
295
245
 
296
246
  function logGlobal(msg) {
297
- // Global events go to account 0
298
247
  log(-1, msg);
299
248
  }
300
249
 
301
- module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop };
250
+ module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.41.0",
3
+ "version": "8.43.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"