dankgrinder 8.45.0 → 8.48.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 +7 -0
  2. package/lib/ui.js +144 -60
  3. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -2826,6 +2826,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2826
2826
  const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
2827
2827
  const randomLoginGap = () => LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS ? LOGIN_GAP_MIN_MS : LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
2828
2828
 
2829
+ ui.setPhase('login');
2829
2830
  ui.log(-1, `Logging in ${accounts.length} accounts...`);
2830
2831
  const BATCH_SIZE = 10;
2831
2832
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
@@ -2874,6 +2875,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2874
2875
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
2875
2876
 
2876
2877
  // ── Phase 2: Inventory check ────────────────────────────────────
2878
+ ui.setPhase('inventory');
2877
2879
  ui.log(-1, `Checking inventory...`);
2878
2880
  let invFailed = 0;
2879
2881
  await Promise.all(activeWorkers.map(async (w) => {
@@ -2887,6 +2889,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2887
2889
  }
2888
2890
 
2889
2891
  // ── Phase 2.5: Balance check ───────────────────────────────────
2892
+ ui.setPhase('balance');
2890
2893
  ui.log(-1, `Checking balances...`);
2891
2894
  for (const w of activeWorkers) {
2892
2895
  try { await w.checkBalance(true); } catch {}
@@ -2899,6 +2902,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2899
2902
  ui.log(-1, `Balance: +⏣${totalCoins.toLocaleString()} across ${activeWorkers.length} accounts`);
2900
2903
 
2901
2904
  // ── Phase 2.75: DM history check ────────────────────────────────
2905
+ ui.setPhase('dms');
2902
2906
  ui.log(-1, `Checking DM history...`);
2903
2907
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
2904
2908
  for (const w of activeWorkers) {
@@ -2924,9 +2928,11 @@ async function start(apiKey, apiUrl, opts = {}) {
2924
2928
  ui.log(-1, `DM: ${parts.length > 0 ? parts.join(', ') : 'clean'}`);
2925
2929
 
2926
2930
  // ── Phase 3: Start grind loops ───────────────────────────────────
2931
+ ui.setPhase('grinding');
2927
2932
  ui.log(-1, `Starting ${activeWorkers.length} grind loops...`);
2928
2933
  ui.setLive(true);
2929
2934
  ui.draw();
2935
+ ui.startRefresh(3000);
2930
2936
  for (const w of activeWorkers) {
2931
2937
  if (!shutdownCalled) w.grindLoop();
2932
2938
  }
@@ -2997,6 +3003,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2997
3003
  shutdownInProgress = true;
2998
3004
  shutdownCalled = true;
2999
3005
  setDashboardActive(false);
3006
+ ui.stopRefresh();
3000
3007
  ui.stop();
3001
3008
  process.stdout.write(c.show + '\n');
3002
3009
 
package/lib/ui.js CHANGED
@@ -1,6 +1,5 @@
1
1
  /**
2
- * CLI Live Dashboard — clean box at top, events stream below.
3
- * Box drawn once. Events appended line-by-line. No flicker.
2
+ * CLI Live Dashboard — ASCII banner, per-account status table, event stream below.
4
3
  */
5
4
 
6
5
  let _startTime = Date.now();
@@ -8,11 +7,26 @@ let _workers = [];
8
7
  let _isShuttingDown = () => false;
9
8
  let _version = '0.0.0';
10
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
11
15
 
12
16
  // ── Spinner frames ────────────────────────────────────────────
13
17
  const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
14
18
  function spinnerFrame() { return SPINNER[Math.floor(Date.now() / 150) % SPINNER.length]; }
15
19
 
20
+ // ── Compact ASCII art banner (fits in 80-col box) ─────────────
21
+ const BANNER_LINES = [
22
+ ' ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗ DANKGRINDER',
23
+ ' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║ v' + 'PLACEHOLDER',
24
+ ' ██║ ██║██║ ██║██╔██╗ ██║██║ ███╗█████╗ ██║ ██║██╔██╗ ██║',
25
+ ' ██║ ██║██║ ██║██║╚██╗██║██║ ██║██╔══╝ ██║ ██║██║╚██╗██║',
26
+ ' ██████╔╝╚██████╔╝██║ ╚████║╚██████╔╝███████╗╚██████╔╝██║ ╚████║',
27
+ ' ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝',
28
+ ];
29
+
16
30
  // ── Palette ───────────────────────────────────────────────────
17
31
  const PALETTE = [
18
32
  '\x1b[38;2;77;212;238m', '\x1b[38;2;255;194;77m', '\x1b[38;2;130;210;100m',
@@ -30,12 +44,14 @@ const c = {
30
44
  yellow: '\x1b[38;2;255;220;80m',
31
45
  cyan: '\x1b[38;2;80;220;255m',
32
46
  blue: '\x1b[38;2;100;160;255m',
47
+ white: '\x1b[37m',
33
48
  };
34
49
  const DIM = c.dim;
35
50
 
36
51
  function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
37
52
  function padR(s, n) { return trunc(s, n).padEnd(n); }
38
53
  function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
54
+ function stripAnsi(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, ''); }
39
55
 
40
56
  function fmtUptime() {
41
57
  const s = Math.floor((Date.now() - _startTime) / 1000);
@@ -50,7 +66,7 @@ function fmtUptime() {
50
66
 
51
67
  // ── Status helpers ─────────────────────────────────────────────
52
68
  function statusColor(w) {
53
- if (!w.loggedIn || !w.channel) return c.red;
69
+ if (!w.channel) return c.red;
54
70
  if (w.paused || w.dashboardPaused) return c.yellow;
55
71
  if (w.busy || w._invRunning || w._sellRunning) return c.yellow;
56
72
  if (Date.now() < w.globalCooldownUntil) return c.blue;
@@ -58,12 +74,12 @@ function statusColor(w) {
58
74
  }
59
75
 
60
76
  function statusText(w) {
61
- if (!w.loggedIn || !w.channel) return 'CONNECTING';
77
+ if (!w.channel) return spinnerFrame() + ' CONN';
62
78
  if (w.paused) return 'PAUSED';
63
79
  if (w.dashboardPaused) return 'HELD';
64
80
  if (w._alert?.type === 'death') return 'DEAD';
65
81
  if (w._alert?.type === 'lowls') return 'LOW LS';
66
- if (w.busy || w._invRunning || w._sellRunning) return spinnerFrame() + ' BUSY';
82
+ if (w.busy || w._invRunning || w._sellRunning) return spinnerFrame() + ' WORK';
67
83
  if (Date.now() < w.globalCooldownUntil) {
68
84
  const wait = Math.ceil((w.globalCooldownUntil - Date.now()) / 1000);
69
85
  return '⏳' + wait + 's';
@@ -71,68 +87,112 @@ function statusText(w) {
71
87
  return '● READY';
72
88
  }
73
89
 
90
+ // ── Format helpers ─────────────────────────────────────────────
91
+ function fmtCoins(n) {
92
+ if (!n && n !== 0) return DIM + '—' + c.reset;
93
+ if (n >= 1_000_000) return c.green + '+' + (n / 1_000_000).toFixed(1) + 'M' + c.reset;
94
+ if (n >= 1_000) return c.green + '+' + (n / 1_000).toFixed(1) + 'k' + c.reset;
95
+ if (n > 0) return c.green + '+' + n + c.reset;
96
+ return DIM + '—' + c.reset;
97
+ }
98
+
99
+ function fmtLifesavers(w) {
100
+ const ls = w._lifesavers;
101
+ if (ls === undefined || ls === null) return DIM + '—' + c.reset;
102
+ if (ls === 0) return c.red + '0' + c.reset;
103
+ if (ls <= 2) return c.yellow + ls + c.reset;
104
+ return c.green + ls + c.reset;
105
+ }
106
+
107
+ function fmtLevel(w) {
108
+ const lv = w._level;
109
+ if (!lv) return DIM + '—' + c.reset;
110
+ return c.cyan + lv + c.reset;
111
+ }
112
+
74
113
  // ── Layout ────────────────────────────────────────────────────
75
114
  function layout() {
76
115
  const W = Math.min(process.stdout.columns || 100, 120);
77
116
  const rows = process.stdout.rows || 40;
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));
117
+ const maxAccounts = Math.min(_workers.length, Math.max(3, rows - 14));
80
118
  return { W, maxAccounts };
81
119
  }
82
120
 
83
- // ── Draw the fixed box ────────────────────────────────────────
121
+ // ── Draw the full dashboard ───────────────────────────────────
84
122
  function draw() {
85
123
  const { W, maxAccounts } = layout();
86
124
  const inner = W - 2;
87
125
 
88
- // ── Save cursor + clear screen ──
89
- process.stdout.write('\x1b[s'); // save cursor
90
- process.stdout.write('\x1b[2J\x1b[H'); // clear screen + home
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
+ }
91
136
 
92
- // ── Title ──
137
+ // ── Top border ──
93
138
  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`);
139
+
140
+ // ── ASCII banner (dynamic includes version) ──
141
+ const bannerWithVer = BANNER_LINES[1].replace('PLACEHOLDER', _version);
142
+ const bannerLines = [BANNER_LINES[0], bannerWithVer, ...BANNER_LINES.slice(2)];
143
+
144
+ for (let i = 0; i < bannerLines.length; i++) {
145
+ const line = bannerLines[i];
146
+ // Gradient: cyan → pink for row 0 and 1, dim for others
147
+ if (i < 2) {
148
+ 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`);
151
+ } 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`);
155
+ }
156
+ }
99
157
 
100
158
  // ── Status bar ──
101
- const running = _workers.filter(w => w.loggedIn && w.channel && !w.paused && !w.dashboardPaused).length;
159
+ const running = _workers.filter(w => w.channel && !w.paused && !w.dashboardPaused).length;
102
160
  const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
103
- const errors = _workers.filter(w => !w.loggedIn || !w.channel).length;
161
+ const errors = _workers.filter(w => !w.channel).length;
162
+ const phaseLabel = _phase === 'grinding' ? '' : ` ${c.dim}[${_phase}]${c.reset}`;
104
163
  const statusParts = [
105
164
  `${c.green}●${c.reset} ${running} online`,
106
165
  paused > 0 ? `${c.yellow}~${c.reset} ${paused} paused` : null,
107
166
  errors > 0 ? `${c.red}E${c.reset} ${errors} error` : null,
108
167
  `${DIM}Ctrl+C${c.reset}`,
109
168
  ].filter(Boolean);
110
- const statusStr = statusParts.join(' ');
169
+ const statusStr = statusParts.join(' ') + phaseLabel;
111
170
  const statusPad = ' '.repeat(Math.max(0, inner - stripAnsi(statusStr)));
112
171
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${statusStr}${statusPad}\x1b[38;2;77;212;238m│\x1b[0m\n`);
113
172
  process.stdout.write(`\x1b[38;2;77;212;238m├${'─'.repeat(inner)}┤\x1b[0m\n`);
114
173
 
115
174
  // ── Table header ──
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);
175
+ 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);
118
177
  col.name += nameExtra;
119
178
 
120
179
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
121
180
  process.stdout.write(`${c.bold}#${c.reset} `);
122
181
  process.stdout.write(`${c.bold}${padR('STATUS', col.st)}${c.reset} `);
123
182
  process.stdout.write(`${c.bold}${padR('ACCOUNT', col.name)}${c.reset} `);
124
- process.stdout.write(`${c.bold}${padR('LAST COMMAND', col.last)}${c.reset} `);
125
- process.stdout.write(`${c.bold}${padL('CMDS', col.cmds)}${c.reset} `);
126
- process.stdout.write(`${c.bold}${padL('OK%', col.ok)}${c.reset} `);
183
+ process.stdout.write(`${c.bold}${padR('DOING', col.cmd)}${c.reset} `);
184
+ process.stdout.write(`${c.bold}${padL('BAL', col.bal)}${c.reset} `);
185
+ process.stdout.write(`${c.bold}${padL('LS', col.ls)}${c.reset} `);
186
+ process.stdout.write(`${c.bold}${padL('LV', col.lv)}${c.reset} `);
127
187
  process.stdout.write(`${c.bold}${padL('EARNED', col.earned)}${c.reset} `);
128
188
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
129
189
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(inner)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
130
190
 
131
191
  // ── Account rows ──
132
192
  const sorted = [..._workers].sort((a, b) => {
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;
193
+ if (!a.channel !== !b.channel) return !a.channel ? 1 : -1;
194
+ const aA = a.channel && !a.paused && !a.dashboardPaused;
195
+ const bA = b.channel && !b.paused && !b.dashboardPaused;
136
196
  if (aA !== bA) return bA ? 1 : -1;
137
197
  return (b.stats.commands || 0) - (a.stats.commands || 0);
138
198
  });
@@ -144,17 +204,19 @@ function draw() {
144
204
  const col2 = wc(wi);
145
205
  const stCol = statusColor(w);
146
206
  const stTxt = statusText(w);
147
- const earned = w.stats.coins > 0 ? `${c.green}+${w.stats.coins.toLocaleString()}${c.reset}` : DIM + '—' + c.reset;
148
- const cmds = w.stats.commands || 0;
149
- const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
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);
150
211
 
151
212
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
152
213
  process.stdout.write(`${DIM}${padL(wi + 1, 2)}${c.reset} `);
153
214
  process.stdout.write(`${stCol}${padR(stTxt, col.st)}${c.reset} `);
154
- process.stdout.write(`${col2}${padR(w.username || '?', col.name)}${c.reset} `);
155
- process.stdout.write(`${DIM}${padR(w.lastStatus || 'idle', col.last)}${c.reset} `);
156
- process.stdout.write(`${padL(cmds, col.cmds)} `);
157
- process.stdout.write(`${padL(rate, col.ok)}% `);
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)} `);
158
220
  process.stdout.write(`${padL(earned, col.earned)} `);
159
221
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
160
222
  }
@@ -165,9 +227,10 @@ function draw() {
165
227
  }
166
228
 
167
229
  // ── Totals ──
168
- let totalCoins = 0, totalCmds = 0, totalOk = 0;
230
+ let totalCoins = 0, totalBal = 0, totalCmds = 0, totalOk = 0;
169
231
  for (const w of _workers) {
170
232
  totalCoins += w.stats.coins || 0;
233
+ totalBal += w.stats.balance || 0;
171
234
  totalCmds += w.stats.commands || 0;
172
235
  totalOk += w.stats.successes || 0;
173
236
  }
@@ -179,32 +242,35 @@ function draw() {
179
242
  process.stdout.write(`${c.bold}Σ${c.reset} `);
180
243
  process.stdout.write(`${DIM}${padL(_workers.length, 2)} acc${c.reset} `);
181
244
  process.stdout.write(`${' '.repeat(col.name)} `);
182
- process.stdout.write(`${' '.repeat(col.last)} `);
183
- process.stdout.write(`${padL(totalCmds, col.cmds)} `);
184
- process.stdout.write(`${padL(rate, col.ok)}% `);
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
- }
245
+ process.stdout.write(`${' '.repeat(col.cmd)} `);
246
+ process.stdout.write(`${padL(totalBal > 0 ? '⏣' + totalBal.toLocaleString() : '—', col.bal)} `);
247
+ process.stdout.write(`${' '.repeat(col.ls)} `);
248
+ process.stdout.write(`${' '.repeat(col.lv)} `);
249
+ process.stdout.write(`${fmtCoins(totalCoins)} `);
190
250
  process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(inner));
191
251
  process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
192
252
 
193
253
  // ── Bottom ──
194
254
  process.stdout.write(`\x1b[38;2;77;212;238m└${'─'.repeat(inner)}┘\x1b[0m\n`);
195
255
 
196
- // Restore cursor to after the box
197
- process.stdout.write('\x1b[u');
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;
198
259
  }
199
260
 
200
- // ── Strip ANSI for length calc ───────────────────────────────
201
- function stripAnsi(s) {
202
- return String(s).replace(/\x1b\[[0-9;]*m/g, '');
261
+ // ── Gradient line ─────────────────────────────────────────────
262
+ function gradientLine(text, r1, g1, b1, r2, g2, b2) {
263
+ let out = '';
264
+ for (let i = 0; i < text.length; i++) {
265
+ const t = text.length <= 1 ? 0 : i / (text.length - 1);
266
+ 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]}`;
267
+ }
268
+ return out + '\x1b[0m';
203
269
  }
204
270
 
205
271
  // ── Event tracking ────────────────────────────────────────────
206
272
  let _eventLines = []; // [accountIdx] = [{text, ts}]
207
- const MAX_EVENTS = 3;
273
+ const MAX_EVENTS = 4;
208
274
 
209
275
  // ── Public API ────────────────────────────────────────────────
210
276
 
@@ -215,36 +281,54 @@ function init({ workers, isShuttingDown }) {
215
281
  _version = '0.0.0';
216
282
  _eventLines = [];
217
283
  _live = false;
284
+ _phase = 'init';
285
+ _boxRow = 0;
286
+ _boxHeight = 0;
287
+ _eventCount = 0;
288
+ _eventCountAtDraw = 0;
218
289
  }
219
290
 
220
- function drawBanner(version) { _version = version; }
291
+ function drawBanner(version) { _version = version || '0.0.0'; }
292
+
221
293
  function start() {}
222
- function stop() { _live = false; process.stdout.write(c.reset + '\n'); }
294
+ function stop() { _live = false; _phase = 'init'; process.stdout.write(c.reset + '\n'); }
295
+
223
296
  function setLive(val) { _live = val; }
224
297
 
298
+ function setPhase(phase) { _phase = phase; }
299
+
225
300
  // log: append event below the box (no redraw of box)
226
301
  function log(accountIdx, msg) {
227
302
  const now = new Date();
228
303
  const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
229
304
 
230
- // Track event per account
231
305
  if (accountIdx >= 0) {
232
306
  if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
233
307
  _eventLines[accountIdx].push({ text: msg, ts });
234
308
  if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
235
309
  }
310
+ _eventCount++;
236
311
 
237
- if (!_live) return; // during login: just track, don't output yet
312
+ if (!_live) return;
238
313
 
239
- // ── Append event cleanly below the box ──
314
+ // Clean event line: colored name + timestamp + message
240
315
  const col2 = accountIdx >= 0 ? wc(accountIdx) : c.cyan;
241
316
  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');
317
+ process.stdout.write(`${col2}${name}${c.reset} ${DIM}[${ts}]${c.reset} ${msg}\n`);
244
318
  }
245
319
 
246
- function logGlobal(msg) {
247
- log(-1, msg);
320
+ function logGlobal(msg) { log(-1, msg); }
321
+
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);
329
+ }
330
+ function stopRefresh() {
331
+ if (_refreshInterval) { clearInterval(_refreshInterval); _refreshInterval = null; }
248
332
  }
249
333
 
250
- module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive };
334
+ module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase, startRefresh, stopRefresh };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.45.0",
3
+ "version": "8.48.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"