dankgrinder 6.3.0 → 6.6.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.
- package/lib/grinder.js +303 -185
- package/package.json +1 -1
package/lib/grinder.js
CHANGED
|
@@ -219,23 +219,69 @@ function gradientLine(text, from, to) {
|
|
|
219
219
|
return out + c.reset;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
function gradientText(text, from, to) {
|
|
223
|
+
return gradientLine(text, from, to);
|
|
224
|
+
}
|
|
225
|
+
|
|
222
226
|
// ── Sparkline graph for earnings trend ───────────────────────
|
|
223
227
|
const SPARK_CHARS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
|
224
228
|
function drawSparkline(data, width = 12) {
|
|
225
|
-
if (!data || data.length === 0) return c.dim + '
|
|
229
|
+
if (!data || data.length === 0) return c.dim + '·····' + c.reset;
|
|
226
230
|
const recent = data.slice(-width);
|
|
227
231
|
const min = Math.min(...recent);
|
|
228
232
|
const max = Math.max(...recent);
|
|
229
233
|
const range = max - min || 1;
|
|
230
234
|
return recent.map(v => {
|
|
231
235
|
const idx = Math.min(7, Math.floor(((v - min) / range) * 8));
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
+
const ch = SPARK_CHARS[idx] || '▁';
|
|
237
|
+
const t = (v - min) / range;
|
|
238
|
+
// Gradient from red->yellow->green based on relative value
|
|
239
|
+
const r = t < 0.5 ? 239 : lerp(251, 52, (t - 0.5) * 2);
|
|
240
|
+
const g = t < 0.5 ? lerp(68, 191, t * 2) : lerp(191, 211, (t - 0.5) * 2);
|
|
241
|
+
const b = t < 0.5 ? lerp(68, 36, t * 2) : lerp(36, 153, (t - 0.5) * 2);
|
|
242
|
+
return rgb(r, g, b) + ch + c.reset;
|
|
236
243
|
}).join('');
|
|
237
244
|
}
|
|
238
245
|
|
|
246
|
+
// ── Advanced progress bar ────────────────────────────────────
|
|
247
|
+
function progressBar(value, max, width, filledColor, emptyColor) {
|
|
248
|
+
const pct = max > 0 ? Math.min(1, value / max) : 0;
|
|
249
|
+
const filled = Math.round(pct * width);
|
|
250
|
+
const empty = width - filled;
|
|
251
|
+
const fc = filledColor || [52, 211, 153];
|
|
252
|
+
const ec = emptyColor || [50, 50, 70];
|
|
253
|
+
return rgb(fc[0], fc[1], fc[2]) + '█'.repeat(filled) + rgb(ec[0], ec[1], ec[2]) + '░'.repeat(empty) + c.reset;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ── Animated braille spinner frames ──────────────────────────
|
|
257
|
+
const BRAILLE_SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
258
|
+
const BLOCK_SPIN = ['▉', '▊', '▋', '▌', '▍', '▎', '▏', '▎', '▍', '▌', '▋', '▊'];
|
|
259
|
+
const PULSE_CHARS = ['○', '◎', '●', '◉', '●', '◎'];
|
|
260
|
+
function getSpinner(type = 'braille') {
|
|
261
|
+
const now = Math.floor(Date.now() / 80);
|
|
262
|
+
if (type === 'block') return BLOCK_SPIN[now % BLOCK_SPIN.length];
|
|
263
|
+
if (type === 'pulse') return PULSE_CHARS[now % PULSE_CHARS.length];
|
|
264
|
+
return BRAILLE_SPIN[now % BRAILLE_SPIN.length];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Box drawing helpers ──────────────────────────────────────
|
|
268
|
+
const BOX = {
|
|
269
|
+
tl: '╭', tr: '╮', bl: '╰', br: '╯',
|
|
270
|
+
h: '─', v: '│', hBold: '━', vBold: '┃',
|
|
271
|
+
dtl: '╔', dtr: '╗', dbl: '╚', dbr: '╝', dh: '═', dv: '║',
|
|
272
|
+
cross: '┼', tee: '├', teeR: '┤', teeD: '┬', teeU: '┴',
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
function boxTop(w, color) { return color + BOX.dtl + BOX.dh.repeat(w - 2) + BOX.dtr + c.reset; }
|
|
276
|
+
function boxMid(w, color) { return color + BOX.tee + BOX.h.repeat(w - 2) + BOX.teeR + c.reset; }
|
|
277
|
+
function boxBot(w, color) { return color + BOX.dbl + BOX.dh.repeat(w - 2) + BOX.dbr + c.reset; }
|
|
278
|
+
function boxLine(content, w, color) {
|
|
279
|
+
const stripped = content.replace(/\x1b\[[0-9;]*m/g, '');
|
|
280
|
+
const pad = Math.max(0, w - 4 - stripped.length);
|
|
281
|
+
return color + BOX.dv + c.reset + ' ' + content + ' '.repeat(pad) + ' ' + color + BOX.dv + c.reset;
|
|
282
|
+
}
|
|
283
|
+
function thinLine(w) { return ' ' + c.dim + BOX.h.repeat(w - 4) + c.reset; }
|
|
284
|
+
|
|
239
285
|
const BANNER_RAW = [
|
|
240
286
|
' ██████╗ █████╗ ███╗ ██╗██╗ ██╗',
|
|
241
287
|
' ██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝',
|
|
@@ -284,24 +330,30 @@ let shutdownCalled = false;
|
|
|
284
330
|
let sessionPeakCoins = 0;
|
|
285
331
|
let isNewHigh = false;
|
|
286
332
|
// RingBuffer: O(1) push, bounded memory, no array shifting or GC pressure
|
|
287
|
-
const recentLogs = new RingBuffer(
|
|
288
|
-
const MAX_LOGS =
|
|
289
|
-
const RENDER_THROTTLE_MS =
|
|
333
|
+
const recentLogs = new RingBuffer(8);
|
|
334
|
+
const MAX_LOGS = 8;
|
|
335
|
+
const RENDER_THROTTLE_MS = 200;
|
|
290
336
|
// Earnings history for sparkline (sample every 10 seconds)
|
|
291
|
-
const earningsHistory = new RingBuffer(
|
|
337
|
+
const earningsHistory = new RingBuffer(30);
|
|
292
338
|
let lastEarningsSample = 0;
|
|
339
|
+
// Per-command stats tracking
|
|
340
|
+
const cmdStats = new Map();
|
|
341
|
+
// Coins per minute history for rate graph
|
|
342
|
+
const cpmHistory = new RingBuffer(20);
|
|
343
|
+
let lastCpmSample = 0;
|
|
293
344
|
|
|
294
345
|
function formatUptime() {
|
|
295
346
|
const s = Math.floor((Date.now() - startTime) / 1000);
|
|
296
347
|
const h = Math.floor(s / 3600);
|
|
297
348
|
const m = Math.floor((s % 3600) / 60);
|
|
298
349
|
const sec = s % 60;
|
|
299
|
-
if (h > 0) return `${h}h ${m}m`;
|
|
350
|
+
if (h > 0) return `${h}h ${m}m ${sec}s`;
|
|
300
351
|
if (m > 0) return `${m}m ${sec}s`;
|
|
301
352
|
return `${sec}s`;
|
|
302
353
|
}
|
|
303
354
|
|
|
304
355
|
function formatCoins(n) {
|
|
356
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
|
305
357
|
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
306
358
|
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
307
359
|
return n.toLocaleString();
|
|
@@ -326,14 +378,15 @@ function renderDashboard() {
|
|
|
326
378
|
|
|
327
379
|
totalBalance = 0; totalCoins = 0; totalCommands = 0;
|
|
328
380
|
let totalErrors = 0;
|
|
381
|
+
let totalLosses = 0;
|
|
329
382
|
for (const w of workers) {
|
|
330
383
|
totalBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
|
|
331
384
|
totalCoins += w.stats.coins || 0;
|
|
332
385
|
totalCommands += w.stats.commands || 0;
|
|
333
386
|
totalErrors += w.stats.errors || 0;
|
|
387
|
+
totalLosses += w.stats.losses || 0;
|
|
334
388
|
}
|
|
335
389
|
const successRate = totalCommands > 0 ? Math.round(((totalCommands - totalErrors) / totalCommands) * 100) : 100;
|
|
336
|
-
// Track session peak and new high
|
|
337
390
|
if (totalCoins > sessionPeakCoins) {
|
|
338
391
|
sessionPeakCoins = totalCoins;
|
|
339
392
|
isNewHigh = true;
|
|
@@ -341,212 +394,225 @@ function renderDashboard() {
|
|
|
341
394
|
}
|
|
342
395
|
|
|
343
396
|
const lines = [];
|
|
344
|
-
const tw = Math.min(process.stdout.columns || 80,
|
|
345
|
-
const
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const
|
|
397
|
+
const tw = Math.max(Math.min(process.stdout.columns || 80, 90), 60);
|
|
398
|
+
const accent = rgb(139, 92, 246);
|
|
399
|
+
const accentDim = rgb(90, 60, 170);
|
|
400
|
+
const green = rgb(52, 211, 153);
|
|
401
|
+
const blue = rgb(96, 165, 250);
|
|
402
|
+
const pink = rgb(236, 72, 153);
|
|
403
|
+
const gold = rgb(255, 215, 0);
|
|
404
|
+
const orange = rgb(251, 146, 60);
|
|
405
|
+
const cyan = rgb(34, 211, 238);
|
|
406
|
+
const red = rgb(239, 68, 68);
|
|
407
|
+
const yellow = rgb(251, 191, 36);
|
|
408
|
+
const white = c.white;
|
|
409
|
+
const dim = c.dim;
|
|
410
|
+
const RE_ANSI = /\x1b\[[0-9;]*m/g;
|
|
411
|
+
|
|
412
|
+
// ━━━ HEADER BOX ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
413
|
+
lines.push(boxTop(tw, accent));
|
|
414
|
+
|
|
415
|
+
// Title line with animated spinner
|
|
416
|
+
const spin = getSpinner('braille');
|
|
417
|
+
const titleGrad = gradientText('DankGrinder', [192, 132, 252], [52, 211, 153]);
|
|
418
|
+
const verStr = `${dim}v${PKG_VERSION}${c.reset}`;
|
|
419
|
+
lines.push(boxLine(`${c.bold}${titleGrad}${c.reset} ${verStr} ${green}${spin}${c.reset}`, tw, accent));
|
|
420
|
+
|
|
421
|
+
// Info bar
|
|
354
422
|
const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const liveIcon = rgb(52, 211, 153) + '●' + c.reset;
|
|
380
|
-
const balStr = `${rgb(192, 132, 252)}${c.bold}⏣ ${formatCoins(totalBalance)}${c.reset}`;
|
|
381
|
-
const earnStr = `${rgb(52, 211, 153)}▲ ${formatCoins(totalCoins)}${c.reset}`;
|
|
423
|
+
const pausedCount = workers.filter(w => w.paused || w.dashboardPaused).length;
|
|
424
|
+
const recovCount = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
|
|
425
|
+
const mode = CLUSTER_ENABLED ? `${cyan}CLUSTER${c.reset}` : `${dim}STANDALONE${c.reset}`;
|
|
426
|
+
const cmdCount = AccountWorker.COMMAND_MAP.length;
|
|
427
|
+
|
|
428
|
+
// Net quality
|
|
429
|
+
const netQ = workers.length > 0
|
|
430
|
+
? workers.reduce((s, w) => s + (w._lastPing < 200 ? 1 : w._lastPing < 500 ? 0.5 : 0), 0) / workers.length : 1;
|
|
431
|
+
const netDot = netQ > 0.8 ? `${green}◉${c.reset}` : netQ > 0.5 ? `${yellow}◉${c.reset}` : `${red}◉${c.reset}`;
|
|
432
|
+
const netLbl = netQ > 0.8 ? `${green}GOOD${c.reset}` : netQ > 0.5 ? `${yellow}FAIR${c.reset}` : `${red}POOR${c.reset}`;
|
|
433
|
+
|
|
434
|
+
lines.push(boxLine(`${mode} ${dim}|${c.reset} ${netDot} ${netLbl} ${dim}|${c.reset} ${blue}${cmdCount}${c.reset} ${dim}cmds${c.reset} ${dim}|${c.reset} ${green}${activeCount}${c.reset}${dim}/${c.reset}${white}${workers.length}${c.reset} ${dim}live${c.reset}${pausedCount > 0 ? ` ${yellow}${pausedCount} paused${c.reset}` : ''}${recovCount > 0 ? ` ${orange}${recovCount} recovering${c.reset}` : ''}`, tw, accent));
|
|
435
|
+
|
|
436
|
+
// ━━━ STATS SECTION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
437
|
+
const midLine = accent + BOX.tee + BOX.h.repeat(tw - 2) + BOX.teeR + c.reset;
|
|
438
|
+
lines.push(midLine);
|
|
439
|
+
|
|
440
|
+
// Balance card
|
|
441
|
+
const balIcon = `${gold}⟐${c.reset}`;
|
|
442
|
+
const balVal = `${c.bold}${gold}⏣ ${formatCoins(totalBalance)}${c.reset}`;
|
|
443
|
+
const peakTag = isNewHigh ? ` ${red}${c.bold}<< NEW HIGH >>${c.reset}` : '';
|
|
444
|
+
lines.push(boxLine(`${balIcon} ${dim}BALANCE${c.reset} ${balVal}${peakTag}`, tw, accent));
|
|
445
|
+
|
|
446
|
+
// Earnings card with sparkline
|
|
382
447
|
const elapsedHrs = (Date.now() - startTime) / 3_600_000;
|
|
383
|
-
const
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
448
|
+
const perHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
|
|
449
|
+
const earnIcon = `${green}▲${c.reset}`;
|
|
450
|
+
const earnVal = `${c.bold}${green}+⏣ ${formatCoins(totalCoins)}${c.reset}`;
|
|
451
|
+
const hrRate = `${dim}(${c.reset}${green}${formatCoins(perHr)}/h${c.reset}${dim})${c.reset}`;
|
|
452
|
+
// Sample sparkline
|
|
453
|
+
const now = Date.now();
|
|
454
|
+
if (now - lastEarningsSample > 8000) { earningsHistory.push(totalCoins); lastEarningsSample = now; }
|
|
455
|
+
const spark = drawSparkline(earningsHistory.toArray(), 20);
|
|
456
|
+
lines.push(boxLine(`${earnIcon} ${dim}EARNED${c.reset} ${earnVal} ${hrRate} ${spark}`, tw, accent));
|
|
457
|
+
|
|
458
|
+
// Peak earnings
|
|
459
|
+
const peakIcon = `${orange}★${c.reset}`;
|
|
460
|
+
lines.push(boxLine(`${peakIcon} ${dim}PEAK${c.reset} ${c.bold}${orange}⏣ ${formatCoins(sessionPeakCoins)}${c.reset} ${dim}this session${c.reset}`, tw, accent));
|
|
461
|
+
|
|
462
|
+
// Performance metrics line
|
|
392
463
|
const cpmVal = globalCmdRate.getRate().toFixed(1);
|
|
393
|
-
const
|
|
464
|
+
const srColor = successRate >= 95 ? green : successRate >= 80 ? yellow : red;
|
|
465
|
+
const srBar = progressBar(successRate, 100, 8, successRate >= 95 ? [52, 211, 153] : successRate >= 80 ? [251, 191, 36] : [239, 68, 68]);
|
|
466
|
+
const perfIcon = `${blue}◆${c.reset}`;
|
|
467
|
+
lines.push(boxLine(`${perfIcon} ${dim}PERF${c.reset} ${blue}${totalCommands}${c.reset} ${dim}cmds${c.reset} ${srColor}${successRate}%${c.reset} ${srBar} ${cyan}${cpmVal}${c.reset}${dim}/min${c.reset} ${yellow}◷${c.reset} ${yellow}${formatUptime()}${c.reset}`, tw, accent));
|
|
394
468
|
|
|
395
|
-
//
|
|
396
|
-
const upStr = `${rgb(251, 191, 36)}◷ ${formatUptime()}${c.reset}`;
|
|
469
|
+
// Memory line
|
|
397
470
|
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
const memStr = `${memColor}${memMB}MB${c.reset} ${memBar}`;
|
|
471
|
+
const memColor = memMB > 900 ? [239, 68, 68] : memMB > 600 ? [251, 191, 36] : [52, 211, 153];
|
|
472
|
+
const memBar = progressBar(memMB, 1024, 12, memColor, [40, 40, 55]);
|
|
473
|
+
const memIcon = `${dim}≡${c.reset}`;
|
|
474
|
+
lines.push(boxLine(`${memIcon} ${dim}MEM${c.reset} ${rgb(memColor[0], memColor[1], memColor[2])}${memMB}MB${c.reset} ${memBar}`, tw, accent));
|
|
403
475
|
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
lines.push(
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
lines.push(thinBar);
|
|
417
|
-
|
|
418
|
-
// Worker rows — paginated for 10K+ accounts
|
|
419
|
-
// Renders up to MAX_VISIBLE_WORKERS rows individually, then shows
|
|
420
|
-
// a compact summary for the rest. This keeps terminal responsive.
|
|
421
|
-
const MAX_VISIBLE_WORKERS = 30;
|
|
422
|
-
const nameWidth = Math.min(18, tw > 70 ? 18 : 12);
|
|
423
|
-
const statusWidth = Math.max(18, tw - nameWidth - 42);
|
|
424
|
-
const RE_ANSI_STRIP = /\x1b\[[0-9;]*m/g;
|
|
425
|
-
|
|
426
|
-
// Find top 3 earners for highlighting
|
|
476
|
+
// ━━━ ACCOUNTS TABLE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
477
|
+
lines.push(midLine);
|
|
478
|
+
|
|
479
|
+
const nameW = Math.min(20, tw > 75 ? 20 : 14);
|
|
480
|
+
const statusW = Math.max(20, tw - nameW - 44);
|
|
481
|
+
|
|
482
|
+
// Column headers with gradient
|
|
483
|
+
const colHead = ` ${dim}##${c.reset} ${dim}STS${c.reset} ${gradientText('Account', [139, 92, 246], [96, 165, 250])}${' '.repeat(Math.max(0, nameW - 7))} ${dim}Balance${c.reset} ${dim}Earned${c.reset} ${dim}Activity${c.reset}`;
|
|
484
|
+
lines.push(boxLine(colHead, tw, accent));
|
|
485
|
+
lines.push(boxLine(`${dim}${'─'.repeat(tw - 6)}${c.reset}`, tw, accent));
|
|
486
|
+
|
|
487
|
+
// Top 3 earners
|
|
427
488
|
const topEarners = [...workers]
|
|
428
489
|
.filter(w => w.running && (w.stats.coins || 0) > 0)
|
|
429
490
|
.sort((a, b) => (b.stats.coins || 0) - (a.stats.coins || 0))
|
|
430
491
|
.slice(0, 3);
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
492
|
+
const topIds = new Set(topEarners.map(w => w.account.id));
|
|
493
|
+
|
|
494
|
+
const MAX_VIS = 30;
|
|
495
|
+
|
|
496
|
+
const renderRow = (wk, idx) => {
|
|
497
|
+
// Use original account number (wk.idx) so removed accounts leave visible gaps
|
|
498
|
+
const origNum = wk.idx + 1;
|
|
499
|
+
const num = `${dim}${origNum.toString().padStart(2)}${c.reset}`;
|
|
500
|
+
const rawStat = (wk.lastStatus || 'idle').replace(RE_ANSI, '');
|
|
501
|
+
const statTxt = rawStat.substring(0, statusW);
|
|
502
|
+
|
|
503
|
+
// Status indicator with animations
|
|
504
|
+
let sts, statLabel;
|
|
505
|
+
const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
|
|
506
|
+
if (wk._tokenInvalid) {
|
|
507
|
+
sts = `${red}✗${c.reset}`;
|
|
508
|
+
statLabel = `${red}TOKEN INVALID${c.reset}`;
|
|
509
|
+
} else if (!wk.running) {
|
|
510
|
+
sts = `${dim}○${c.reset}`;
|
|
511
|
+
statLabel = `${dim}offline${c.reset}`;
|
|
512
|
+
} else if (isRecov) {
|
|
513
|
+
const sL = Math.ceil((wk._errorCooldownUntil - Date.now()) / 1000);
|
|
514
|
+
sts = `${orange}${getSpinner('braille')}${c.reset}`;
|
|
515
|
+
statLabel = `${orange}recovering #${wk._recoveryAttempts} (${sL}s)${c.reset}`;
|
|
451
516
|
} else if (wk.paused) {
|
|
452
|
-
|
|
453
|
-
|
|
517
|
+
sts = `${red}⏸${c.reset}`;
|
|
518
|
+
statLabel = `${red}PAUSED${c.reset}`;
|
|
454
519
|
} else if (wk.dashboardPaused) {
|
|
455
|
-
|
|
456
|
-
|
|
520
|
+
sts = `${yellow}⏸${c.reset}`;
|
|
521
|
+
statLabel = `${yellow}paused (user)${c.reset}`;
|
|
457
522
|
} else if (wk.busy) {
|
|
458
|
-
|
|
459
|
-
|
|
523
|
+
sts = `${green}${getSpinner('pulse')}${c.reset}`;
|
|
524
|
+
statLabel = `${dim}${statTxt}${c.reset}`;
|
|
460
525
|
} else {
|
|
461
|
-
|
|
462
|
-
|
|
526
|
+
sts = `${green}●${c.reset}`;
|
|
527
|
+
statLabel = `${dim}${statTxt}${c.reset}`;
|
|
463
528
|
}
|
|
464
529
|
|
|
465
|
-
//
|
|
466
|
-
let medal = '
|
|
467
|
-
if (
|
|
530
|
+
// Medal for top earners
|
|
531
|
+
let medal = ' ';
|
|
532
|
+
if (topIds.has(wk.account.id)) {
|
|
468
533
|
const rank = topEarners.findIndex(e => e.account.id === wk.account.id);
|
|
469
|
-
|
|
534
|
+
// Use text medals instead of emoji for better terminal compat
|
|
535
|
+
medal = rank === 0 ? `${gold}1st${c.reset}` : rank === 1 ? `${rgb(192, 192, 192)}2nd${c.reset}` : `${rgb(205, 127, 50)}3rd${c.reset}`;
|
|
470
536
|
}
|
|
471
537
|
|
|
472
|
-
const name = `${wk.color}${c.bold}${(wk.username || '?').substring(0,
|
|
538
|
+
const name = `${wk.color}${c.bold}${(wk.username || '?').substring(0, nameW).padEnd(nameW)}${c.reset}`;
|
|
539
|
+
|
|
540
|
+
// Balance with icon
|
|
473
541
|
const bal = wk.stats.balance > 0
|
|
474
|
-
? `${
|
|
475
|
-
: `${
|
|
476
|
-
|
|
477
|
-
//
|
|
478
|
-
const
|
|
479
|
-
const
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
lines.push(` ${pos} ${medal}${dot} ${name.padEnd(nameWidth + wk.color.length + c.bold.length + c.reset.length + 2)} ${bal} ${earned} ${stateLabel}`);
|
|
542
|
+
? `${gold}⏣${c.reset}${white}${formatCoins(wk.stats.balance).padStart(9)}${c.reset}`
|
|
543
|
+
: `${dim}⏣ -${c.reset}`;
|
|
544
|
+
|
|
545
|
+
// Earnings with mini progress
|
|
546
|
+
const earnNum = wk.stats.coins || 0;
|
|
547
|
+
const earnBarW = earnNum > 0 ? Math.min(6, Math.max(1, Math.floor(Math.log10(earnNum + 1)))) : 0;
|
|
548
|
+
const earnBar = progressBar(earnBarW, 6, 6, [52, 211, 153], [40, 40, 55]);
|
|
549
|
+
const earned = earnNum > 0
|
|
550
|
+
? `${green}+${formatCoins(earnNum).padEnd(7)}${c.reset}${earnBar}`
|
|
551
|
+
: `${dim} - ${c.reset}${progressBar(0, 6, 6)}`;
|
|
552
|
+
|
|
553
|
+
lines.push(boxLine(`${num} ${sts}${medal.padEnd(medal.length > 1 ? 3 : 1)} ${name} ${bal} ${earned} ${statLabel}`, tw, accent));
|
|
488
554
|
};
|
|
489
555
|
|
|
490
|
-
if (workers.length <=
|
|
491
|
-
for (let i = 0; i < workers.length; i++)
|
|
556
|
+
if (workers.length <= MAX_VIS) {
|
|
557
|
+
for (let i = 0; i < workers.length; i++) renderRow(workers[i], i);
|
|
492
558
|
} else {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
let
|
|
497
|
-
for (let i = MAX_VISIBLE_WORKERS; i < workers.length; i++) {
|
|
559
|
+
for (let i = 0; i < MAX_VIS; i++) renderRow(workers[i], i);
|
|
560
|
+
const rest = workers.length - MAX_VIS;
|
|
561
|
+
let ha = 0, hp = 0, hr = 0, ho = 0;
|
|
562
|
+
for (let i = MAX_VIS; i < workers.length; i++) {
|
|
498
563
|
const w = workers[i];
|
|
499
|
-
if (!w.running)
|
|
500
|
-
else if (w.paused || w.dashboardPaused)
|
|
501
|
-
else if (w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now())
|
|
502
|
-
else
|
|
503
|
-
}
|
|
504
|
-
const parts = [`${
|
|
505
|
-
if (
|
|
506
|
-
if (
|
|
507
|
-
if (
|
|
508
|
-
if (
|
|
509
|
-
lines.push(`
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
//
|
|
513
|
-
const recoveringCount = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
|
|
514
|
-
const pausedCount = workers.filter(w => w.paused).length;
|
|
564
|
+
if (!w.running) ho++;
|
|
565
|
+
else if (w.paused || w.dashboardPaused) hp++;
|
|
566
|
+
else if (w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()) hr++;
|
|
567
|
+
else ha++;
|
|
568
|
+
}
|
|
569
|
+
const parts = [`${dim}... +${rest} more${c.reset}`];
|
|
570
|
+
if (ha > 0) parts.push(`${green}● ${ha}${c.reset}`);
|
|
571
|
+
if (hp > 0) parts.push(`${yellow}⏸ ${hp}${c.reset}`);
|
|
572
|
+
if (hr > 0) parts.push(`${orange}↻ ${hr}${c.reset}`);
|
|
573
|
+
if (ho > 0) parts.push(`${dim}○ ${ho}${c.reset}`);
|
|
574
|
+
lines.push(boxLine(` ${parts.join(` ${dim}·${c.reset} `)}`, tw, accent));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// ━━━ HEALTH SUMMARY ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
515
578
|
const totalRecoveries = workers.reduce((s, w) => s + (w._totalRecoveries || 0), 0);
|
|
516
579
|
const totalDisconnects = workers.reduce((s, w) => s + (w._disconnectCount || 0), 0);
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
if (
|
|
521
|
-
if (
|
|
522
|
-
if (
|
|
523
|
-
|
|
580
|
+
if (recovCount > 0 || pausedCount > 0 || totalRecoveries > 0 || totalDisconnects > 0) {
|
|
581
|
+
lines.push(boxLine(`${dim}${'─'.repeat(tw - 6)}${c.reset}`, tw, accent));
|
|
582
|
+
const hParts = [];
|
|
583
|
+
if (recovCount > 0) hParts.push(`${orange}${getSpinner('braille')} ${recovCount} recovering${c.reset}`);
|
|
584
|
+
if (pausedCount > 0) hParts.push(`${red}⏸ ${pausedCount} paused${c.reset}`);
|
|
585
|
+
if (totalRecoveries > 0) hParts.push(`${dim}${totalRecoveries} auto-healed${c.reset}`);
|
|
586
|
+
if (totalDisconnects > 0) hParts.push(`${dim}${totalDisconnects} reconnects${c.reset}`);
|
|
587
|
+
lines.push(boxLine(`${dim}HEALTH${c.reset} ${hParts.join(` ${dim}·${c.reset} `)}`, tw, accent));
|
|
524
588
|
}
|
|
525
589
|
|
|
526
|
-
// Cluster info
|
|
590
|
+
// Cluster info
|
|
527
591
|
if (CLUSTER_ENABLED) {
|
|
528
592
|
const nodeShort = NODE_ID.substring(0, 12);
|
|
529
593
|
const claimedCount = workers.filter(w => w.running).length;
|
|
530
|
-
lines.push(
|
|
594
|
+
lines.push(boxLine(`${cyan}CLUSTER${c.reset} ${dim}node:${c.reset}${cyan}${nodeShort}${c.reset} ${dim}claimed:${c.reset}${white}${claimedCount}${c.reset}`, tw, accent));
|
|
531
595
|
}
|
|
532
596
|
|
|
533
|
-
//
|
|
597
|
+
// ━━━ LIVE LOG FEED ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
534
598
|
const logEntries = recentLogs.toArray();
|
|
535
599
|
if (logEntries.length > 0) {
|
|
536
|
-
lines.push(
|
|
600
|
+
lines.push(midLine);
|
|
601
|
+
const logTitle = gradientText(' LIVE FEED ', [139, 92, 246], [52, 211, 153]);
|
|
602
|
+
lines.push(boxLine(`${logTitle}`, tw, accent));
|
|
537
603
|
for (const entry of logEntries) {
|
|
538
|
-
lines.push(
|
|
604
|
+
lines.push(boxLine(`${dim}${entry}${c.reset}`, tw, accent));
|
|
539
605
|
}
|
|
540
606
|
}
|
|
541
607
|
|
|
542
|
-
|
|
608
|
+
// ━━━ FOOTER ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
609
|
+
lines.push(boxBot(tw, accent));
|
|
543
610
|
|
|
544
|
-
//
|
|
611
|
+
// Render
|
|
545
612
|
process.stdout.write('\x1b[H');
|
|
546
613
|
for (const line of lines) {
|
|
547
614
|
process.stdout.write(c.clearLine + '\r' + line + '\n');
|
|
548
615
|
}
|
|
549
|
-
// Erase everything below the dashboard (clears ghost bars, trailing lines)
|
|
550
616
|
process.stdout.write('\x1b[J');
|
|
551
617
|
dashboardLines = lines.length;
|
|
552
618
|
dashboardRendering = false;
|
|
@@ -2670,6 +2736,22 @@ async function start(apiKey, apiUrl) {
|
|
|
2670
2736
|
console.log(` ${checks.join(' ')}`);
|
|
2671
2737
|
console.log('');
|
|
2672
2738
|
|
|
2739
|
+
// ── Animated loading bar helper ──────────────────────────────
|
|
2740
|
+
const barW = Math.min(40, (process.stdout.columns || 80) - 30);
|
|
2741
|
+
let loginDone = 0;
|
|
2742
|
+
const drawLoginProgress = () => {
|
|
2743
|
+
const pct = accounts.length > 0 ? loginDone / accounts.length : 0;
|
|
2744
|
+
const filled = Math.round(pct * barW);
|
|
2745
|
+
const empty = barW - filled;
|
|
2746
|
+
const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
|
|
2747
|
+
const bar = rgb(139, 92, 246) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(empty) + c.reset;
|
|
2748
|
+
const pctStr = `${Math.round(pct * 100)}%`;
|
|
2749
|
+
process.stdout.write(`\r ${rgb(139, 92, 246)}${spin}${c.reset} ${c.dim}Logging in...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${loginDone}${c.reset}${c.dim}/${c.reset}${c.white}${accounts.length}${c.reset} ${c.dim}${pctStr}${c.reset} `);
|
|
2750
|
+
};
|
|
2751
|
+
|
|
2752
|
+
// Progress animation timer
|
|
2753
|
+
const progressInterval = setInterval(drawLoginProgress, 80);
|
|
2754
|
+
|
|
2673
2755
|
// Phase 1: Login all accounts (optimized for speed)
|
|
2674
2756
|
const LOGIN_PROGRESS_EVERY = 10;
|
|
2675
2757
|
// Reduced delays: 50-150ms between logins (faster startup for 1k+ accounts)
|
|
@@ -2701,18 +2783,24 @@ async function start(apiKey, apiUrl) {
|
|
|
2701
2783
|
workers.push(worker);
|
|
2702
2784
|
workerMap.set(acc.id, worker);
|
|
2703
2785
|
await worker.start();
|
|
2786
|
+
loginDone++;
|
|
2704
2787
|
}));
|
|
2705
2788
|
|
|
2706
2789
|
// Small gap between batches
|
|
2707
2790
|
if (i + BATCH_SIZE < accounts.length) {
|
|
2708
2791
|
const gapMs = randomLoginGap();
|
|
2709
|
-
log('info', `${c.dim}Logged in ${Math.min(i + BATCH_SIZE, accounts.length)}/${accounts.length}...${c.reset}`);
|
|
2710
2792
|
await new Promise(r => setTimeout(r, gapMs));
|
|
2711
2793
|
}
|
|
2712
2794
|
|
|
2713
2795
|
hintGC();
|
|
2714
2796
|
}
|
|
2715
2797
|
|
|
2798
|
+
clearInterval(progressInterval);
|
|
2799
|
+
// Clear the progress line and show done
|
|
2800
|
+
process.stdout.write(`\r${c.clearLine}`);
|
|
2801
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Login complete${c.reset} ${rgb(52, 211, 153)}${loginDone}/${accounts.length}${c.reset} ${c.dim}accounts connected${c.reset}`);
|
|
2802
|
+
console.log('');
|
|
2803
|
+
|
|
2716
2804
|
// Login summary: show invalid tokens clearly
|
|
2717
2805
|
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
2718
2806
|
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
@@ -2732,12 +2820,24 @@ async function start(apiKey, apiUrl) {
|
|
|
2732
2820
|
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
2733
2821
|
|
|
2734
2822
|
// Phase 2: Run inventory on ALL valid accounts in parallel (must complete before grinding)
|
|
2735
|
-
log(
|
|
2823
|
+
console.log(` ${rgb(139, 92, 246)}${BRAILLE_SPIN[0]}${c.reset} ${c.dim}Checking inventory for ${c.reset}${c.bold}${activeWorkers.length}${c.reset}${c.dim} accounts...${c.reset}`);
|
|
2736
2824
|
|
|
2737
|
-
//
|
|
2825
|
+
// Animated inventory progress
|
|
2738
2826
|
let invDone = 0;
|
|
2739
2827
|
let invFailed = 0;
|
|
2740
2828
|
const total = activeWorkers.length;
|
|
2829
|
+
const invBarW = Math.min(40, (process.stdout.columns || 80) - 30);
|
|
2830
|
+
|
|
2831
|
+
const drawInvProgress = () => {
|
|
2832
|
+
const pct = total > 0 ? invDone / total : 0;
|
|
2833
|
+
const filled = Math.round(pct * invBarW);
|
|
2834
|
+
const empty = invBarW - filled;
|
|
2835
|
+
const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
|
|
2836
|
+
const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(empty) + c.reset;
|
|
2837
|
+
process.stdout.write(`\r ${rgb(34, 211, 238)}${spin}${c.reset} ${c.dim}Inventory...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${invDone}${c.reset}${c.dim}/${c.reset}${c.white}${total}${c.reset} ${c.dim}${Math.round(pct * 100)}%${c.reset} `);
|
|
2838
|
+
};
|
|
2839
|
+
|
|
2840
|
+
const invProgressInterval = setInterval(drawInvProgress, 80);
|
|
2741
2841
|
|
|
2742
2842
|
await Promise.all(activeWorkers.map(async (w, i) => {
|
|
2743
2843
|
try {
|
|
@@ -2754,16 +2854,20 @@ async function start(apiKey, apiUrl) {
|
|
|
2754
2854
|
}
|
|
2755
2855
|
}));
|
|
2756
2856
|
|
|
2757
|
-
|
|
2758
|
-
|
|
2857
|
+
clearInterval(invProgressInterval);
|
|
2858
|
+
process.stdout.write(`\r${c.clearLine}`);
|
|
2759
2859
|
|
|
2860
|
+
// Final summary
|
|
2760
2861
|
if (invFailed > 0) {
|
|
2761
|
-
log(
|
|
2862
|
+
console.log(` ${rgb(239, 68, 68)}✗${c.reset} ${c.bold}Inventory incomplete${c.reset} ${rgb(52, 211, 153)}${invDone}${c.reset}${c.dim}/${c.reset}${c.white}${total}${c.reset} done, ${rgb(239, 68, 68)}${invFailed} failed${c.reset}`);
|
|
2863
|
+
log('error', `${c.red}Not starting grind loops — ${invFailed} accounts failed inventory.${c.reset}`);
|
|
2762
2864
|
return;
|
|
2763
2865
|
}
|
|
2764
2866
|
|
|
2765
|
-
|
|
2766
|
-
log('
|
|
2867
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Inventory complete${c.reset} ${rgb(52, 211, 153)}${invDone}/${total}${c.reset} ${c.dim}all clear${c.reset}`);
|
|
2868
|
+
console.log('');
|
|
2869
|
+
console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
|
|
2870
|
+
console.log('');
|
|
2767
2871
|
|
|
2768
2872
|
// Phase 3: Start all grind loops (only for valid workers)
|
|
2769
2873
|
for (const w of activeWorkers) {
|
|
@@ -2780,6 +2884,13 @@ async function start(apiKey, apiUrl) {
|
|
|
2780
2884
|
process.stdout.write(c.hide);
|
|
2781
2885
|
dashboardLines = 0;
|
|
2782
2886
|
|
|
2887
|
+
// Re-render on terminal resize so layout adapts to window size
|
|
2888
|
+
process.stdout.on('resize', () => {
|
|
2889
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
2890
|
+
dashboardLines = 0;
|
|
2891
|
+
scheduleRender();
|
|
2892
|
+
});
|
|
2893
|
+
|
|
2783
2894
|
setInterval(() => scheduleRender(), 1000);
|
|
2784
2895
|
scheduleRender();
|
|
2785
2896
|
|
|
@@ -2928,10 +3039,17 @@ function setupKeyboardShortcuts() {
|
|
|
2928
3039
|
process.stdin.resume();
|
|
2929
3040
|
process.stdin.setEncoding('utf8');
|
|
2930
3041
|
|
|
2931
|
-
//
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
3042
|
+
// Premium styled keyboard shortcuts with gradient box
|
|
3043
|
+
const accent = rgb(139, 92, 246);
|
|
3044
|
+
const dim = c.dim;
|
|
3045
|
+
const kw = 60;
|
|
3046
|
+
console.log('');
|
|
3047
|
+
console.log(` ${accent}╭${'─'.repeat(kw)}╮${c.reset}`);
|
|
3048
|
+
console.log(` ${accent}│${c.reset} ${gradientText('KEYBOARD SHORTCUTS', [192, 132, 252], [52, 211, 153])}${' '.repeat(kw - 20)}${accent}│${c.reset}`);
|
|
3049
|
+
console.log(` ${accent}├${'─'.repeat(kw)}┤${c.reset}`);
|
|
3050
|
+
console.log(` ${accent}│${c.reset} ${rgb(96, 165, 250)}P${c.reset} ${dim}Pause all${c.reset} ${rgb(52, 211, 153)}R${c.reset} ${dim}Resume all${c.reset} ${rgb(251, 191, 36)}S${c.reset} ${dim}Status${c.reset} ${rgb(239, 68, 68)}Q${c.reset} ${dim}Quit${c.reset}${' '.repeat(Math.max(0, kw - 54))}${accent}│${c.reset}`);
|
|
3051
|
+
console.log(` ${accent}╰${'─'.repeat(kw)}╯${c.reset}`);
|
|
3052
|
+
console.log('');
|
|
2935
3053
|
|
|
2936
3054
|
process.stdin.on('data', (key) => {
|
|
2937
3055
|
const k = key.toString().toLowerCase();
|