dankgrinder 6.0.0 → 6.3.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 +302 -134
- package/package.json +1 -1
package/lib/grinder.js
CHANGED
|
@@ -219,6 +219,23 @@ function gradientLine(text, from, to) {
|
|
|
219
219
|
return out + c.reset;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
// ── Sparkline graph for earnings trend ───────────────────────
|
|
223
|
+
const SPARK_CHARS = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
|
224
|
+
function drawSparkline(data, width = 12) {
|
|
225
|
+
if (!data || data.length === 0) return c.dim + '──────' + c.reset;
|
|
226
|
+
const recent = data.slice(-width);
|
|
227
|
+
const min = Math.min(...recent);
|
|
228
|
+
const max = Math.max(...recent);
|
|
229
|
+
const range = max - min || 1;
|
|
230
|
+
return recent.map(v => {
|
|
231
|
+
const idx = Math.min(7, Math.floor(((v - min) / range) * 8));
|
|
232
|
+
const char = SPARK_CHARS[idx] || '▁';
|
|
233
|
+
return v >= max * 0.9 ? rgb(52, 211, 153) + char + c.reset :
|
|
234
|
+
v <= min * 1.1 ? rgb(239, 68, 68) + char + c.reset :
|
|
235
|
+
rgb(139, 92, 246) + char + c.reset;
|
|
236
|
+
}).join('');
|
|
237
|
+
}
|
|
238
|
+
|
|
222
239
|
const BANNER_RAW = [
|
|
223
240
|
' ██████╗ █████╗ ███╗ ██╗██╗ ██╗',
|
|
224
241
|
' ██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝',
|
|
@@ -264,10 +281,15 @@ let totalCoins = 0;
|
|
|
264
281
|
let totalCommands = 0;
|
|
265
282
|
let startTime = Date.now();
|
|
266
283
|
let shutdownCalled = false;
|
|
284
|
+
let sessionPeakCoins = 0;
|
|
285
|
+
let isNewHigh = false;
|
|
267
286
|
// RingBuffer: O(1) push, bounded memory, no array shifting or GC pressure
|
|
268
287
|
const recentLogs = new RingBuffer(6);
|
|
269
288
|
const MAX_LOGS = 6;
|
|
270
289
|
const RENDER_THROTTLE_MS = 250;
|
|
290
|
+
// Earnings history for sparkline (sample every 10 seconds)
|
|
291
|
+
const earningsHistory = new RingBuffer(20);
|
|
292
|
+
let lastEarningsSample = 0;
|
|
271
293
|
|
|
272
294
|
function formatUptime() {
|
|
273
295
|
const s = Math.floor((Date.now() - startTime) / 1000);
|
|
@@ -311,70 +333,109 @@ function renderDashboard() {
|
|
|
311
333
|
totalErrors += w.stats.errors || 0;
|
|
312
334
|
}
|
|
313
335
|
const successRate = totalCommands > 0 ? Math.round(((totalCommands - totalErrors) / totalCommands) * 100) : 100;
|
|
336
|
+
// Track session peak and new high
|
|
337
|
+
if (totalCoins > sessionPeakCoins) {
|
|
338
|
+
sessionPeakCoins = totalCoins;
|
|
339
|
+
isNewHigh = true;
|
|
340
|
+
setTimeout(() => { isNewHigh = false; }, 3000);
|
|
341
|
+
}
|
|
314
342
|
|
|
315
343
|
const lines = [];
|
|
316
344
|
const tw = Math.min(process.stdout.columns || 80, 78);
|
|
317
345
|
const thinBar = c.dim + '─'.repeat(tw) + c.reset;
|
|
318
346
|
const bar = rgb(139, 92, 246) + c.bold + '━'.repeat(tw) + c.reset;
|
|
319
347
|
const doubleBar = rgb(139, 92, 246) + '╔' + '═'.repeat(tw - 2) + '╗' + c.reset;
|
|
348
|
+
const doubleBarMid = rgb(139, 92, 246) + '╠' + '═'.repeat(tw - 2) + '╣' + c.reset;
|
|
320
349
|
const doubleBarBot = rgb(139, 92, 246) + '╚' + '═'.repeat(tw - 2) + '╝' + c.reset;
|
|
321
350
|
|
|
322
|
-
// Header with
|
|
351
|
+
// Header with title, stats, and mode
|
|
323
352
|
lines.push(doubleBar);
|
|
324
353
|
const cmdCount = AccountWorker.COMMAND_MAP.length;
|
|
325
354
|
const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
|
|
326
355
|
const mode = CLUSTER_ENABLED ? `${rgb(34, 211, 238)}Cluster${c.reset}` : `${c.dim}Standalone${c.reset}`;
|
|
327
356
|
|
|
328
|
-
// Animated spinner
|
|
357
|
+
// Animated spinner and gradient title
|
|
329
358
|
const spinners = ['◐', '◓', '◑', '◒'];
|
|
330
359
|
const spinner = spinners[Math.floor(Date.now() / 250) % 4];
|
|
331
360
|
const animatedSpinner = `${rgb(52, 211, 153)}${spinner}${c.reset}`;
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
361
|
+
const gradientTitle = gradientLine('DankGrinder', [192, 132, 252], [34, 211, 238]);
|
|
362
|
+
|
|
363
|
+
// Network quality indicator (based on recent latency/errors)
|
|
364
|
+
const netQuality = workers.length > 0
|
|
365
|
+
? workers.reduce((s, w) => s + (w._lastPing < 200 ? 1 : w._lastPing < 500 ? 0.5 : 0), 0) / workers.length
|
|
366
|
+
: 1;
|
|
367
|
+
const netIcon = netQuality > 0.8 ? `${rgb(52, 211, 153)}◉${c.reset}` :
|
|
368
|
+
netQuality > 0.5 ? `${rgb(251, 191, 36)}◉${c.reset}` :
|
|
369
|
+
`${rgb(239, 68, 68)}◉${c.reset}`;
|
|
370
|
+
const netLabel = netQuality > 0.8 ? `${c.dim}good${c.reset}` :
|
|
371
|
+
netQuality > 0.5 ? `${c.dim}fair${c.reset}` :
|
|
372
|
+
`${c.dim}poor${c.reset}`;
|
|
373
|
+
|
|
374
|
+
lines.push(` ${c.bold}${gradientTitle}${c.reset} ${c.dim}v${PKG_VERSION}${c.reset}`);
|
|
375
|
+
lines.push(` ${mode} ${c.dim}·${c.reset} ${netIcon} ${netLabel} ${c.dim}·${c.reset} ${c.white}${cmdCount} commands${c.reset} ${c.dim}·${c.reset} ${animatedSpinner} ${rgb(52, 211, 153)}${activeCount}${c.reset}${c.dim}/${c.reset}${c.white}${workers.length}${c.reset} ${c.dim}live${c.reset}`);
|
|
376
|
+
|
|
377
|
+
// Stats row 1: Balance & Earnings
|
|
378
|
+
lines.push(doubleBarMid);
|
|
341
379
|
const liveIcon = rgb(52, 211, 153) + '●' + c.reset;
|
|
342
380
|
const balStr = `${rgb(192, 132, 252)}${c.bold}⏣ ${formatCoins(totalBalance)}${c.reset}`;
|
|
343
381
|
const earnStr = `${rgb(52, 211, 153)}▲ ${formatCoins(totalCoins)}${c.reset}`;
|
|
344
|
-
// Coins/hour rate
|
|
345
382
|
const elapsedHrs = (Date.now() - startTime) / 3_600_000;
|
|
346
383
|
const coinsPerHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
|
|
347
384
|
const rateLabel = `${rgb(52, 211, 153)}${formatCoins(coinsPerHr)}/h${c.reset}`;
|
|
385
|
+
|
|
386
|
+
// Stats row 2: Commands & Performance
|
|
348
387
|
const cmdStr = `${rgb(96, 165, 250)}${totalCommands}${c.reset}${c.dim} cmds${c.reset}`;
|
|
349
388
|
const rateStr = successRate >= 95
|
|
350
389
|
? `${rgb(52, 211, 153)}● ${successRate}%${c.reset}`
|
|
351
390
|
: successRate >= 80 ? `${rgb(251, 191, 36)}● ${successRate}%${c.reset}`
|
|
352
391
|
: `${rgb(239, 68, 68)}● ${successRate}%${c.reset}`;
|
|
392
|
+
const cpmVal = globalCmdRate.getRate().toFixed(1);
|
|
393
|
+
const cpmStr = `${rgb(34, 211, 238)}${cpmVal}${c.reset}${c.dim}/m${c.reset}`;
|
|
394
|
+
|
|
395
|
+
// Stats row 3: Uptime & Memory
|
|
353
396
|
const upStr = `${rgb(251, 191, 36)}◷ ${formatUptime()}${c.reset}`;
|
|
354
|
-
// Memory usage (RSS in MB) with bar indicator
|
|
355
397
|
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
356
398
|
const memPct = Math.min(100, (memMB / 1024) * 100);
|
|
357
399
|
const memBarWidth = Math.floor((memPct / 100) * 10);
|
|
358
400
|
const memBar = rgb(52, 211, 153) + '▅'.repeat(memBarWidth) + c.dim + '▅'.repeat(10 - memBarWidth) + c.reset;
|
|
359
401
|
const memColor = memMB > 900 ? rgb(239, 68, 68) : memMB > 600 ? rgb(251, 191, 36) : rgb(52, 211, 153);
|
|
360
402
|
const memStr = `${memColor}${memMB}MB${c.reset} ${memBar}`;
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
const
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
403
|
+
|
|
404
|
+
// Sample earnings for sparkline every 10 seconds
|
|
405
|
+
const now = Date.now();
|
|
406
|
+
if (now - lastEarningsSample > 10000) {
|
|
407
|
+
earningsHistory.push(totalCoins);
|
|
408
|
+
lastEarningsSample = now;
|
|
409
|
+
}
|
|
410
|
+
const sparkline = drawSparkline(earningsHistory.toArray(), 16);
|
|
411
|
+
const peakIndicator = isNewHigh ? `${rgb(255, 100, 100)} 🚀 NEW HIGH!${c.reset}` : '';
|
|
412
|
+
|
|
413
|
+
lines.push(` ${liveIcon} ${balStr} ${c.dim}│${c.reset} ${earnStr} ${c.dim}(${c.reset}${rateLabel}${c.dim})${c.reset}${peakIndicator}`);
|
|
414
|
+
lines.push(` ${cmdStr} ${c.dim}(${c.reset}${rateStr}${c.dim})${c.reset} ${c.dim}│${c.reset} ${cpmStr} ${c.dim}│${c.reset} ${upStr}`);
|
|
415
|
+
lines.push(` ${c.dim}Peak:${c.reset} ${rgb(255, 215, 0)}⏣ ${formatCoins(sessionPeakCoins)}${c.reset} ${c.dim}│${c.reset} Trend: ${sparkline} ${c.dim}│${c.reset} ${memStr}`);
|
|
367
416
|
lines.push(thinBar);
|
|
368
417
|
|
|
369
418
|
// Worker rows — paginated for 10K+ accounts
|
|
370
419
|
// Renders up to MAX_VISIBLE_WORKERS rows individually, then shows
|
|
371
420
|
// a compact summary for the rest. This keeps terminal responsive.
|
|
372
|
-
const MAX_VISIBLE_WORKERS =
|
|
373
|
-
const nameWidth = Math.min(
|
|
374
|
-
const statusWidth = Math.max(
|
|
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);
|
|
375
424
|
const RE_ANSI_STRIP = /\x1b\[[0-9;]*m/g;
|
|
376
425
|
|
|
377
|
-
|
|
426
|
+
// Find top 3 earners for highlighting
|
|
427
|
+
const topEarners = [...workers]
|
|
428
|
+
.filter(w => w.running && (w.stats.coins || 0) > 0)
|
|
429
|
+
.sort((a, b) => (b.stats.coins || 0) - (a.stats.coins || 0))
|
|
430
|
+
.slice(0, 3);
|
|
431
|
+
const topEarnerIds = new Set(topEarners.map(w => w.account.id));
|
|
432
|
+
|
|
433
|
+
// Column headers
|
|
434
|
+
lines.push(` ${c.dim}#${c.reset} ${c.dim}Account${' '.repeat(nameWidth - 5)}${c.reset} ${c.dim}Balance${c.reset} ${c.dim}Earned${c.reset} ${c.dim}Status${c.reset}`);
|
|
435
|
+
lines.push(` ${c.dim}${'─'.repeat(tw - 4)}${c.reset}`);
|
|
436
|
+
|
|
437
|
+
const renderWorkerRow = (wk, index) => {
|
|
438
|
+
const pos = `${c.dim}#${c.reset}${c.bold}${(index + 1).toString().padStart(2, ' ')}${c.reset}`;
|
|
378
439
|
const rawStatus = (wk.lastStatus || 'idle').replace(RE_ANSI_STRIP, '');
|
|
379
440
|
const last = rawStatus.substring(0, statusWidth);
|
|
380
441
|
|
|
@@ -401,12 +462,19 @@ function renderDashboard() {
|
|
|
401
462
|
stateLabel = `${c.dim}${last}${c.reset}`;
|
|
402
463
|
}
|
|
403
464
|
|
|
465
|
+
// Top earner medal
|
|
466
|
+
let medal = ' ';
|
|
467
|
+
if (topEarnerIds.has(wk.account.id)) {
|
|
468
|
+
const rank = topEarners.findIndex(e => e.account.id === wk.account.id);
|
|
469
|
+
medal = rank === 0 ? `${rgb(255, 215, 0)}🥇${c.reset}` : rank === 1 ? `${rgb(192, 192, 192)}🥈${c.reset}` : `${rgb(205, 127, 50)}🥉${c.reset}`;
|
|
470
|
+
}
|
|
471
|
+
|
|
404
472
|
const name = `${wk.color}${c.bold}${(wk.username || '?').substring(0, nameWidth)}${c.reset}`;
|
|
405
473
|
const bal = wk.stats.balance > 0
|
|
406
|
-
? `${rgb(251, 191, 36)}⏣${c.reset} ${c.white}${formatCoins(wk.stats.balance).padStart(
|
|
407
|
-
: `${c.dim}⏣
|
|
474
|
+
? `${rgb(251, 191, 36)}⏣${c.reset} ${c.white}${formatCoins(wk.stats.balance).padStart(8)}${c.reset}`
|
|
475
|
+
: `${c.dim}⏣ -${c.reset}`;
|
|
408
476
|
|
|
409
|
-
//
|
|
477
|
+
// Enhanced progress bar for earned coins
|
|
410
478
|
const earnedNum = wk.stats.coins || 0;
|
|
411
479
|
const earnedBarWidth = earnedNum > 0 ? Math.min(5, Math.max(1, Math.floor(Math.log10(earnedNum + 1)))) : 0;
|
|
412
480
|
const earnedBar = earnedNum > 0
|
|
@@ -416,14 +484,14 @@ function renderDashboard() {
|
|
|
416
484
|
? `${rgb(52, 211, 153)}+${formatCoins(earnedNum)}${c.reset} ${earnedBar}`
|
|
417
485
|
: `${c.dim}+0${c.reset} ${earnedBar}`;
|
|
418
486
|
|
|
419
|
-
lines.push(` ${dot} ${name.padEnd(nameWidth + wk.color.length + c.bold.length + c.reset.length)} ${bal} ${earned} ${stateLabel}`);
|
|
487
|
+
lines.push(` ${pos} ${medal}${dot} ${name.padEnd(nameWidth + wk.color.length + c.bold.length + c.reset.length + 2)} ${bal} ${earned} ${stateLabel}`);
|
|
420
488
|
};
|
|
421
489
|
|
|
422
490
|
if (workers.length <= MAX_VISIBLE_WORKERS) {
|
|
423
|
-
for (let i = 0; i < workers.length; i++) renderWorkerRow(workers[i]);
|
|
491
|
+
for (let i = 0; i < workers.length; i++) renderWorkerRow(workers[i], i);
|
|
424
492
|
} else {
|
|
425
493
|
// Pagination: show first MAX_VISIBLE_WORKERS, summarize rest
|
|
426
|
-
for (let i = 0; i < MAX_VISIBLE_WORKERS; i++) renderWorkerRow(workers[i]);
|
|
494
|
+
for (let i = 0; i < MAX_VISIBLE_WORKERS; i++) renderWorkerRow(workers[i], i);
|
|
427
495
|
const remaining = workers.length - MAX_VISIBLE_WORKERS;
|
|
428
496
|
let hiddenActive = 0, hiddenPaused = 0, hiddenRecovering = 0, hiddenOffline = 0;
|
|
429
497
|
for (let i = MAX_VISIBLE_WORKERS; i < workers.length; i++) {
|
|
@@ -433,31 +501,33 @@ function renderDashboard() {
|
|
|
433
501
|
else if (w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()) hiddenRecovering++;
|
|
434
502
|
else hiddenActive++;
|
|
435
503
|
}
|
|
436
|
-
const parts = [`${c.dim}... +${remaining} more${c.reset}`];
|
|
437
|
-
if (hiddenActive > 0) parts.push(`${rgb(52, 211, 153)}${hiddenActive} active${c.reset}`);
|
|
438
|
-
if (hiddenPaused > 0) parts.push(`${rgb(251, 191, 36)}${hiddenPaused} paused${c.reset}`);
|
|
439
|
-
if (hiddenRecovering > 0) parts.push(`${rgb(251, 191, 36)}${hiddenRecovering} recovering${c.reset}`);
|
|
440
|
-
if (hiddenOffline > 0) parts.push(`${c.dim}${hiddenOffline} offline${c.reset}`);
|
|
504
|
+
const parts = [`${c.dim}┃${c.reset} ${c.dim}... +${remaining} more${c.reset}`];
|
|
505
|
+
if (hiddenActive > 0) parts.push(`${rgb(52, 211, 153)}● ${hiddenActive} active${c.reset}`);
|
|
506
|
+
if (hiddenPaused > 0) parts.push(`${rgb(251, 191, 36)}⏸ ${hiddenPaused} paused${c.reset}`);
|
|
507
|
+
if (hiddenRecovering > 0) parts.push(`${rgb(251, 191, 36)}↻ ${hiddenRecovering} recovering${c.reset}`);
|
|
508
|
+
if (hiddenOffline > 0) parts.push(`${c.dim}○ ${hiddenOffline} offline${c.reset}`);
|
|
441
509
|
lines.push(` ${parts.join(` ${c.dim}·${c.reset} `)}`);
|
|
442
510
|
}
|
|
443
511
|
|
|
444
|
-
// Recovery summary line
|
|
512
|
+
// Recovery & status summary line with enhanced styling
|
|
445
513
|
const recoveringCount = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
|
|
446
514
|
const pausedCount = workers.filter(w => w.paused).length;
|
|
447
515
|
const totalRecoveries = workers.reduce((s, w) => s + (w._totalRecoveries || 0), 0);
|
|
448
|
-
|
|
516
|
+
const totalDisconnects = workers.reduce((s, w) => s + (w._disconnectCount || 0), 0);
|
|
517
|
+
if (recoveringCount > 0 || pausedCount > 0 || totalRecoveries > 0 || totalDisconnects > 0) {
|
|
449
518
|
const parts = [];
|
|
450
519
|
if (recoveringCount > 0) parts.push(`${rgb(251, 191, 36)}↻ ${recoveringCount} recovering${c.reset}`);
|
|
451
520
|
if (pausedCount > 0) parts.push(`${rgb(239, 68, 68)}⏸ ${pausedCount} paused${c.reset}`);
|
|
452
521
|
if (totalRecoveries > 0) parts.push(`${c.dim}${totalRecoveries} auto-recovered${c.reset}`);
|
|
453
|
-
|
|
522
|
+
if (totalDisconnects > 0) parts.push(`${c.dim}${totalDisconnects} reconnects${c.reset}`);
|
|
523
|
+
lines.push(` ${c.dim}┃${c.reset} ${parts.join(` ${c.dim}·${c.reset} `)}`);
|
|
454
524
|
}
|
|
455
525
|
|
|
456
|
-
// Cluster info line
|
|
526
|
+
// Cluster info line with enhanced styling
|
|
457
527
|
if (CLUSTER_ENABLED) {
|
|
458
528
|
const nodeShort = NODE_ID.substring(0, 12);
|
|
459
529
|
const claimedCount = workers.filter(w => w.running).length;
|
|
460
|
-
lines.push(` ${rgb(34, 211, 238)}
|
|
530
|
+
lines.push(` ${rgb(34, 211, 238)}╔${c.reset}${c.dim} Cluster: ${nodeShort} · ${claimedCount} claimed ${c.reset}${rgb(34, 211, 238)}╗${c.reset}`);
|
|
461
531
|
}
|
|
462
532
|
|
|
463
533
|
// Log section
|
|
@@ -1170,11 +1240,10 @@ class AccountWorker {
|
|
|
1170
1240
|
const tries = Math.max(1, Number.isFinite(maxAttempts) ? maxAttempts : 1);
|
|
1171
1241
|
let lastErr = null;
|
|
1172
1242
|
for (let attempt = 1; attempt <= tries; attempt++) {
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
}
|
|
1243
|
+
const baseLabel = startupProgress ? `[inv] ${startupProgress.current}/${startupProgress.total}` : '[inv]';
|
|
1244
|
+
const attemptLabel = tries > 1 ? ` [try ${attempt}/${tries}]` : '';
|
|
1245
|
+
const progressLine = `${baseLabel}${c.bold} ${this.username}${c.reset}${attemptLabel}`;
|
|
1246
|
+
process.stdout.write(`\x1b[2K\r${progressLine}`);
|
|
1178
1247
|
|
|
1179
1248
|
try {
|
|
1180
1249
|
const result = await commands.runInventory({
|
|
@@ -1184,9 +1253,9 @@ class AccountWorker {
|
|
|
1184
1253
|
accountId: this.account.id,
|
|
1185
1254
|
redis,
|
|
1186
1255
|
onPageProgress: ({ page, total }) => {
|
|
1187
|
-
//
|
|
1256
|
+
// Update progress on same line
|
|
1188
1257
|
const erase = '\x1b[2K\r';
|
|
1189
|
-
process.stdout.write(`${erase}${this.
|
|
1258
|
+
process.stdout.write(`${erase}${baseLabel} ${c.bold}${this.username}${c.reset} · page ${page}/${total}${attemptLabel}`);
|
|
1190
1259
|
},
|
|
1191
1260
|
});
|
|
1192
1261
|
|
|
@@ -1194,9 +1263,9 @@ class AccountWorker {
|
|
|
1194
1263
|
throw new Error(`incomplete pages (${result.pagesVisited || 0}/${result.pagesTotal || 0})`);
|
|
1195
1264
|
}
|
|
1196
1265
|
|
|
1197
|
-
//
|
|
1198
|
-
|
|
1199
|
-
|
|
1266
|
+
// Final result on same line
|
|
1267
|
+
const resultLine = `${baseLabel} ${c.bold}${this.username}${c.reset}: ${c.green}${result.items?.length || 0} items${c.reset}, ⏣ ${c.green}${(result.totalValue || 0).toLocaleString()}${c.reset} net${attemptLabel}`;
|
|
1268
|
+
process.stdout.write(`\x1b[2K\r${resultLine}\n`);
|
|
1200
1269
|
try {
|
|
1201
1270
|
await fetch(`${API_URL}/api/grinder/inventory`, {
|
|
1202
1271
|
method: 'POST',
|
|
@@ -1220,7 +1289,9 @@ class AccountWorker {
|
|
|
1220
1289
|
} catch (e) {
|
|
1221
1290
|
lastErr = e;
|
|
1222
1291
|
if (attempt < tries) {
|
|
1223
|
-
|
|
1292
|
+
const baseLabel = startupProgress ? `[inv] ${startupProgress.current}/${startupProgress.total}` : '[inv]';
|
|
1293
|
+
const retryLine = `${baseLabel} ${c.bold}${this.username}${c.reset}: ${c.yellow}attempt ${attempt}/${tries} failed${c.reset} — retrying...`;
|
|
1294
|
+
process.stdout.write(`\x1b[2K\r${retryLine}\n`);
|
|
1224
1295
|
await new Promise((r) => setTimeout(r, 1500 + Math.floor(Math.random() * 1500)));
|
|
1225
1296
|
continue;
|
|
1226
1297
|
}
|
|
@@ -1229,7 +1300,9 @@ class AccountWorker {
|
|
|
1229
1300
|
|
|
1230
1301
|
throw lastErr || new Error('inventory check failed');
|
|
1231
1302
|
} catch (e) {
|
|
1232
|
-
|
|
1303
|
+
const baseLabel = startupProgress ? `[inv] ${startupProgress.current}/${startupProgress.total}` : '[inv]';
|
|
1304
|
+
const failLine = `${baseLabel} ${c.bold}${this.username}${c.reset}: ${c.red}failed${c.reset} — ${e.message}`;
|
|
1305
|
+
process.stdout.write(`\x1b[2K\r${failLine}\n`);
|
|
1233
1306
|
return { ok: false, error: e.message };
|
|
1234
1307
|
} finally {
|
|
1235
1308
|
this._invRunning = false;
|
|
@@ -2114,7 +2187,7 @@ class AccountWorker {
|
|
|
2114
2187
|
}
|
|
2115
2188
|
|
|
2116
2189
|
const prefix = this.account.use_slash ? '/' : 'pls';
|
|
2117
|
-
this.setStatus(
|
|
2190
|
+
this.setStatus(formatCommandName(item.cmd));
|
|
2118
2191
|
|
|
2119
2192
|
// Report "running" to dashboard
|
|
2120
2193
|
const nextItemRun = this.commandQueue?.peek?.();
|
|
@@ -2226,7 +2299,7 @@ class AccountWorker {
|
|
|
2226
2299
|
if (text.includes('alert') || text.includes('notification') ||
|
|
2227
2300
|
text.includes('you have a pending') || text.includes('check your alerts')) {
|
|
2228
2301
|
if (!this.busy) {
|
|
2229
|
-
this.log('info', 'Alert detected → running
|
|
2302
|
+
this.log('info', 'Alert detected → running alert');
|
|
2230
2303
|
this.busy = true;
|
|
2231
2304
|
const prefix = this.account.use_slash ? '/' : 'pls';
|
|
2232
2305
|
this.runCommand('alert', prefix).finally(() => { this.busy = false; });
|
|
@@ -2327,22 +2400,37 @@ class AccountWorker {
|
|
|
2327
2400
|
if (!this.account.discord_token) { this.log('error', 'No token'); return; }
|
|
2328
2401
|
if (!this.account.channel_id) { this.log('error', 'No channel'); return; }
|
|
2329
2402
|
|
|
2403
|
+
const LOGIN_TIMEOUT_MS = 30_000; // 30s max per account login
|
|
2404
|
+
|
|
2330
2405
|
return new Promise((resolve) => {
|
|
2406
|
+
let resolved = false;
|
|
2407
|
+
const done = () => { if (!resolved) { resolved = true; resolve(); } };
|
|
2408
|
+
|
|
2409
|
+
// Timeout guard — don't let a single account hang the batch
|
|
2410
|
+
const timeoutId = setTimeout(() => {
|
|
2411
|
+
if (!resolved) {
|
|
2412
|
+
this.log('warn', 'Login timed out after 30s — will retry in background');
|
|
2413
|
+
done();
|
|
2414
|
+
// Retry login in background after timeout
|
|
2415
|
+
this._retryLoginBackground();
|
|
2416
|
+
}
|
|
2417
|
+
}, LOGIN_TIMEOUT_MS);
|
|
2418
|
+
|
|
2331
2419
|
this.client.on('ready', async () => {
|
|
2420
|
+
clearTimeout(timeoutId);
|
|
2332
2421
|
this.username = this.client.user.tag || this.username;
|
|
2333
2422
|
this.avatarUrl = this.client.user.displayAvatarURL?.({ format: 'png', dynamic: true, size: 128 }) || null;
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
} catch { /* silent */ }
|
|
2423
|
+
// Report status non-blocking
|
|
2424
|
+
fetch(`${API_URL}/api/grinder/status`, {
|
|
2425
|
+
method: 'POST',
|
|
2426
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2427
|
+
body: JSON.stringify({ account_id: this.account.id, discord_username: this.username, avatar_url: this.avatarUrl }),
|
|
2428
|
+
}).catch(() => {});
|
|
2341
2429
|
|
|
2342
2430
|
this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
|
|
2343
2431
|
if (!this.channel) {
|
|
2344
2432
|
this.log('error', `Channel not found`);
|
|
2345
|
-
|
|
2433
|
+
done(); return;
|
|
2346
2434
|
}
|
|
2347
2435
|
|
|
2348
2436
|
const enabledCmds = [
|
|
@@ -2365,49 +2453,108 @@ class AccountWorker {
|
|
|
2365
2453
|
this.log('success', `#${chName} · ${enabledCmds.length} cmds`);
|
|
2366
2454
|
this.setStatus('starting...');
|
|
2367
2455
|
|
|
2368
|
-
// Load daily/weekly/monthly done state from Redis
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2456
|
+
// Load daily/weekly/monthly done state from Redis (non-blocking for login speed)
|
|
2457
|
+
this._loadRedisState().catch(() => {});
|
|
2458
|
+
|
|
2459
|
+
// Reduced settle time — 200ms is enough for most gateways
|
|
2460
|
+
await new Promise(r => setTimeout(r, 200));
|
|
2461
|
+
done();
|
|
2462
|
+
});
|
|
2463
|
+
|
|
2464
|
+
// Handle login errors so they don't hang
|
|
2465
|
+
this.client.on('error', (err) => {
|
|
2466
|
+
if (!resolved) {
|
|
2467
|
+
clearTimeout(timeoutId);
|
|
2468
|
+
const msg = (err?.message || '').toLowerCase();
|
|
2469
|
+
if (msg.includes('token_invalid') || msg.includes('invalid token') || msg.includes('401')) {
|
|
2470
|
+
this.log('error', `✗ TOKEN INVALID — this token is no longer valid`);
|
|
2471
|
+
this.paused = true;
|
|
2472
|
+
this._tokenInvalid = true;
|
|
2473
|
+
} else {
|
|
2474
|
+
this.log('error', `Login error: ${err?.message || err}`);
|
|
2381
2475
|
}
|
|
2382
|
-
|
|
2383
|
-
try {
|
|
2384
|
-
const balData = await redis.get(`dkg:bal:${this.account.id}`);
|
|
2385
|
-
if (balData) {
|
|
2386
|
-
const { wallet, bank } = JSON.parse(balData);
|
|
2387
|
-
if (wallet > 0 || bank > 0) {
|
|
2388
|
-
this.stats.balance = wallet;
|
|
2389
|
-
this.stats.bankBalance = bank;
|
|
2390
|
-
await fetch(`${API_URL}/api/grinder/status`, {
|
|
2391
|
-
method: 'POST',
|
|
2392
|
-
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2393
|
-
body: JSON.stringify({ account_id: this.account.id, balance: wallet, bank_balance: bank, total_balance: wallet + bank }),
|
|
2394
|
-
}).catch(() => {});
|
|
2395
|
-
}
|
|
2396
|
-
}
|
|
2397
|
-
} catch {}
|
|
2476
|
+
done();
|
|
2398
2477
|
}
|
|
2399
|
-
|
|
2400
|
-
// Let Discord gateway settle (reduced for faster startup)
|
|
2401
|
-
await new Promise(r => setTimeout(r, 500));
|
|
2402
|
-
resolve();
|
|
2403
2478
|
});
|
|
2404
2479
|
|
|
2405
2480
|
// Attach auto-recovery event listeners before login
|
|
2406
2481
|
this._attachRecoveryListeners();
|
|
2407
|
-
this.client.login(this.account.discord_token)
|
|
2482
|
+
this.client.login(this.account.discord_token).catch((err) => {
|
|
2483
|
+
clearTimeout(timeoutId);
|
|
2484
|
+
const msg = (err?.message || '').toLowerCase();
|
|
2485
|
+
if (msg.includes('token_invalid') || msg.includes('invalid token') || msg.includes('401') || msg.includes('incorrect login')) {
|
|
2486
|
+
this.log('error', `✗ TOKEN INVALID — check this account's token`);
|
|
2487
|
+
this.paused = true;
|
|
2488
|
+
this._tokenInvalid = true;
|
|
2489
|
+
// Report invalid status to API
|
|
2490
|
+
fetch(`${API_URL}/api/grinder/status`, {
|
|
2491
|
+
method: 'POST',
|
|
2492
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2493
|
+
body: JSON.stringify({ account_id: this.account.id, active: false, status: 'token_invalid' }),
|
|
2494
|
+
}).catch(() => {});
|
|
2495
|
+
sendWebhook('Token Invalid', `**${this.account.label || this.account.id}** has an invalid Discord token.`, 0xef4444);
|
|
2496
|
+
} else {
|
|
2497
|
+
this.log('error', `Login failed: ${err?.message || 'unknown error'}`);
|
|
2498
|
+
}
|
|
2499
|
+
done();
|
|
2500
|
+
});
|
|
2408
2501
|
});
|
|
2409
2502
|
}
|
|
2410
2503
|
|
|
2504
|
+
/** Load Redis cached state in background (non-blocking) */
|
|
2505
|
+
async _loadRedisState() {
|
|
2506
|
+
if (!redis) return;
|
|
2507
|
+
for (const cmd of ['daily', 'weekly', 'monthly', 'drops']) {
|
|
2508
|
+
try {
|
|
2509
|
+
const val = await redis.get(`dkg:done:${this.account.id}:${cmd}`);
|
|
2510
|
+
if (val) {
|
|
2511
|
+
const ttlSec = await redis.ttl(`dkg:done:${this.account.id}:${cmd}`);
|
|
2512
|
+
if (ttlSec > 0) {
|
|
2513
|
+
this.doneToday.set(cmd, Date.now() + ttlSec * 1000);
|
|
2514
|
+
this.log('info', `${cmd} already claimed (${Math.round(ttlSec / 3600)}h remaining)`);
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
} catch {}
|
|
2518
|
+
}
|
|
2519
|
+
// Load cached balance from Redis
|
|
2520
|
+
try {
|
|
2521
|
+
const balData = await redis.get(`dkg:bal:${this.account.id}`);
|
|
2522
|
+
if (balData) {
|
|
2523
|
+
const { wallet, bank } = JSON.parse(balData);
|
|
2524
|
+
if (wallet > 0 || bank > 0) {
|
|
2525
|
+
this.stats.balance = wallet;
|
|
2526
|
+
this.stats.bankBalance = bank;
|
|
2527
|
+
await fetch(`${API_URL}/api/grinder/status`, {
|
|
2528
|
+
method: 'POST',
|
|
2529
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2530
|
+
body: JSON.stringify({ account_id: this.account.id, balance: wallet, bank_balance: bank, total_balance: wallet + bank }),
|
|
2531
|
+
}).catch(() => {});
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
} catch {}
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
/** Retry login in background after a timeout */
|
|
2538
|
+
async _retryLoginBackground() {
|
|
2539
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
2540
|
+
if (this.client && !this.running) {
|
|
2541
|
+
this.log('info', 'Retrying login in background...');
|
|
2542
|
+
try {
|
|
2543
|
+
this.client.destroy();
|
|
2544
|
+
this.client = createLeanClient();
|
|
2545
|
+
this._attachRecoveryListeners();
|
|
2546
|
+
await this.client.login(this.account.discord_token);
|
|
2547
|
+
this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
|
|
2548
|
+
if (this.channel) {
|
|
2549
|
+
this.username = this.client.user?.tag || this.username;
|
|
2550
|
+
this.log('success', `Background login OK`);
|
|
2551
|
+
}
|
|
2552
|
+
} catch (err) {
|
|
2553
|
+
this.log('error', `Background login failed: ${err?.message || err}`);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2411
2558
|
stop() {
|
|
2412
2559
|
this.running = false;
|
|
2413
2560
|
this.paused = false;
|
|
@@ -2537,13 +2684,19 @@ async function start(apiKey, apiUrl) {
|
|
|
2537
2684
|
};
|
|
2538
2685
|
|
|
2539
2686
|
// Parallel login in batches of 10 to avoid rate limits while being fast
|
|
2687
|
+
// Within each batch, stagger logins by 100-600ms to avoid gateway flood
|
|
2540
2688
|
const BATCH_SIZE = 10;
|
|
2541
2689
|
for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
|
|
2542
2690
|
if (shutdownCalled) break;
|
|
2543
2691
|
const batch = accounts.slice(i, Math.min(i + BATCH_SIZE, accounts.length));
|
|
2544
2692
|
|
|
2545
|
-
//
|
|
2693
|
+
// Staggered parallel login: fire each login with a small jitter delay
|
|
2546
2694
|
await Promise.all(batch.map(async (acc, idx) => {
|
|
2695
|
+
// Stagger within batch: 0ms for first, 100-600ms for subsequent
|
|
2696
|
+
if (idx > 0) {
|
|
2697
|
+
const jitter = 100 + Math.floor(Math.random() * 500);
|
|
2698
|
+
await new Promise(r => setTimeout(r, jitter));
|
|
2699
|
+
}
|
|
2547
2700
|
const worker = new AccountWorker(acc, i + idx);
|
|
2548
2701
|
workers.push(worker);
|
|
2549
2702
|
workerMap.set(acc.id, worker);
|
|
@@ -2560,21 +2713,33 @@ async function start(apiKey, apiUrl) {
|
|
|
2560
2713
|
hintGC();
|
|
2561
2714
|
}
|
|
2562
2715
|
|
|
2563
|
-
//
|
|
2564
|
-
|
|
2716
|
+
// Login summary: show invalid tokens clearly
|
|
2717
|
+
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
2718
|
+
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
2719
|
+
if (invalidWorkers.length > 0) {
|
|
2720
|
+
console.log('');
|
|
2721
|
+
log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
|
|
2722
|
+
for (const w of invalidWorkers) {
|
|
2723
|
+
log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
|
|
2724
|
+
}
|
|
2725
|
+
console.log('');
|
|
2726
|
+
}
|
|
2727
|
+
if (timedOutWorkers.length > 0) {
|
|
2728
|
+
log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
// Filter out workers with invalid tokens from grinding
|
|
2732
|
+
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
2733
|
+
|
|
2734
|
+
// Phase 2: Run inventory on ALL valid accounts in parallel (must complete before grinding)
|
|
2735
|
+
log('info', `${c.dim}Checking inventory for ${activeWorkers.length} accounts...${c.reset}`);
|
|
2565
2736
|
|
|
2566
2737
|
// Parallel inventory checks with single-line progress
|
|
2567
2738
|
let invDone = 0;
|
|
2568
2739
|
let invFailed = 0;
|
|
2569
|
-
const total =
|
|
2570
|
-
|
|
2571
|
-
await Promise.all(workers.map(async (w, i) => {
|
|
2572
|
-
const label = w?.username || w?.account?.label || 'account';
|
|
2573
|
-
|
|
2574
|
-
// Update progress on same line
|
|
2575
|
-
const progress = `[inv] ${invDone + invFailed + 1}/${total}: ${label}`;
|
|
2576
|
-
process.stdout.write(`\x1b[2K\r${c.dim}${progress}${c.reset}`);
|
|
2740
|
+
const total = activeWorkers.length;
|
|
2577
2741
|
|
|
2742
|
+
await Promise.all(activeWorkers.map(async (w, i) => {
|
|
2578
2743
|
try {
|
|
2579
2744
|
const invRes = await w.checkInventory({
|
|
2580
2745
|
force: true,
|
|
@@ -2589,20 +2754,19 @@ async function start(apiKey, apiUrl) {
|
|
|
2589
2754
|
}
|
|
2590
2755
|
}));
|
|
2591
2756
|
|
|
2592
|
-
// Final
|
|
2593
|
-
process.stdout.write('\n');
|
|
2757
|
+
// Final summary
|
|
2594
2758
|
log('success', `Inventory: ${invDone}/${total} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}` : ''}`);
|
|
2595
2759
|
|
|
2596
2760
|
if (invFailed > 0) {
|
|
2597
|
-
log('error', `${c.red}Inventory phase incomplete: ${invDone}/${
|
|
2761
|
+
log('error', `${c.red}Inventory phase incomplete: ${invDone}/${activeWorkers.length} complete, ${invFailed} failed/incomplete. Not starting grind loops.${c.reset}`);
|
|
2598
2762
|
return;
|
|
2599
2763
|
}
|
|
2600
2764
|
|
|
2601
2765
|
const invSummaryColor = invFailed > 0 ? c.yellow : c.green;
|
|
2602
|
-
log('success', `${c.dim}Inventory phase complete: ${invSummaryColor}${invDone}/${
|
|
2766
|
+
log('success', `${c.dim}Inventory phase complete: ${invSummaryColor}${invDone}/${activeWorkers.length}${c.reset}${c.dim} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}${c.dim}` : ''}. Starting grind loops...${c.reset}`);
|
|
2603
2767
|
|
|
2604
|
-
// Phase 3: Start all grind loops
|
|
2605
|
-
for (const w of
|
|
2768
|
+
// Phase 3: Start all grind loops (only for valid workers)
|
|
2769
|
+
for (const w of activeWorkers) {
|
|
2606
2770
|
if (!shutdownCalled) w.grindLoop();
|
|
2607
2771
|
}
|
|
2608
2772
|
|
|
@@ -2764,7 +2928,10 @@ function setupKeyboardShortcuts() {
|
|
|
2764
2928
|
process.stdin.resume();
|
|
2765
2929
|
process.stdin.setEncoding('utf8');
|
|
2766
2930
|
|
|
2767
|
-
|
|
2931
|
+
// Modern styled keyboard shortcuts with box drawing
|
|
2932
|
+
console.log(`\n ${rgb(139, 92, 246)}╭${c.reset}${c.dim}${'─'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╮${c.reset}`);
|
|
2933
|
+
console.log(` ${rgb(139, 92, 246)}│${c.reset} ${c.bold}Shortcuts:${c.reset} ${c.dim}p${c.reset}=pause ${c.dim}r${c.reset}=resume ${c.dim}s${c.reset}=status ${c.dim}q${c.reset}=quit ${c.dim}?${c.reset}=help ${rgb(139, 92, 246)}│${c.reset}`);
|
|
2934
|
+
console.log(` ${rgb(139, 92, 246)}╰${c.reset}${c.dim}${'─'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╯${c.reset}`);
|
|
2768
2935
|
|
|
2769
2936
|
process.stdin.on('data', (key) => {
|
|
2770
2937
|
const k = key.toString().toLowerCase();
|
|
@@ -2780,7 +2947,9 @@ function setupKeyboardShortcuts() {
|
|
|
2780
2947
|
if (k === 'p') {
|
|
2781
2948
|
let paused = 0;
|
|
2782
2949
|
workers.forEach(w => { if (w.running && !w.paused) { w.paused = true; paused++; } });
|
|
2783
|
-
console.log(`\n ${c.
|
|
2950
|
+
console.log(`\n ${rgb(139, 92, 246)}╭${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╮${c.reset}`);
|
|
2951
|
+
console.log(` ${rgb(139, 92, 246)}│${c.reset} ${c.yellow}⏸ Paused ${paused} accounts${c.reset} ${rgb(139, 92, 246)}│${c.reset}`);
|
|
2952
|
+
console.log(` ${rgb(139, 92, 246)}╰${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╯${c.reset}`);
|
|
2784
2953
|
return;
|
|
2785
2954
|
}
|
|
2786
2955
|
|
|
@@ -2788,40 +2957,39 @@ function setupKeyboardShortcuts() {
|
|
|
2788
2957
|
if (k === 'r') {
|
|
2789
2958
|
let resumed = 0;
|
|
2790
2959
|
workers.forEach(w => { if (w.paused) { w.paused = false; resumed++; } });
|
|
2791
|
-
console.log(`\n ${c.
|
|
2960
|
+
console.log(`\n ${rgb(139, 92, 246)}╭${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╮${c.reset}`);
|
|
2961
|
+
console.log(` ${rgb(139, 92, 246)}│${c.reset} ${c.green}● Resumed ${resumed} accounts${c.reset} ${rgb(139, 92, 246)}│${c.reset}`);
|
|
2962
|
+
console.log(` ${rgb(139, 92, 246)}╰${c.reset}${c.dim}${'─'.repeat(40)}${c.reset}${rgb(139, 92, 246)}╯${c.reset}`);
|
|
2792
2963
|
return;
|
|
2793
2964
|
}
|
|
2794
2965
|
|
|
2795
2966
|
// s = show status summary
|
|
2796
2967
|
if (k === 's') {
|
|
2797
|
-
console.log(`\n ${c.bold}Status Summary:${c.reset}`);
|
|
2798
2968
|
const active = workers.filter(w => w.running && !w.paused).length;
|
|
2799
2969
|
const paused = workers.filter(w => w.paused).length;
|
|
2800
2970
|
const offline = workers.filter(w => !w.running).length;
|
|
2801
2971
|
const recovering = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
|
|
2802
|
-
|
|
2803
|
-
console.log(
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
if (num >= 1 && num <= 9 && workers[num - 1]) {
|
|
2810
|
-
const w = workers[num - 1];
|
|
2811
|
-
w.paused = !w.paused;
|
|
2812
|
-
console.log(`\n ${w.color}${w.username}${c.reset} ${w.paused ? c.yellow + 'paused' : c.green + 'resumed'}${c.reset}`);
|
|
2972
|
+
const totalEarn = workers.reduce((s, w) => s + (w.stats.coins || 0), 0);
|
|
2973
|
+
console.log(`\n ${rgb(139, 92, 246)}╔${c.reset}${c.bold}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╗${c.reset}`);
|
|
2974
|
+
console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.bold}Status Summary${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
|
|
2975
|
+
console.log(` ${rgb(139, 92, 246)}╠${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╣${c.reset}`);
|
|
2976
|
+
console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.green}● ${active} active${c.reset} ${c.yellow}⏸ ${paused} paused${c.reset} ${c.red}○ ${offline} offline${c.reset} ${c.yellow}↻ ${recovering} recovering${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
|
|
2977
|
+
console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.dim}Total earnings:${c.reset} ${rgb(52, 211, 153)}⏣ ${totalEarn.toLocaleString()}${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
|
|
2978
|
+
console.log(` ${rgb(139, 92, 246)}╚${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╝${c.reset}`);
|
|
2813
2979
|
return;
|
|
2814
2980
|
}
|
|
2815
2981
|
|
|
2816
2982
|
// ? = show help
|
|
2817
2983
|
if (k === '?' || k === 'h') {
|
|
2818
|
-
console.log(`\n ${c.bold}
|
|
2819
|
-
console.log(` ${c.
|
|
2820
|
-
console.log(` ${c.
|
|
2821
|
-
console.log(` ${c.white}
|
|
2822
|
-
console.log(` ${c.white}
|
|
2823
|
-
console.log(` ${c.white}
|
|
2824
|
-
console.log(` ${c.white}
|
|
2984
|
+
console.log(`\n ${rgb(139, 92, 246)}╔${c.reset}${c.bold}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╗${c.reset}`);
|
|
2985
|
+
console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.bold}Keyboard Shortcuts${c.reset} ${rgb(139, 92, 246)}║${c.reset}`);
|
|
2986
|
+
console.log(` ${rgb(139, 92, 246)}╠${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╣${c.reset}`);
|
|
2987
|
+
console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}p${c.reset} Pause all accounts ${rgb(139, 92, 246)}║${c.reset}`);
|
|
2988
|
+
console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}r${c.reset} Resume all accounts ${rgb(139, 92, 246)}║${c.reset}`);
|
|
2989
|
+
console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}s${c.reset} Show status summary ${rgb(139, 92, 246)}║${c.reset}`);
|
|
2990
|
+
console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}q${c.reset} Quit gracefully ${rgb(139, 92, 246)}║${c.reset}`);
|
|
2991
|
+
console.log(` ${rgb(139, 92, 246)}║${c.reset} ${c.white}?${c.reset} Show this help ${rgb(139, 92, 246)}║${c.reset}`);
|
|
2992
|
+
console.log(` ${rgb(139, 92, 246)}╚${c.reset}${c.dim}${'═'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╝${c.reset}`);
|
|
2825
2993
|
return;
|
|
2826
2994
|
}
|
|
2827
2995
|
});
|