dankgrinder 8.74.0 → 8.75.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 (2) hide show
  1. package/lib/ui.js +178 -374
  2. package/package.json +1 -1
package/lib/ui.js CHANGED
@@ -1,47 +1,74 @@
1
- /**
2
- * CLI Live Dashboard — cursor-positioned, fixed-width columns.
3
- * Design: Option 1 box + animated dot status + current logs + events box below.
4
- */
1
+ const logUpdate = require('log-update');
2
+ const chalk = require('chalk');
3
+ const figlet = require('figlet');
5
4
 
6
- let _startTime = Date.now();
7
5
  let _workers = [];
8
6
  let _version = '0.0.0';
9
7
  let _live = false;
10
- let _phase = 'init';
8
+ let _startTime = Date.now();
9
+ let _events = [];
10
+ const MAX_EVENTS = 5;
11
+ let _refreshTimer = null;
11
12
 
12
- // Terminal dimensions
13
- let _W = 110;
14
- let _inner = 108;
15
- let _maxAccounts = 4;
13
+ function applyGradient(str) {
14
+ const lines = str.split('\n');
15
+ const maxL = Math.max(...lines.map(l => l.length));
16
+ let res = '';
17
+ lines.forEach(line => {
18
+ for (let c = 0; c < line.length; c++) {
19
+ let t = c / (maxL || 1);
20
+ res += chalk.rgb(
21
+ Math.floor(255 - (t * 255)),
22
+ Math.floor(t * 200),
23
+ Math.floor(t * 255) + 100 > 255 ? 255 : Math.floor(t * 255) + 100
24
+ )(line[c]);
25
+ }
26
+ res += '\n';
27
+ });
28
+ return res;
29
+ }
16
30
 
17
- // Row map
18
- let _accountRows = []; // _accountRows[accountIdx] = starting row
19
- let _bottomRow = 0;
31
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
32
+ function getSpinner() { return SPINNER_FRAMES[Math.floor(Date.now() / 80) % SPINNER_FRAMES.length]; }
20
33
 
21
- // ── Fixed column widths ─────────────────────────────────────────
22
- // # | ACCOUNT | BAL | LS | LV | LOGS
23
- // 3 19 12 3 4 rest
24
- const NUM_W = 3; // " # "
25
- const ACC_W = 19; // dot + name (padded to 18, dot takes 1)
26
- const BAL_W = 12; // "⏣95,230" padded
27
- const LS_W = 3; // lifesavers
28
- const LV_W = 4; // level
29
- // ROW = '│ ' + # + ' ' + dot + name + ' ' + bal + ' ' + ls + ' ' + lv + ' ' + logs + ' │'
30
- // = 2 + 2 + 1 + 1 + 18 + 1 + 12 + 1 + 3 + 1 + 4 + 1 + N + 2 = 50 + N
31
- // LOGS_W computed dynamically
34
+ const LOAD_FRAMES = ['[= ]', '[== ]', '[=== ]', '[ ===]', '[ ==]', '[ =]', '[ ==]', '[ ===]', '[=== ]', '[== ]'];
35
+ function getLoader() { return LOAD_FRAMES[Math.floor(Date.now() / 150) % LOAD_FRAMES.length]; }
32
36
 
33
- // ── Spinner frames ──────────────────────────────────────────────
34
- const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
35
- function spinnerFrame() { return SPINNER[Math.floor(Date.now() / 150) % SPINNER.length]; }
37
+ const ACC_COLORS = ['#ff0054', '#ffbd00', '#390099', '#9e0059', '#ff5400', '#00f5d4', '#00bbf9', '#fee440', '#f15bb5', '#9b5de5'];
38
+ function getAccountColor(str) {
39
+ let hash = 0;
40
+ for (let i = 0; i < str.length; i++) hash = str.charCodeAt(i) + ((hash << 5) - hash);
41
+ return chalk.hex(ACC_COLORS[Math.abs(hash) % ACC_COLORS.length]);
42
+ }
36
43
 
