dankgrinder 8.73.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 +179 -330
  2. package/package.json +1 -1
package/lib/ui.js CHANGED
@@ -1,370 +1,219 @@
1
- /**
2
- * CLI Live Dashboard — cursor-positioned in-place row updates.
3
- * Box drawn once. Rows updated in-place. Events below box.
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';
11
-
12
- // Terminal dimensions
13
- let _W = 100;
14
- let _inner = 98;
15
- let _maxAccounts = 4;
8
+ let _startTime = Date.now();
9
+ let _events = [];
10
+ const MAX_EVENTS = 5;
11
+ let _refreshTimer = null;
16
12
 
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;
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
+ }
21
30
 
22
- // ── Spinner frames ────────────────────────────────────────────
23
- const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
24
- function spinnerFrame() { return SPINNER[Math.floor(Date.now() / 150) % SPINNER.length]; }
31
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
32
+ function getSpinner() { return SPINNER_FRAMES[Math.floor(Date.now() / 80) % SPINNER_FRAMES.length]; }
25
33
 
26
- // ── ASCII banner ──────────────────────────────────────────────
27
- const BANNER_LINES = [
28
- ' ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗ DANKGRINDER',
29
- ' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║ vPLACEHOLDER',
30
- ' ██║ ██║██║ ██║██╔██╗ ██║██║ ███╗█████╗ ██║ ██║██╔██╗ ██║',
31
- ' ██║ ██║██║ ██║██║╚██╗██║██║ ██║██╔══╝ ██║ ██║██║╚██╗██║',
32
- ' ██████╔╝╚██████╔╝██║ ╚████║╚██████╔╝███████╗╚██████╔╝██║ ╚████║',
33
- ' ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝',
34
- ];
34
+ const LOAD_FRAMES = ['[= ]', '[== ]', '[=== ]', '[ ===]', '[ ==]', '[ =]', '[ ==]', '[ ===]', '[=== ]', '[== ]'];
35
+ function getLoader() { return LOAD_FRAMES[Math.floor(Date.now() / 150) % LOAD_FRAMES.length]; }
35
36
 
36
- // ── Palette ───────────────────────────────────────────────────
37
- const PALETTE = [
38
- '\x1b[38;2;77;212;238m', '\x1b[38;2;255;194;77m', '\x1b[38;2;130;210;100m',
39
- '\x1b[38;2;255;120;200m', '\x1b[38;2;120;180;255m', '\x1b[38;2;255;180;80m',
40
- '\x1b[38;2;180;130;255m', '\x1b[38;2;100;255;180m', '\x1b[38;2;255;150;100m',
41
- '\x1b[38;2;150;255;200m', '\x1b[38;2;255;255;120m', '\x1b[38;2;200;150;255m',
42
- ];
43
- function wc(idx) { return PALETTE[idx % PALETTE.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
+ }
44
43
 
45
- // ── ANSI helpers ──────────────────────────────────────────────
46
- const c = {
47
- reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
48
- green: '\x1b[38;2;80;255;120m',
49
- red: '\x1b[38;2;255;80;100m',
50
- yellow: '\x1b[38;2;255;220;80m',
51
- cyan: '\x1b[38;2;80;220;255m',
52
- white: '\x1b[37m',
44
+ const STATUS_DOT = {
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') }
53
50
  };
54
- const DIM = c.dim;
55
- const B = '\x1b[38;2;77;212;238m'; // box border color
56
51
 
57
- function row(n) { process.stdout.write(`\x1b[${n};1H`); }
58
- function clrLine() { process.stdout.write('\x1b[2K'); }
59
- function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
60
- function padR(s, n) { return trunc(s, n).padEnd(n); }
61
- function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
62
- function stripAnsi(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, ''); }
63
- function ln() { process.stdout.write('\n'); }
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
+ }
64
62
 
65
63
  function fmtUptime() {
66
64
  const s = Math.floor((Date.now() - _startTime) / 1000);
67
65
  if (s < 60) return `${s}s`;
68
66
  const m = Math.floor(s / 60);
69
67
  const h = Math.floor(m / 60);
70
- if (h > 0) return `${h}h${m % 60}m`;
68
+ if (h > 0) return `${h}h ${m % 60}m`;
71
69
  return `${m}m`;
72
70
  }
73
71
 
74
- // ── Status helpers ─────────────────────────────────────────────
75
- function statusColor(w) {
76
- if (!w.channel) return c.red;
77
- if (w.paused || w.dashboardPaused) return c.yellow;
78
- if (w.busy || w._invRunning || w._sellRunning) return c.yellow;
79
- if (Date.now() < w.globalCooldownUntil) return c.cyan;
80
- return c.green;
72
+ function getDot(w) {
73
+ if (!w.channel) return STATUS_DOT.connect;
74
+ if (w.paused || w.dashboardPaused) return STATUS_DOT.paused;
75
+ if (w._alert?.type === 'death') return STATUS_DOT.dead;
76
+ if (w.busy || w._invRunning || w._sellRunning) return STATUS_DOT.busy;
77
+ if (w.globalCooldownUntil && Date.now() < w.globalCooldownUntil) return STATUS_DOT.busy;
78
+ return STATUS_DOT.online;
81
79
  }
82
80
 
83
- function statusText(w) {
84
- if (!w.channel) return spinnerFrame() + ' CONN';
85
- if (w.paused) return 'PAUSED';
86
- if (w.dashboardPaused) return 'HELD';
87
- if (w._alert?.type === 'death') return 'DEAD';
88
- if (w._alert?.type === 'lowls') return 'LOW LS';
89
- if (w.busy || w._invRunning || w._sellRunning) return spinnerFrame() + ' WORK';
90
- if (Date.now() < w.globalCooldownUntil) {
91
- const wait = Math.ceil((w.globalCooldownUntil - Date.now()) / 1000);
92
- return '⏳' + padL(wait, 3) + '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
93
103
  }
94
- return '● READY';
95
- }
96
-
97
- // ── Format helpers ─────────────────────────────────────────────
98
- function fmtCoins(n) {
99
- if (!n && n !== 0) return DIM + '—' + c.reset;
100
- if (n >= 1_000_000) return c.green + '+' + (n / 1_000_000).toFixed(1) + 'M' + c.reset;
101
- if (n >= 1_000) return c.green + '+' + (n / 1_000).toFixed(1) + 'k' + c.reset;
102
- if (n > 0) return c.green + '+' + n + c.reset;
103
- return DIM + '—' + c.reset;
104
- }
105
-
106
- function fmtLifesavers(w) {
107
- const ls = w._lifesavers;
108
- if (ls === undefined || ls === null) return DIM + '—' + c.reset;
109
- if (ls === 0) return c.red + '0' + c.reset;
110
- if (ls <= 2) return c.yellow + ls + c.reset;
111
- return c.green + ls + c.reset;
112
- }
113
-
114
- function fmtLevel(w) {
115
- const lv = w._level;
116
- if (!lv) return DIM + '—' + c.reset;
117
- return c.cyan + lv + c.reset;
118
- }
119
-
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: 18, bal: 10, ls: 3, lv: 3, earned: 10 };
131
- // Account row visual parts: '│ ' + '# ' + status + ' ' + name + ' ' + doing + ' ' + bal + ' ' + ls + ' ' + lv + ' ' + earned + ' │'
132
- const rowPadding = 1 + 2 + 1 + col.st + 1 + col.name + 1 + col.cmd + 1 + col.bal + 1 + col.ls + 1 + col.lv + 1 + col.earned + 2;
133
- const nameExtra = Math.max(0, _inner - rowPadding);
134
- col.name += nameExtra;
135
- return col;
136
104
  }
137
105
 
138
- // ── Build account row string ─────────────────────────────────
139
- function accountRow(w, wi, col) {
140
- const col2 = wc(wi);
141
- const stCol = statusColor(w);
142
- const stTxt = statusText(w);
143
- const earned = fmtCoins(w.stats.coins);
144
- const bal = w.stats.balance !== undefined ? fmtCoins(w.stats.balance) : DIM + '?' + c.reset;
145
- const ls = fmtLifesavers(w);
146
- const lv = fmtLevel(w);
147
- const name = padR(trunc(w.username || '?', col.name), col.name);
148
- const doing = padR(w.lastStatus || 'idle', col.cmd);
149
- const earnedStr = stripAnsi(earned);
150
- const balStr = stripAnsi(bal);
151
-
152
- // Pad earned and bal to column widths
153
- const earnedPadded = earned.padEnd(col.earned + (earned.length - earnedStr.length));
154
- const balPadded = bal.padEnd(col.bal + (bal.length - balStr.length));
155
-
156
- return (
157
- `${B}│\x1b[0m ` +
158
- `${DIM}${padL(wi + 1, 2)}${c.reset} ` +
159
- `${stCol}${padR(stTxt, col.st)}${c.reset} ` +
160
- `${col2}${name}${c.reset} ` +
161
- `${DIM}${doing}${c.reset} ` +
162
- `${balPadded} ` +
163
- `${padL(ls, col.ls)} ` +
164
- `${padL(lv, col.lv)} ` +
165
- `${earnedPadded} ` +
166
- `${B}│\x1b[0m`
167
- );
168
- }
169
-
170
- // ── Draw the FULL box (called once) ─────────────────────────
171
- function draw() {
172
- layout();
173
- const col = getCol();
174
- const bannerH = BANNER_LINES.length;
175
-
176
- // Clear entire screen + home
177
- process.stdout.write('\x1b[2J\x1b[H');
178
-
179
- let r = 1;
180
-
181
- // ── Top border ──
182
- row(r++); clrLine();
183
- process.stdout.write(`${B}┌${'─'.repeat(_inner)}┐`);
184
- ln();
185
-
186
- // ── Banner ──
187
- for (let i = 0; i < bannerH; i++) {
188
- row(r++); clrLine();
189
- const line = i === 1 ? BANNER_LINES[1].replace('PLACEHOLDER', _version) : BANNER_LINES[i];
190
- if (i < 2) {
191
- const gradLine = gradientLine(line, 77, 212, 238, 255, 92, 147);
192
- const ansiLen = stripAnsi(gradLine).length;
193
- const totalContent = 2 + ansiLen; // left spaces + content
194
- process.stdout.write(`${B}│\x1b[0m ${gradLine}${' '.repeat(Math.max(0, _inner - totalContent - 1))}${B}│`);
195
- } else {
196
- const ansiLen = stripAnsi(line).length;
197
- const totalContent = 2 + ansiLen;
198
- process.stdout.write(`${B}│\x1b[0m ${DIM}${line}${c.reset}${' '.repeat(Math.max(0, _inner - totalContent - 1))}${B}│`);
199
- }
200
- ln();
201
- }
202
-
203
- // ── Status bar ──
204
- const running = _workers.filter(w => w.channel && !w.paused && !w.dashboardPaused).length;
205
- const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
206
- const errors = _workers.filter(w => !w.channel).length;
207
- const phaseLabel = _phase === 'grinding' ? '' : ` ${DIM}[${_phase}]${c.reset}`;
208
- const statusParts = [
209
- `${c.green}●${c.reset} ${running} online`,
210
- paused > 0 ? `${c.yellow}~${c.reset} ${paused} paused` : null,
211
- errors > 0 ? `${c.red}E${c.reset} ${errors} error` : null,
212
- `${DIM}Ctrl+C${c.reset}`,
213
- ].filter(Boolean);
214
- const statusStr = statusParts.join(' ') + phaseLabel;
215
-
216
- row(r++); clrLine();
217
- process.stdout.write(`${B}│\x1b[0m ${statusStr}${' '.repeat(_inner - 1 - stripAnsi(statusStr))}${B}│`);
218
- ln();
219
-
220
- // ── Divider ──
221
- row(r++); clrLine();
222
- process.stdout.write(`${B}├${'─'.repeat(_inner)}┤`);
223
- ln();
224
-
225
- // ── Header ──
226
- row(r++); clrLine();
227
- 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}│`);
228
- ln();
229
-
230
- // ── HR ──
231
- row(r++); clrLine();
232
- process.stdout.write(`${B}│\x1b[0m ${'─'.repeat(_inner)} ${B}│`);
233
- ln();
234
-
235
- // ── 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);
236
124
  const sorted = [..._workers].sort((a, b) => {
237
- if (!a.channel !== !b.channel) return !a.channel ? 1 : -1;
125
+ if (!a.channel !== !b.channel) return a.channel ? -1 : 1;
238
126
  const aA = a.channel && !a.paused && !a.dashboardPaused;
239
- const bA = b.channel && !b.paused && !b.dashboardPaused;
240
- if (aA !== bA) return bA ? 1 : -1;
127
+ const bB = b.channel && !b.paused && !b.dashboardPaused;
128
+ if (aA !== bB) return aA ? -1 : 1;
241
129
  return (b.stats.commands || 0) - (a.stats.commands || 0);
242
130
  });
243
- const shown = sorted.slice(0, _maxAccounts);
244
-
245
- _accountRows = [];
246
- for (let si = 0; si < shown.length; si++) {
247
- const w = shown[si];
248
- const wi = _workers.indexOf(w);
249
- _accountRows[wi] = r;
250
- row(r++); clrLine();
251
- process.stdout.write(accountRow(w, wi, col));
252
- ln();
253
- }
254
-
255
- // ── Totals divider ──
256
- _totalsRow = r;
257
- row(r++); clrLine();
258
- process.stdout.write(`${B}│\x1b[0m ${'─'.repeat(_inner)} ${B}│`);
259
- ln();
260
-
261
- // ── Totals ──
262
- let totalCoins = 0, totalBal = 0;
263
- for (const w of _workers) {
131
+
132
+ let totalCoins = 0, totalBal = 0, onlineCount = 0;
133
+ for (let i = 0; i < _workers.length; i++) {
134
+ const w = _workers[i];
264
135
  totalCoins += w.stats.coins || 0;
265
136
  totalBal += w.stats.balance || 0;
137
+ if (w.channel && !w.paused && !w.dashboardPaused) onlineCount++;
266
138
  }
267
- const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
268
- const balStr = totalBal > 0 ? '⏣' + totalBal.toLocaleString() : '—';
269
- const balPadded = balStr.padEnd(col.bal);
270
- const earnedStr = stripAnsi(fmtCoins(totalCoins));
271
-
272
- row(r++); clrLine();
273
- process.stdout.write(
274
- `${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}│`
275
- );
276
- ln();
277
-
278
- // ── Bottom ──
279
- _bottomRow = r;
280
- row(r++); clrLine();
281
- process.stdout.write(`${B}└${'─'.repeat(_inner)}┘`);
282
- ln();
283
- }
284
-
285
- // ── Update ONE account row in-place ─────────────────────────
286
- function updateAccountRow(accountIdx) {
287
- if (!_live) return;
288
- const rowNum = _accountRows[accountIdx];
289
- if (!rowNum) return; // not visible
290
- const w = _workers[accountIdx];
291
- if (!w) return;
292
- const col = getCol();
293
139
 
294
- row(rowNum); clrLine();
295
- process.stdout.write(accountRow(w, accountIdx, col));
296
- ln();
297
- }
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());
155
+ } else {
156
+ activity = chalk.gray(getLoader());
157
+ }
298
158
 
299
- // ── Gradient line ─────────────────────────────────────────────
300
- function gradientLine(text, r1, g1, b1, r2, g2, b2) {
301
- let out = '';
302
- for (let i = 0; i < text.length; i++) {
303
- const t = text.length <= 1 ? 0 : i / (text.length - 1);
304
- 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`;
305
192
  }
306
- return out + '\x1b[0m';
307
- }
308
-
309
- // ── Event tracking ────────────────────────────────────────────
310
- let _eventLines = [];
311
- const MAX_EVENTS = 15;
312
-
313
- // ── Public API ────────────────────────────────────────────────
314
-
315
- function init({ workers, isShuttingDown }) {
316
- _startTime = Date.now();
317
- _workers = workers;
318
- _version = '0.0.0';
319
- _eventLines = [];
320
- _live = false;
321
- _phase = 'init';
322
- _accountRows = [];
323
- }
324
-
325
- function drawBanner(version) { _version = version || '0.0.0'; }
326
- function start() {}
327
-
328
- function stop() {
329
- _live = false;
330
- _phase = 'init';
331
- process.stdout.write('\x1b[2J\x1b[H' + c.reset + '\n');
332
- }
333
-
334
- function setLive(val) { _live = val; }
335
- function setPhase(phase) { _phase = phase; }
336
-
337
- function log(accountIdx, msg) {
338
- const now = new Date();
339
- const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
340
-
341
- if (accountIdx >= 0) {
342
- if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
343
- _eventLines[accountIdx].push({ text: msg, ts });
344
- if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
345
- if (_live) updateAccountRow(accountIdx);
193
+
194
+ if (_workers.length > maxShown) {
195
+ out += chalk.bold.dim(` ... and ${(_workers.length - maxShown).toLocaleString()} more accounts running ${getSpinner()}\n`);
346
196
  }
347
197
 
348
- if (!_live) return;
349
-
350
- const col2 = accountIdx >= 0 ? wc(accountIdx) : c.cyan;
351
- const name = accountIdx >= 0 ? trunc(_workers[accountIdx]?.username || '?', 14) : 'GLOBAL';
352
- process.stdout.write(`${col2}${name}${c.reset} ${DIM}[${ts}]${c.reset} ${msg}\n`);
353
- }
354
-
355
- function logGlobal(msg) { log(-1, msg); }
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';
356
207
 
357
- // ── Refresh ──────────────────────────────────────────────────
358
- let _refreshTimer = null;
359
- function startRefresh() {
360
- if (_refreshTimer) return;
361
- _refreshTimer = setInterval(() => {
362
- if (!_live) return;
363
- for (let i = 0; i < _workers.length; i++) updateAccountRow(i);
364
- }, 1500);
365
- }
366
- function stopRefresh() {
367
- if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
208
+ logUpdate(out);
368
209
  }
369
210
 
370
- module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase, updateAccountRow, startRefresh, stopRefresh };
211
+ function start() {}
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.73.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"