dankgrinder 6.1.0 → 6.5.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 +449 -233
- 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;
|
|
@@ -2400,22 +2466,37 @@ class AccountWorker {
|
|
|
2400
2466
|
if (!this.account.discord_token) { this.log('error', 'No token'); return; }
|
|
2401
2467
|
if (!this.account.channel_id) { this.log('error', 'No channel'); return; }
|
|
2402
2468
|
|
|
2469
|
+
const LOGIN_TIMEOUT_MS = 30_000; // 30s max per account login
|
|
2470
|
+
|
|
2403
2471
|
return new Promise((resolve) => {
|
|
2472
|
+
let resolved = false;
|
|
2473
|
+
const done = () => { if (!resolved) { resolved = true; resolve(); } };
|
|
2474
|
+
|
|
2475
|
+
// Timeout guard — don't let a single account hang the batch
|
|
2476
|
+
const timeoutId = setTimeout(() => {
|
|
2477
|
+
if (!resolved) {
|
|
2478
|
+
this.log('warn', 'Login timed out after 30s — will retry in background');
|
|
2479
|
+
done();
|
|
2480
|
+
// Retry login in background after timeout
|
|
2481
|
+
this._retryLoginBackground();
|
|
2482
|
+
}
|
|
2483
|
+
}, LOGIN_TIMEOUT_MS);
|
|
2484
|
+
|
|
2404
2485
|
this.client.on('ready', async () => {
|
|
2486
|
+
clearTimeout(timeoutId);
|
|
2405
2487
|
this.username = this.client.user.tag || this.username;
|
|
2406
2488
|
this.avatarUrl = this.client.user.displayAvatarURL?.({ format: 'png', dynamic: true, size: 128 }) || null;
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
} catch { /* silent */ }
|
|
2489
|
+
// Report status non-blocking
|
|
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, discord_username: this.username, avatar_url: this.avatarUrl }),
|
|
2494
|
+
}).catch(() => {});
|
|
2414
2495
|
|
|
2415
2496
|
this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
|
|
2416
2497
|
if (!this.channel) {
|
|
2417
2498
|
this.log('error', `Channel not found`);
|
|
2418
|
-
|
|
2499
|
+
done(); return;
|
|
2419
2500
|
}
|
|
2420
2501
|
|
|
2421
2502
|
const enabledCmds = [
|
|
@@ -2438,49 +2519,108 @@ class AccountWorker {
|
|
|
2438
2519
|
this.log('success', `#${chName} · ${enabledCmds.length} cmds`);
|
|
2439
2520
|
this.setStatus('starting...');
|
|
2440
2521
|
|
|
2441
|
-
// Load daily/weekly/monthly done state from Redis
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2522
|
+
// Load daily/weekly/monthly done state from Redis (non-blocking for login speed)
|
|
2523
|
+
this._loadRedisState().catch(() => {});
|
|
2524
|
+
|
|
2525
|
+
// Reduced settle time — 200ms is enough for most gateways
|
|
2526
|
+
await new Promise(r => setTimeout(r, 200));
|
|
2527
|
+
done();
|
|
2528
|
+
});
|
|
2529
|
+
|
|
2530
|
+
// Handle login errors so they don't hang
|
|
2531
|
+
this.client.on('error', (err) => {
|
|
2532
|
+
if (!resolved) {
|
|
2533
|
+
clearTimeout(timeoutId);
|
|
2534
|
+
const msg = (err?.message || '').toLowerCase();
|
|
2535
|
+
if (msg.includes('token_invalid') || msg.includes('invalid token') || msg.includes('401')) {
|
|
2536
|
+
this.log('error', `✗ TOKEN INVALID — this token is no longer valid`);
|
|
2537
|
+
this.paused = true;
|
|
2538
|
+
this._tokenInvalid = true;
|
|
2539
|
+
} else {
|
|
2540
|
+
this.log('error', `Login error: ${err?.message || err}`);
|
|
2454
2541
|
}
|
|
2455
|
-
|
|
2456
|
-
try {
|
|
2457
|
-
const balData = await redis.get(`dkg:bal:${this.account.id}`);
|
|
2458
|
-
if (balData) {
|
|
2459
|
-
const { wallet, bank } = JSON.parse(balData);
|
|
2460
|
-
if (wallet > 0 || bank > 0) {
|
|
2461
|
-
this.stats.balance = wallet;
|
|
2462
|
-
this.stats.bankBalance = bank;
|
|
2463
|
-
await fetch(`${API_URL}/api/grinder/status`, {
|
|
2464
|
-
method: 'POST',
|
|
2465
|
-
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2466
|
-
body: JSON.stringify({ account_id: this.account.id, balance: wallet, bank_balance: bank, total_balance: wallet + bank }),
|
|
2467
|
-
}).catch(() => {});
|
|
2468
|
-
}
|
|
2469
|
-
}
|
|
2470
|
-
} catch {}
|
|
2542
|
+
done();
|
|
2471
2543
|
}
|
|
2472
|
-
|
|
2473
|
-
// Let Discord gateway settle (reduced for faster startup)
|
|
2474
|
-
await new Promise(r => setTimeout(r, 500));
|
|
2475
|
-
resolve();
|
|
2476
2544
|
});
|
|
2477
2545
|
|
|
2478
2546
|
// Attach auto-recovery event listeners before login
|
|
2479
2547
|
this._attachRecoveryListeners();
|
|
2480
|
-
this.client.login(this.account.discord_token)
|
|
2548
|
+
this.client.login(this.account.discord_token).catch((err) => {
|
|
2549
|
+
clearTimeout(timeoutId);
|
|
2550
|
+
const msg = (err?.message || '').toLowerCase();
|
|
2551
|
+
if (msg.includes('token_invalid') || msg.includes('invalid token') || msg.includes('401') || msg.includes('incorrect login')) {
|
|
2552
|
+
this.log('error', `✗ TOKEN INVALID — check this account's token`);
|
|
2553
|
+
this.paused = true;
|
|
2554
|
+
this._tokenInvalid = true;
|
|
2555
|
+
// Report invalid status to API
|
|
2556
|
+
fetch(`${API_URL}/api/grinder/status`, {
|
|
2557
|
+
method: 'POST',
|
|
2558
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2559
|
+
body: JSON.stringify({ account_id: this.account.id, active: false, status: 'token_invalid' }),
|
|
2560
|
+
}).catch(() => {});
|
|
2561
|
+
sendWebhook('Token Invalid', `**${this.account.label || this.account.id}** has an invalid Discord token.`, 0xef4444);
|
|
2562
|
+
} else {
|
|
2563
|
+
this.log('error', `Login failed: ${err?.message || 'unknown error'}`);
|
|
2564
|
+
}
|
|
2565
|
+
done();
|
|
2566
|
+
});
|
|
2481
2567
|
});
|
|
2482
2568
|
}
|
|
2483
2569
|
|
|
2570
|
+
/** Load Redis cached state in background (non-blocking) */
|
|
2571
|
+
async _loadRedisState() {
|
|
2572
|
+
if (!redis) return;
|
|
2573
|
+
for (const cmd of ['daily', 'weekly', 'monthly', 'drops']) {
|
|
2574
|
+
try {
|
|
2575
|
+
const val = await redis.get(`dkg:done:${this.account.id}:${cmd}`);
|
|
2576
|
+
if (val) {
|
|
2577
|
+
const ttlSec = await redis.ttl(`dkg:done:${this.account.id}:${cmd}`);
|
|
2578
|
+
if (ttlSec > 0) {
|
|
2579
|
+
this.doneToday.set(cmd, Date.now() + ttlSec * 1000);
|
|
2580
|
+
this.log('info', `${cmd} already claimed (${Math.round(ttlSec / 3600)}h remaining)`);
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
} catch {}
|
|
2584
|
+
}
|
|
2585
|
+
// Load cached balance from Redis
|
|
2586
|
+
try {
|
|
2587
|
+
const balData = await redis.get(`dkg:bal:${this.account.id}`);
|
|
2588
|
+
if (balData) {
|
|
2589
|
+
const { wallet, bank } = JSON.parse(balData);
|
|
2590
|
+
if (wallet > 0 || bank > 0) {
|
|
2591
|
+
this.stats.balance = wallet;
|
|
2592
|
+
this.stats.bankBalance = bank;
|
|
2593
|
+
await fetch(`${API_URL}/api/grinder/status`, {
|
|
2594
|
+
method: 'POST',
|
|
2595
|
+
headers: { Authorization: `Bearer ${API_KEY}`, 'Content-Type': 'application/json' },
|
|
2596
|
+
body: JSON.stringify({ account_id: this.account.id, balance: wallet, bank_balance: bank, total_balance: wallet + bank }),
|
|
2597
|
+
}).catch(() => {});
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
} catch {}
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
/** Retry login in background after a timeout */
|
|
2604
|
+
async _retryLoginBackground() {
|
|
2605
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
2606
|
+
if (this.client && !this.running) {
|
|
2607
|
+
this.log('info', 'Retrying login in background...');
|
|
2608
|
+
try {
|
|
2609
|
+
this.client.destroy();
|
|
2610
|
+
this.client = createLeanClient();
|
|
2611
|
+
this._attachRecoveryListeners();
|
|
2612
|
+
await this.client.login(this.account.discord_token);
|
|
2613
|
+
this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
|
|
2614
|
+
if (this.channel) {
|
|
2615
|
+
this.username = this.client.user?.tag || this.username;
|
|
2616
|
+
this.log('success', `Background login OK`);
|
|
2617
|
+
}
|
|
2618
|
+
} catch (err) {
|
|
2619
|
+
this.log('error', `Background login failed: ${err?.message || err}`);
|
|
2620
|
+
}
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2484
2624
|
stop() {
|
|
2485
2625
|
this.running = false;
|
|
2486
2626
|
this.paused = false;
|
|
@@ -2596,6 +2736,22 @@ async function start(apiKey, apiUrl) {
|
|
|
2596
2736
|
console.log(` ${checks.join(' ')}`);
|
|
2597
2737
|
console.log('');
|
|
2598
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} ${dim}Logging in...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${loginDone}${c.reset}${dim}/${c.reset}${c.white}${accounts.length}${c.reset} ${dim}${pctStr}${c.reset} `);
|
|
2750
|
+
};
|
|
2751
|
+
|
|
2752
|
+
// Progress animation timer
|
|
2753
|
+
const progressInterval = setInterval(drawLoginProgress, 80);
|
|
2754
|
+
|
|
2599
2755
|
// Phase 1: Login all accounts (optimized for speed)
|
|
2600
2756
|
const LOGIN_PROGRESS_EVERY = 10;
|
|
2601
2757
|
// Reduced delays: 50-150ms between logins (faster startup for 1k+ accounts)
|
|
@@ -2610,38 +2766,80 @@ async function start(apiKey, apiUrl) {
|
|
|
2610
2766
|
};
|
|
2611
2767
|
|
|
2612
2768
|
// Parallel login in batches of 10 to avoid rate limits while being fast
|
|
2769
|
+
// Within each batch, stagger logins by 100-600ms to avoid gateway flood
|
|
2613
2770
|
const BATCH_SIZE = 10;
|
|
2614
2771
|
for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
|
|
2615
2772
|
if (shutdownCalled) break;
|
|
2616
2773
|
const batch = accounts.slice(i, Math.min(i + BATCH_SIZE, accounts.length));
|
|
2617
2774
|
|
|
2618
|
-
//
|
|
2775
|
+
// Staggered parallel login: fire each login with a small jitter delay
|
|
2619
2776
|
await Promise.all(batch.map(async (acc, idx) => {
|
|
2777
|
+
// Stagger within batch: 0ms for first, 100-600ms for subsequent
|
|
2778
|
+
if (idx > 0) {
|
|
2779
|
+
const jitter = 100 + Math.floor(Math.random() * 500);
|
|
2780
|
+
await new Promise(r => setTimeout(r, jitter));
|
|
2781
|
+
}
|
|
2620
2782
|
const worker = new AccountWorker(acc, i + idx);
|
|
2621
2783
|
workers.push(worker);
|
|
2622
2784
|
workerMap.set(acc.id, worker);
|
|
2623
2785
|
await worker.start();
|
|
2786
|
+
loginDone++;
|
|
2624
2787
|
}));
|
|
2625
2788
|
|
|
2626
2789
|
// Small gap between batches
|
|
2627
2790
|
if (i + BATCH_SIZE < accounts.length) {
|
|
2628
2791
|
const gapMs = randomLoginGap();
|
|
2629
|
-
log('info', `${c.dim}Logged in ${Math.min(i + BATCH_SIZE, accounts.length)}/${accounts.length}...${c.reset}`);
|
|
2630
2792
|
await new Promise(r => setTimeout(r, gapMs));
|
|
2631
2793
|
}
|
|
2632
2794
|
|
|
2633
2795
|
hintGC();
|
|
2634
2796
|
}
|
|
2635
2797
|
|
|
2636
|
-
|
|
2637
|
-
|
|
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} ${dim}accounts connected${c.reset}`);
|
|
2802
|
+
console.log('');
|
|
2803
|
+
|
|
2804
|
+
// Login summary: show invalid tokens clearly
|
|
2805
|
+
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
2806
|
+
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
2807
|
+
if (invalidWorkers.length > 0) {
|
|
2808
|
+
console.log('');
|
|
2809
|
+
log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
|
|
2810
|
+
for (const w of invalidWorkers) {
|
|
2811
|
+
log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
|
|
2812
|
+
}
|
|
2813
|
+
console.log('');
|
|
2814
|
+
}
|
|
2815
|
+
if (timedOutWorkers.length > 0) {
|
|
2816
|
+
log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
// Filter out workers with invalid tokens from grinding
|
|
2820
|
+
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
2638
2821
|
|
|
2639
|
-
//
|
|
2822
|
+
// Phase 2: Run inventory on ALL valid accounts in parallel (must complete before grinding)
|
|
2823
|
+
console.log(` ${rgb(139, 92, 246)}${BRAILLE_SPIN[0]}${c.reset} ${dim}Checking inventory for ${c.reset}${c.bold}${activeWorkers.length}${c.reset}${dim} accounts...${c.reset}`);
|
|
2824
|
+
|
|
2825
|
+
// Animated inventory progress
|
|
2640
2826
|
let invDone = 0;
|
|
2641
2827
|
let invFailed = 0;
|
|
2642
|
-
const total =
|
|
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} ${dim}Inventory...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${invDone}${c.reset}${dim}/${c.reset}${c.white}${total}${c.reset} ${dim}${Math.round(pct * 100)}%${c.reset} `);
|
|
2838
|
+
};
|
|
2643
2839
|
|
|
2644
|
-
|
|
2840
|
+
const invProgressInterval = setInterval(drawInvProgress, 80);
|
|
2841
|
+
|
|
2842
|
+
await Promise.all(activeWorkers.map(async (w, i) => {
|
|
2645
2843
|
try {
|
|
2646
2844
|
const invRes = await w.checkInventory({
|
|
2647
2845
|
force: true,
|
|
@@ -2656,19 +2854,23 @@ async function start(apiKey, apiUrl) {
|
|
|
2656
2854
|
}
|
|
2657
2855
|
}));
|
|
2658
2856
|
|
|
2659
|
-
|
|
2660
|
-
|
|
2857
|
+
clearInterval(invProgressInterval);
|
|
2858
|
+
process.stdout.write(`\r${c.clearLine}`);
|
|
2661
2859
|
|
|
2860
|
+
// Final summary
|
|
2662
2861
|
if (invFailed > 0) {
|
|
2663
|
-
log(
|
|
2862
|
+
console.log(` ${rgb(239, 68, 68)}✗${c.reset} ${c.bold}Inventory incomplete${c.reset} ${rgb(52, 211, 153)}${invDone}${c.reset}${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}`);
|
|
2664
2864
|
return;
|
|
2665
2865
|
}
|
|
2666
2866
|
|
|
2667
|
-
|
|
2668
|
-
log('
|
|
2867
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Inventory complete${c.reset} ${rgb(52, 211, 153)}${invDone}/${total}${c.reset} ${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('');
|
|
2669
2871
|
|
|
2670
|
-
// Phase 3: Start all grind loops
|
|
2671
|
-
for (const w of
|
|
2872
|
+
// Phase 3: Start all grind loops (only for valid workers)
|
|
2873
|
+
for (const w of activeWorkers) {
|
|
2672
2874
|
if (!shutdownCalled) w.grindLoop();
|
|
2673
2875
|
}
|
|
2674
2876
|
|
|
@@ -2682,6 +2884,13 @@ async function start(apiKey, apiUrl) {
|
|
|
2682
2884
|
process.stdout.write(c.hide);
|
|
2683
2885
|
dashboardLines = 0;
|
|
2684
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
|
+
|
|
2685
2894
|
setInterval(() => scheduleRender(), 1000);
|
|
2686
2895
|
scheduleRender();
|
|
2687
2896
|
|
|
@@ -2830,10 +3039,17 @@ function setupKeyboardShortcuts() {
|
|
|
2830
3039
|
process.stdin.resume();
|
|
2831
3040
|
process.stdin.setEncoding('utf8');
|
|
2832
3041
|
|
|
2833
|
-
//
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
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('');
|
|
2837
3053
|
|
|
2838
3054
|
process.stdin.on('data', (key) => {
|
|
2839
3055
|
const k = key.toString().toLowerCase();
|