37
- // ── Status dot + color ───────────────────────────────────────────
38
44
  const STATUS_DOT = {
39
- online: { dot: '●', color: '\x1b[38;2;80;255;120m' }, // green
40
- busy: { dot: '◐', color: '\x1b[38;2;255;220;80m' }, // yellow
41
- paused: { dot: '○', color: '\x1b[38;2;180;180;180m' }, // dim gray
42
- dead: { dot: '✗', color: '\x1b[38;2;255;80;100m' }, // red
43
- connect: { dot: '◯', color: '\x1b[38;2;255;180;80m' }, // orange
45
+ online: { dot: '●', color: chalk.hex('#50ff78') },
46
+ busy: { dot: '◐', color: chalk.hex('#ffdc50') },
47
+ paused: { dot: '○', color: chalk.hex('#b4b4b4') },
48
+ dead: { dot: '✗', color: chalk.hex('#ff5064') },
49
+ connect: { dot: '◯', color: chalk.hex('#ffb450') }
44
50
  };
51
+
52
+ function init({ workers }) { _workers = workers; _startTime = Date.now(); _events = []; _live = false; }
53
+
54
+ function drawBanner(version) {
55
+ _version = version || '0.0.0';
56
+ console.clear();
57
+ const titleStr = figlet.textSync('DANK GRINDER', { font: 'ANSI Shadow' });
58
+ console.log(chalk.bold(applyGradient(titleStr)));
59
+ console.log(chalk.bold.magenta(`v${_version} — ${getLoader()} Booting... `));
60
+ console.log();
61
+ }
62
+
63
+ function fmtUptime() {
64
+ const s = Math.floor((Date.now() - _startTime) / 1000);
65
+ if (s < 60) return `${s}s`;
66
+ const m = Math.floor(s / 60);
67
+ const h = Math.floor(m / 60);
68
+ if (h > 0) return `${h}h ${m % 60}m`;
69
+ return `${m}m`;
70
+ }
71
+
45
72
  function getDot(w) {
46
73
  if (!w.channel) return STATUS_DOT.connect;
47
74
  if (w.paused || w.dashboardPaused) return STATUS_DOT.paused;
@@ -51,173 +78,49 @@ function getDot(w) {
51
78
  return STATUS_DOT.online;
52
79
  }
53
80
 
54
- // ── ANSI helpers ─────────────────────────────────────────────────
55
- const c = {
56
- reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
57
- green: '\x1b[38;2;80;255;120m',
58
- red: '\x1b[38;2;255;80;100m',
59
- yellow: '\x1b[38;2;255;220;80m',
60
- cyan: '\x1b[38;2;80;220;255m',
61
- };
62
- const DIM = c.dim;
63
- const B = '\x1b[38;2;77;212;238m'; // box border blue
64
-
65
- function row(n) { process.stdout.write(`\x1b[${n};1H`); }
66
- function clrLine() { process.stdout.write('\x1b[2K'); }
67
- function ln() { process.stdout.write('\n'); }
68
- function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
69
- function padR(s, n) { return trunc(s, n).padEnd(n); }
70
- function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
71
- function stripAnsi(s) { return String(s || '').replace(/\x1b\[[0-9;]*m/g, ''); }
72
-
73
- // ── Format helpers ───────────────────────────────────────────────
74
- function fmtCoins(n) {
75
- if (!n && n !== 0) return '—';
76
- if (n >= 1_000_000) return '+' + (n / 1_000_000).toFixed(1) + 'M';
77
- if (n >= 1_000) return '+' + (n / 1_000).toFixed(1) + 'k';
78
- if (n > 0) return '+' + n;
79
- return '—';
80
- }
81
-
82
- function fmtBal(n) {
83
- if (!n && n !== 0) return DIM + '—' + c.reset;
84
- const s = n >= 1_000 ? n.toLocaleString() : String(n);
85
- return c.green + '⏣' + s + c.reset;
86
- }
87
-
88
- function fmtLifesavers(w) {
89
- const ls = w._lifesavers;
90
- if (ls === undefined || ls === null) return DIM + '—' + c.reset;
91
- if (ls === 0) return c.red + '0' + c.reset;
92
- if (ls <= 2) return c.yellow + ls + c.reset;
93
- return c.green + ls + c.reset;
94
- }
95
-
96
- function fmtLevel(w) {
97
- const lv = w._level;
98
- if (!lv) return DIM + '—' + c.reset;
99
- return c.cyan + padL(lv, 4) + c.reset;
100
- }
101
-
102
- // ── Layout ─────────────────────────────────────────────────────
103
- function layout() {
104
- _W = Math.min(process.stdout.columns || 110, 130);
105
- _inner = _W - 2;
106
- const rows = process.stdout.rows || 40;
107
- // Main box: banner(6) + status(1) + divider(1) + header(1) + hr(1) + accounts + totals(2) + bottom(1) = 13 + accounts
108
- _maxAccounts = Math.min(_workers.length, Math.max(3, rows - 16));
109
- }
110
-
111
- // ── Get LOGS column width ─────────────────────────────────────────
112
- function getLogsW() {
113
- // ROW = │ + # + ' ' + dot+name + ' ' + bal + ' ' + ls + ' ' + lv + ' ' + logs + ' │ + \n
114
- // 1 2 1 19 1 12 1 3 1 4 1 N 1 1 = 48+N
115
- const FIXED = NUM_W + 1 + ACC_W + 1 + BAL_W + 1 + LS_W + 1 + LV_W + 1 + 2;
116
- return Math.max(20, _inner - FIXED);
117
- }
118
-
119
- // ── Build one account row ─────────────────────────────────────────
120
- function accountRow(w, wi) {
121
- const LOG_W = getLogsW();
122
- const dot = getDot(w);
123
- const name = padR(trunc(w.username || '?', ACC_W - 1), ACC_W - 1); // -1 for dot
124
- const bal = w.stats.balance !== undefined ? w.stats.balance : null;
125
- const ls = fmtLifesavers(w);
126
- const lv = fmtLevel(w);
127
-
128
- // Current log: lastStatus or cooldown
129
- let logText = w.lastStatus || 'idle';
130
- if (w.globalCooldownUntil && Date.now() < w.globalCooldownUntil) {
131
- const s = Math.ceil((w.globalCooldownUntil - Date.now()) / 1000);
132
- logText = s > 60 ? `cooldown ${Math.ceil(s/60)}m` : `cooldown ${s}s`;
81
+ function addEvent(type, msg) {
82
+ const now = new Date();
83
+ const ts = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
84
+ const icons = { info: chalk.blue(''), warn: chalk.yellow('⚠'), error: chalk.red('✗'), success: chalk.green('✔') };
85
+ _events.unshift({ icon: icons[type] || icons.info, msg: chalk.bold(msg), ts });
86
+ if (_events.length > MAX_EVENTS) _events.pop();
87
+ if (_live) render();
88
+ }
89
+
90
+ function logGlobal(msg) { addEvent('info', msg); }
91
+ function log(accountIdx, msg) { if (!_live && accountIdx === -1) console.log(chalk.bold(msg)); }
92
+ function updateAccountRow() { }
93
+
94
+ // Precision width padding
95
+ function padFixed(str, len, isAnsiPrepared = false) {
96
+ if (!isAnsiPrepared) {
97
+ if (str.length > len) return str.substring(0, len - 1) + '…';
98
+ return str.padEnd(len, ' ');
99
+ } else {
100
+ const rawLen = str.replace(/\x1b\[[0-9;]*m/g, '').length;
101
+ if (rawLen < len) return str + ' '.repeat(len - rawLen);
102
+ return str; // Assume caller pre-truncated the inner text
133
103
  }
134
- if (w.paused || w.dashboardPaused) logText = 'paused';
135
- if (w._alert?.type === 'death') logText = 'DEAD — lifesavers?';
136
- const logPadded = padR(logText, LOG_W);
137
-
138
- const numStr = padL(String(wi + 1), NUM_W - 1); // " 1" or "10"
139
-
140
- return (
141
- `${B}│\x1b[0m` +
142
- `${DIM}${numStr}${c.reset} ` +
143
- `${dot.dot}${dot.color}${name}${c.reset} ` +
144
- `${fmtBal(bal)} ` +
145
- `${ls} ` +
146
- `${lv} ` +
147
- `${DIM}${logPadded}${c.reset} ` +
148
- `${B}│\x1b[0m`
149
- );
150
- }
151
-
152
- // ── Top border helper ─────────────────────────────────────────────
153
- function hRule(char) {
154
- process.stdout.write(`${B}│\x1b[0m${char.repeat(_inner)}${B}│`);
155
- }
156
-
157
- // ── Header row ───────────────────────────────────────────────────
158
- function headerRow(LOG_W) {
159
- return (
160
- `${B}│\x1b[0m` +
161
- `${c.bold}${padL('#', NUM_W - 1)}${c.reset} ` +
162
- `${c.bold}${padR('ACCOUNT', ACC_W)}${c.reset} ` +
163
- `${c.bold}${padL('BAL', BAL_W)}${c.reset} ` +
164
- `${c.bold}${padL('LS', LS_W)}${c.reset} ` +
165
- `${c.bold}${padL('LV', LV_W)}${c.reset} ` +
166
- `${c.bold}${padR('LOGS', LOG_W)}${c.reset} ` +
167
- `${B}│\x1b[0m`
168
- );
169
104
  }
170
105
 
171
- // ── Draw FULL box (called once on startup) ─────────────────────
172
- function draw() {
173
- layout();
174
- const LOG_W = getLogsW();
175
-
176
- // Clear screen + home
177
- process.stdout.write('\x1b[2J\x1b[H');
178
-
179
- let r = 1;
180
- const top = _inner;
181
-
182
- // ── Top border ──
183
- row(r++); clrLine();
184
- process.stdout.write(`${B}┌${'─'.repeat(top)}┐`);
185
- ln();
186
-
187
- // ── Title bar ──
188
- const up = fmtUptime();
189
- let totalCoins = 0, totalBal = 0;
190
- for (const w of _workers) { totalCoins += w.stats.coins || 0; totalBal += w.stats.balance || 0; }
191
- const totalBalStr = totalBal > 0 ? c.green + '⏣' + totalBal.toLocaleString() + c.reset : DIM + '—' + c.reset;
192
- const online = _workers.filter(w => w.channel && !w.paused && !w.dashboardPaused).length;
193
-
194
- const titleLeft = `${c.bold}DANKGRINDER${c.reset} ${c.dim}v${_version}${c.reset}`;
195
- const titleRight = `${c.green}●${c.reset} ${online} online ${totalBalStr} uptime ${up}`;
196
- const totalW = stripAnsi(titleLeft).length + stripAnsi(titleRight).length;
197
- const padding = Math.max(0, top - totalW);
198
- const leftPad = Math.floor(padding / 2);
199
- const rightPad = padding - leftPad;
200
-
201
- row(r++); clrLine();
202
- process.stdout.write(`${B}│\x1b[0m${' '.repeat(leftPad)}${titleLeft}${' '.repeat(rightPad)}${titleRight}${B}│`);
203
- ln();
204
-
205
- // ── Divider ──
206
- row(r++); clrLine();
207
- process.stdout.write(`${B}├${'─'.repeat(top)}┤`);
208
- ln();
209
-
210
- // ── Column header ──
211
- row(r++); clrLine();
212
- process.stdout.write(headerRow(LOG_W));
213
- ln();
214
-
215
- // ── HR ──
216
- row(r++); clrLine();
217
- process.stdout.write(`${B}│\x1b[0m${'─'.repeat(top)}${B}│`);
218
- ln();
219
-
220
- // ── Account rows ──
106
+ function render() {
107
+ if (!_live) return;
108
+ const cols = process.stdout.columns || 110;
109
+ const rows = process.stdout.rows || 30;
110
+
111
+ const titleStr = figlet.textSync('DANK GRINDER', { font: 'ANSI Shadow' });
112
+ let out = chalk.bold(applyGradient(titleStr));
113
+
114
+ const sep = chalk.hex('#4dd4ee').bold('━'.repeat(cols));
115
+ out += sep + '\n';
116
+
117
+ const wCols = { num: 4, name: 18, bal: 14, ls: 6, lv: 6 };
118
+ let logW = Math.max(15, cols - (wCols.num + wCols.name + wCols.bal + wCols.ls + wCols.lv + 12));
119
+
120
+ out += `${chalk.bold(' #').padEnd(wCols.num)} ${chalk.bold('ACCOUNT').padEnd(wCols.name)} ${chalk.bold('BAL').padEnd(wCols.bal)} ${chalk.bold('LS').padEnd(wCols.ls)} ${chalk.bold('LV').padEnd(wCols.lv)} ${chalk.bold('LOGS')} \n`;
121
+ out += sep + '\n';
122
+
123
+ const maxShown = Math.max(3, rows - 24);
221
124
  const sorted = [..._workers].sort((a, b) => {
222
125
  if (!a.channel !== !b.channel) return a.channel ? -1 : 1;
223
126
  const aA = a.channel && !a.paused && !a.dashboardPaused;
@@ -225,191 +128,92 @@ function draw() {
225
128
  if (aA !== bB) return aA ? -1 : 1;
226
129
  return (b.stats.commands || 0) - (a.stats.commands || 0);
227
130
  });
228
- const shown = sorted.slice(0, _maxAccounts);
229
-
230
- _accountRows = [];
231
- for (let si = 0; si < shown.length; si++) {
232
- const w = shown[si];
233
- const wi = _workers.indexOf(w);
234
- _accountRows[wi] = r;
235
- row(r++); clrLine();
236
- process.stdout.write(accountRow(w, wi));
237
- ln();
131
+
132
+ let totalCoins = 0, totalBal = 0, onlineCount = 0;
133
+ for (let i = 0; i < _workers.length; i++) {
134
+ const w = _workers[i];
135
+ totalCoins += w.stats.coins || 0;
136
+ totalBal += w.stats.balance || 0;
137
+ if (w.channel && !w.paused && !w.dashboardPaused) onlineCount++;
238
138
  }
239
139
 
240
- // ── Totals divider ──
241
- row(r++); clrLine();
242
- process.stdout.write(`${B}│\x1b[0m${'─'.repeat(top)}${B}│`);
243
- ln();
244
-
245
- // ── Totals row ──
246
- const coinsStr = fmtCoins(totalCoins);
247
- const coinsLabel = `${c.bold}Σ${c.reset} ${_workers.length} accounts`;
248
- const coinsVal = `${c.green}${coinsStr}${c.reset} coins`;
249
- const valLen = stripAnsi(coinsVal);
250
- const labelLen = stripAnsi(coinsLabel);
251
- const gap = Math.max(0, top - valLen - labelLen - 4);
252
-
253
- row(r++); clrLine();
254
- process.stdout.write(`${B}│\x1b[0m${' '.repeat(2)}${coinsLabel}${' '.repeat(gap)}${coinsVal}${' '.repeat(2)}${B}│`);
255
- ln();
256
-
257
- // ── Bottom border ──
258
- _bottomRow = r;
259
- row(r++); clrLine();
260
- process.stdout.write(`${B}└${'─'.repeat(top)}┘`);
261
- ln();
262
-
263
- // ── Events box header ──
264
- r++; // blank row
265
- row(r++); clrLine();
266
- process.stdout.write(`${B}┌${'─'.repeat(top)}┐`);
267
- ln();
268
- row(r++); clrLine();
269
- process.stdout.write(`${B}│\x1b[0m ${c.bold}EVENTS${c.reset}${' '.repeat(top - 10)}${B}│`);
270
- ln();
271
- row(r++); clrLine();
272
- process.stdout.write(`${B}├${'─'.repeat(top)}┤`);
273
- ln();
274
- }
275
-
276
- // ── Update ONE account row in-place ───────────────────────────────
277
- function updateAccountRow(accountIdx) {
278
- if (!_live) return;
279
- const rowNum = _accountRows[accountIdx];
280
- if (!rowNum) return;
281
- const w = _workers[accountIdx];
282
- if (!w) return;
283
- row(rowNum); clrLine();
284
- process.stdout.write(accountRow(w, accountIdx));
285
- ln();
286
- }
287
-
288
- // ── Event log tracking ─────────────────────────────────────────────
289
- const MAX_EVENTS = 8;
290
- let _events = [];
291
-
292
- function addEvent(type, msg) {
293
- const now = new Date();
294
- const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
295
- const icons = { info: 'ℹ', warn: '⚠', error: '✗', success: '✔', debug: '⌘' };
296
- const colors = { info: c.cyan, warn: c.yellow, error: c.red, success: c.green, debug: DIM };
297
- const icon = icons[type] || 'ℹ';
298
- const col = colors[type] || c.cyan;
299
-
300
- _events.unshift({ icon, col, msg, ts });
301
- if (_events.length > MAX_EVENTS) _events.pop();
302
- if (_live) drawEvents();
303
- }
304
-
305
- // ── Draw events box ───────────────────────────────────────────────
306
- function drawEvents() {
307
- if (!_live) return;
308
- const LOG_W = getLogsW();
309
- const inner = _inner;
310
-
311
- // Draw from _bottomRow + 2 (after the events box header rows)
312
- let r = _bottomRow + 2;
313
-
314
- for (let i = 0; i < MAX_EVENTS; i++) {
315
- row(r); clrLine();
316
- if (i < _events.length) {
317
- const ev = _events[i];
318
- const msgPadded = padR(`${ev.icon} ${ev.msg}`, inner - 30);
319
- const tsPadded = padL(`[${ev.ts}]`, 20);
320
- process.stdout.write(`${B}│\x1b[0m ${ev.col}${msgPadded}${c.reset}${tsPadded} ${B}│`);
140
+ const shown = sorted.slice(0, maxShown);
141
+ for (let i = 0; i < shown.length; i++) {
142
+ const w = shown[i];
143
+ const rawIdx = String(_workers.indexOf(w) + 1);
144
+
145
+ let logText = w.lastStatus || 'idle';
146
+ let activity = '';
147
+ if (w.globalCooldownUntil && Date.now() < w.globalCooldownUntil) {
148
+ const s = Math.ceil((w.globalCooldownUntil - Date.now()) / 1000);
149
+ logText = s > 60 ? `cooldown ${Math.ceil(s/60)}m` : `cd ${s}s`;
150
+ activity = chalk.yellow(getSpinner());
151
+ } else if (w.paused || w.dashboardPaused) {
152
+ logText = 'paused';
153
+ } else if (w.channel) {
154
+ activity = chalk.cyan(getSpinner());
321
155
  } else {
322
- process.stdout.write(`${B}│\x1b[0m${' '.repeat(inner)}${B}│`);
156
+ activity = chalk.gray(getLoader());
323
157
  }
324
- ln();
325
- r++;
326
- }
327
-
328
- // Bottom border of events box
329
- row(r); clrLine();
330
- process.stdout.write(`${B}└${'─'.repeat(inner)}┘`);
331
- ln();
332
- }
333
158
 
334
- // ── Gradient line (unused in new design but kept) ─────────────────
335
- function gradientLine(text, r1, g1, b1, r2, g2, b2) {
336
- let out = '';
337
- for (let i = 0; i < text.length; i++) {
338
- const t = text.length <= 1 ? 0 : i / (text.length - 1);
339
- 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]}`;
159
+ // Fixed widths construction
160
+ const numPart = padFixed(` ${rawIdx}`, wCols.num);
161
+
162
+ // Name Part
163
+ const dot = getDot(w);
164
+ let uName = w.username || '?';
165
+ // dot + space + name -> subtract 2 from allowed length
166
+ if (uName.length > wCols.name - 2) uName = uName.substring(0, wCols.name - 3) + '…';
167
+ const cName = `${dot.color(dot.dot)} ${getAccountColor(uName)(chalk.bold(uName))}`;
168
+ const namePart = padFixed(cName, wCols.name, true);
169
+
170
+ // Bal Part
171
+ let bVal = w.stats.balance !== undefined ? (w.stats.balance >= 1000 ? w.stats.balance.toLocaleString() : String(w.stats.balance)) : '—';
172
+ bVal = bVal.length > wCols.bal - 2 ? bVal.substring(0, wCols.bal - 3) + '…' : bVal;
173
+ const cBal = bVal === '—' ? chalk.dim('—') : chalk.bold.green(`⏣${bVal}`);
174
+ const balPart = padFixed(cBal, wCols.bal, true);
175
+
176
+ // LS Part
177
+ let lVal = w._lifesavers !== undefined ? String(w._lifesavers) : '—';
178
+ const lColor = lVal === '—' ? chalk.dim : (w._lifesavers === 0 ? chalk.bold.red : (w._lifesavers <= 2 ? chalk.bold.yellow : chalk.bold.cyan));
179
+ const lsPart = padFixed(lColor(lVal.substring(0, wCols.ls)), wCols.ls, true);
180
+
181
+ // LV Part
182
+ let lvVal = w._level !== undefined ? String(w._level) : '—';
183
+ const lvPart = padFixed(chalk.bold.magenta(lvVal.substring(0, wCols.lv)), wCols.lv, true);
184
+
185
+ // Log Part
186
+ let lStr = logText;
187
+ if (lStr.length > logW - 3) lStr = lStr.substring(0, logW - 4) + '…';
188
+ const cLog = `${activity} ${chalk.dim.bold(lStr)}`;
189
+ const logPart = padFixed(cLog, logW, true);
190
+
191
+ out += `${chalk.dim(numPart)} ${namePart} ${balPart} ${lsPart} ${lvPart} ${logPart}\n`;
192
+ }
193
+
194
+ if (_workers.length > maxShown) {
195
+ out += chalk.bold.dim(` ... and ${(_workers.length - maxShown).toLocaleString()} more accounts running ${getSpinner()}\n`);
340
196
  }
341
- return out + '\x1b[0m';
342
- }
343
197
 
344
- function fmtUptime() {
345
- const s = Math.floor((Date.now() - _startTime) / 1000);
346
- if (s < 60) return `${s}s`;
347
- const m = Math.floor(s / 60);
348
- const h = Math.floor(m / 60);
349
- if (h > 0) return `${h}h${m % 60}m`;
350
- return `${m}m`;
351
- }
198
+ out += sep + '\n';
199
+ const totC = totalCoins >= 1000000 ? (totalCoins/1000000).toFixed(1)+'M' : (totalCoins >= 1000 ? (totalCoins/1000).toFixed(1)+'k' : totalCoins);
200
+ out += ` ${chalk.bold( ACCOUNTS:')} ${chalk.bold.magenta(_workers.length)} │ ${chalk.bold('⚡ ONLINE:')} ${chalk.bold.cyan(onlineCount)} │ ${chalk.bold('💰 TOTAL:')} ${chalk.bold.green('⏣' + totalBal.toLocaleString())} │ ${chalk.bold('📈 GAINED:')} ${chalk.bold.yellow('+' + totC)} │ ${chalk.bold('⏱ UP:')} ${chalk.bold.dim(fmtUptime())}\n`;
201
+ out += sep + '\n';
202
+
203
+ out += chalk.bold.cyan(` 📻 LATEST EVENTS \n`) + chalk.dim.bold('─'.repeat(cols)) + '\n';
204
+ if (_events.length === 0) out += chalk.dim(' Quiet here...\n');
205
+ _events.forEach(ev => { out += chalk.dim(` [${ev.ts}] `) + `${ev.icon} ${ev.msg}\n`; });
206
+ out += chalk.dim.bold('─'.repeat(cols)) + '\n';
352
207
 
353
- // ── Public API ─────────────────────────────────────────────────────
354
- function init({ workers }) {
355
- _startTime = Date.now();
356
- _workers = workers;
357
- _version = '0.0.0';
358
- _events = [];
359
- _live = false;
360
- _phase = 'init';
361
- _accountRows = [];
208
+ logUpdate(out);
362
209
  }
363
210
 
364
- function drawBanner(version) { _version = version || '0.0.0'; }
365
211
  function start() {}
366
- function stop() {
367
- _live = false;
368
- process.stdout.write('\x1b[2J\x1b[H' + c.reset + '\n');
369
- }
370
- function setLive(val) { _live = val; }
371
- function setPhase(phase) { _phase = phase; }
372
-
373
- function log(accountIdx, msg) {
374
- if (accountIdx >= 0 && _live) updateAccountRow(accountIdx);
375
- }
376
-
377
- function logGlobal(msg) {
378
- if (!_live) return;
379
- addEvent('info', msg);
380
- }
381
-
382
- // ── Refresh timer ─────────────────────────────────────────────────
383
- let _refreshTimer = null;
384
- function startRefresh() {
385
- if (_refreshTimer) return;
386
- _refreshTimer = setInterval(() => {
387
- if (!_live) return;
388
- // Update all visible account rows
389
- for (const idx of Object.keys(_accountRows)) {
390
- updateAccountRow(parseInt(idx));
391
- }
392
- // Refresh uptime bar
393
- const online = _workers.filter(w => w.channel && !w.paused && !w.dashboardPaused).length;
394
- const up = fmtUptime();
395
- let totalBal = 0;
396
- for (const w of _workers) totalBal += w.stats.balance || 0;
397
- const totalBalStr = totalBal > 0 ? c.green + '⏣' + totalBal.toLocaleString() + c.reset : DIM + '—' + c.reset;
398
- // Update title bar on row 2 (row 1 = top border)
399
- const titleLeft = `${c.bold}DANKGRINDER${c.reset} ${c.dim}v${_version}${c.reset}`;
400
- const titleRight = `${c.green}●${c.reset} ${online} online ${totalBalStr} uptime ${up}`;
401
- const totalW = stripAnsi(titleLeft) + stripAnsi(titleRight);
402
- const padding = Math.max(0, _inner - totalW);
403
- const leftPad = Math.floor(padding / 2);
404
- const rightPad = padding - leftPad;
405
- row(2); clrLine();
406
- process.stdout.write(`${B}│\x1b[0m${' '.repeat(leftPad)}${titleLeft}${' '.repeat(rightPad)}${titleRight}${B}│`);
407
- ln();
408
- }, 1000);
409
- }
410
-
411
- function stopRefresh() {
412
- if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
413
- }
414
-
415
- module.exports = { init, drawBanner, start, draw, log, logGlobal, stop, setLive, setPhase, updateAccountRow, startRefresh, stopRefresh, addEvent };
212
+ function draw() { render(); }
213
+ function setLive(val) { _live = val; if (val) render(); }
214
+ function setPhase(phase) {}
215
+ function startRefresh() { if (!_refreshTimer) _refreshTimer = setInterval(() => { if (_live) render(); }, 80); }
216
+ function stopRefresh() { if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; } }
217
+ function stop() { _live = false; stopRefresh(); logUpdate.clear(); }
218
+
219
+ module.exports = { init, drawBanner, start, draw, log, logGlobal, stop, setLive, setPhase, updateAccountRow, startRefresh, stopRefresh, addEvent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.74.0",
3
+ "version": "8.75.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"