dankgrinder 6.21.0 → 6.27.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 CHANGED
@@ -458,7 +458,7 @@ function renderDashboard() {
458
458
  lines.push(bRow(` ${c.bold}${titleGrad}${c.reset} ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset}`));
459
459
  }
460
460
 
461
- lines.push(bRow(` ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset}`));
461
+ lines.push(bRow(` ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset} ${Y}◷${c.reset} ${D}UP${c.reset} ${c.bold}${Y}${formatUptime()}${c.reset}`));
462
462
 
463
463
  // Subtitle info
464
464
  const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
@@ -487,52 +487,54 @@ function renderDashboard() {
487
487
  lines.push(bEmpty);
488
488
 
489
489
  // ═══════════════════════════════════════════════════════════════
490
- // STATS PANEL (split: left = metrics, right = big trend)
490
+ // STATS PANEL left: all metrics | right: big trend + rate
491
491
  // ═══════════════════════════════════════════════════════════════
492
492
  lines.push(bSep);
493
493
  lines.push(bEmpty);
494
494
 
495
- // Earnings sparkline data
496
495
  const now = Date.now();
497
496
  if (now - lastEarningsSample > 8000) { earningsHistory.push(totalCoins); lastEarningsSample = now; }
498
497
  const elapsedHrs = (Date.now() - startTime) / 3_600_000;
499
498
  const perHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
500
- const peakFlag = isNewHigh ? ` ${R}${c.bold}* NEW HIGH *${c.reset}` : '';
501
499
 
502
- // Left column: fixed metrics (left-aligned)
500
+ // ── Compute metric values ─────────────────────────────────────
503
501
  const cpmVal = globalCmdRate.getRate().toFixed(1);
504
502
  const srColor = successRate >= 95 ? G : successRate >= 80 ? Y : R;
505
- const srBarW = Math.min(15, Math.floor(iw * 0.1));
506
- const srBar = progressBar(successRate, 100, srBarW, successRate >= 95 ? [52, 211, 153] : successRate >= 80 ? [251, 191, 36] : [239, 68, 68]);
503
+ const srBar = progressBar(successRate, 100, 10, successRate >= 95 ? [52, 211, 153] : successRate >= 80 ? [251, 191, 36] : [239, 68, 68]);
507
504
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
508
505
  const memCol = memMB > 900 ? [239, 68, 68] : memMB > 600 ? [251, 191, 36] : [52, 211, 153];
509
- const memBarW = Math.min(15, Math.floor(iw * 0.1));
510
- const memBar = progressBar(memMB, 1024, memBarW, memCol, [40, 40, 55]);
506
+ const memBar = progressBar(memMB, 1024, 10, memCol, [40, 40, 55]);
507
+ const perHrColor = perHr >= 0 ? G : R;
508
+ const perHrSign = perHr >= 0 ? '+' : '';
509
+ const newHighFlag = isNewHigh ? ` ${R}${c.bold}★ NEW HIGH ★${c.reset}` : '';
511
510
 
512
- // Right column: big trend sparkline
513
- const sparkW = Math.floor(iw * 0.42);
511
+ // ── Big trend sparkline (takes ~40% of inner width) ─────────
512
+ const sparkW = Math.max(28, Math.floor(iw * 0.4));
514
513
  const spark = drawSparkline(earningsHistory.toArray(), sparkW);
514
+ const sparkLabel = `${A}~${c.reset} ${D}TREND${c.reset}`;
515
515
 
516
- // Build split rows
517
- const leftHalf = Math.floor(iw * 0.52);
518
-
519
- // Row 1: Balance + EARNED | TREND (big sparkline)
520
- const leftRow1 = `${Au}⟐${c.reset} ${D}BALANCE${c.reset} ${c.bold}${Au}⏣ ${formatCoins(totalBalance)}${c.reset} ${G}▲${c.reset} ${D}EARNED${c.reset} ${c.bold}${G}+⏣ ${formatCoins(totalCoins)}${c.reset}${peakFlag}`;
521
- const rightRow1 = `${A}~${c.reset} ${D}TREND${c.reset} ${spark}`;
522
- const combined1 = `${leftRow1}${' '.repeat(Math.max(2, leftHalf - leftRow1.replace(RE, '').length))}${rightRow1}`;
523
- lines.push(bRow(` ${combined1}`));
516
+ // ── Left metric rows (each left-aligned, ANSI-aware padding) ─
517
+ // Helper: ANSI-strip-aware pad strip ANSI then pad the visible content
518
+ const padRow = (content, totalVis) => {
519
+ const vis = content.replace(RE, '').length;
520
+ return content + ' '.repeat(Math.max(0, totalVis - vis));
521
+ };
524
522
 
525
- // Row 2: Peak (left)
526
- const peakRow = `${O}★${c.reset} ${D}PEAK${c.reset} ${c.bold}${O} ${formatCoins(sessionPeakCoins)}${c.reset}`;
527
- lines.push(bRow(` ${peakRow}${' '.repeat(Math.max(2, leftHalf - peakRow.replace(RE, '').length))}${Y}◷${c.reset} ${D}UP${c.reset} ${c.bold}${Y}${formatUptime()}${c.reset}`));
523
+ const leftTotal = iw - sparkW - 10; // reserve space for spark + gap
524
+ const lRow1 = `${Au}⟐${c.reset} ${D}BALANCE${c.reset} ${c.bold}${Au}⏣${c.reset} ${formatCoins(totalBalance)}`;
525
+ const lRow2 = `${G}▲${c.reset} ${D}EARNED${c.reset} ${c.bold}${G}${perHrSign}⏣${c.reset} ${formatCoins(totalCoins)}${newHighFlag}`;
526
+ const lRow3 = `${O}★${c.reset} ${D}PEAK${c.reset} ${c.bold}${O}⏣${c.reset} ${formatCoins(sessionPeakCoins)}`;
527
+ const lRow4 = `${B}◆${c.reset} ${D}CMDS${c.reset} ${c.bold}${B}${totalCommands}${c.reset} ${srColor}${successRate}%${c.reset} ${srBar} ${Cy}${cpmVal}${c.reset}${D}/min${c.reset}`;
528
+ const lRow5 = `${D}≡${c.reset} ${D}MEM${c.reset} ${rgb(memCol[0], memCol[1], memCol[2])}${c.bold}${memMB}MB${c.reset} ${memBar}`;
528
529
 
529
- // Row 3: CMDS (left) + success bar + rate
530
- const cmdsRow = `${B}◆${c.reset} ${D}CMDS${c.reset} ${c.bold}${B}${totalCommands}${c.reset} ${srColor}${successRate}%${c.reset} ${srBar} ${Cy}${cpmVal}${c.reset}${D}/min${c.reset}`;
531
- lines.push(bRow(` ${cmdsRow}`));
530
+ // Build right column label
531
+ const rRate = `${perHrColor}${perHrSign}⏣${c.reset} ${formatCoins(Math.abs(perHr))}/h`;
532
532
 
533
- // Row 4: MEM (left)
534
- const memRow = `${D}≡${c.reset} ${D}MEM${c.reset} ${rgb(memCol[0], memCol[1], memCol[2])}${c.bold}${memMB}MB${c.reset} ${memBar}`;
535
- lines.push(bRow(` ${memRow}`));
533
+ lines.push(bRow(` ${padRow(lRow1, leftTotal)} ${sparkLabel} ${spark}`));
534
+ lines.push(bRow(` ${padRow(lRow2, leftTotal)} ${D}────────${c.reset} ${c.dim}earned${c.reset}`));
535
+ lines.push(bRow(` ${padRow(lRow3, leftTotal)} ${D} ${c.reset} ${rRate}`));
536
+ lines.push(bRow(` ${padRow(lRow4, leftTotal)} ${D} ${c.reset}`));
537
+ lines.push(bRow(` ${padRow(lRow5, leftTotal)} ${D} ${c.reset}`));
536
538
 
537
539
  lines.push(bEmpty);
538
540
 
@@ -642,9 +644,9 @@ function renderDashboard() {
642
644
  else if (ls != null) lsStr = `${G}♥${ls}${c.reset}`;
643
645
  else lsStr = `${D}♥?${c.reset}`;
644
646
 
645
- // ── Level indicator ──
647
+ // ── Level indicator (fixed width so value changes don't jitter) ──
646
648
  const lvl = wk._level || 0;
647
- const lvlStr = lvl > 0 ? `${Cy}L${lvl}${c.reset}` : `${D}L?${c.reset}`;
649
+ const lvlStr = lvl > 0 ? `${Cy}L${String(lvl).padStart(3)}${c.reset}` : `${D}L???${c.reset}`;
648
650
 
649
651
  // ── Earned (fixed visible width) ──
650
652
  const earnNum = wk.stats.coins || 0;
@@ -1413,6 +1415,7 @@ class AccountWorker {
1413
1415
  startupProgress = null,
1414
1416
  requireComplete = false,
1415
1417
  maxAttempts = 1,
1418
+ silent = false,
1416
1419
  } = options;
1417
1420
  if (this._invRunning) return { ok: false, skipped: 'busy' };
1418
1421
  if (!force && this._lastInvCheck && Date.now() - this._lastInvCheck < 300_000) return { ok: false, skipped: 'recent' };
@@ -1426,7 +1429,7 @@ class AccountWorker {
1426
1429
  const baseLabel = startupProgress ? `[inv] ${startupProgress.current}/${startupProgress.total}` : '[inv]';
1427
1430
  const attemptLabel = tries > 1 ? ` [try ${attempt}/${tries}]` : '';
1428
1431
  const progressLine = `${baseLabel}${c.bold} ${this.username}${c.reset}${attemptLabel}`;
1429
- process.stdout.write(`\x1b[2K\r${progressLine}`);
1432
+ if (!silent) process.stdout.write(`\x1b[2K\r${progressLine}`);
1430
1433
 
1431
1434
  try {
1432
1435
  const result = await commands.runInventory({
@@ -1436,9 +1439,10 @@ class AccountWorker {
1436
1439
  accountId: this.account.id,
1437
1440
  redis,
1438
1441
  onPageProgress: ({ page, total }) => {
1439
- // Update progress on same line
1440
- const erase = '\x1b[2K\r';
1441
- process.stdout.write(`${erase}${baseLabel} ${c.bold}${this.username}${c.reset} · page ${page}/${total}${attemptLabel}`);
1442
+ if (!silent) {
1443
+ const erase = '\x1b[2K\r';
1444
+ process.stdout.write(`${erase}${baseLabel} ${c.bold}${this.username}${c.reset} · page ${page}/${total}${attemptLabel}`);
1445
+ }
1442
1446
  },
1443
1447
  });
1444
1448
 
@@ -1447,8 +1451,10 @@ class AccountWorker {
1447
1451
  }
1448
1452
 
1449
1453
  // Final result on same line
1450
- 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}`;
1451
- process.stdout.write(`\x1b[2K\r${resultLine}\n`);
1454
+ if (!silent) {
1455
+ 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}`;
1456
+ process.stdout.write(`\x1b[2K\r${resultLine}\n`);
1457
+ }
1452
1458
 
1453
1459
  // Extract lifesaver count from inventory and cache in Redis
1454
1460
  if (result.items && redis) {
@@ -1492,7 +1498,7 @@ class AccountWorker {
1492
1498
  if (attempt < tries) {
1493
1499
  const baseLabel = startupProgress ? `[inv] ${startupProgress.current}/${startupProgress.total}` : '[inv]';
1494
1500
  const retryLine = `${baseLabel} ${c.bold}${this.username}${c.reset}: ${c.yellow}attempt ${attempt}/${tries} failed${c.reset} — retrying...`;
1495
- process.stdout.write(`\x1b[2K\r${retryLine}\n`);
1501
+ if (!silent) process.stdout.write(`\x1b[2K\r${retryLine}\n`);
1496
1502
  await new Promise((r) => setTimeout(r, 1500 + Math.floor(Math.random() * 1500)));
1497
1503
  continue;
1498
1504
  }
@@ -1503,7 +1509,7 @@ class AccountWorker {
1503
1509
  } catch (e) {
1504
1510
  const baseLabel = startupProgress ? `[inv] ${startupProgress.current}/${startupProgress.total}` : '[inv]';
1505
1511
  const failLine = `${baseLabel} ${c.bold}${this.username}${c.reset}: ${c.red}failed${c.reset} — ${e.message}`;
1506
- process.stdout.write(`\x1b[2K\r${failLine}\n`);
1512
+ if (!silent) process.stdout.write(`\x1b[2K\r${failLine}\n`);
1507
1513
  return { ok: false, error: e.message };
1508
1514
  } finally {
1509
1515
  this._invRunning = false;
@@ -3014,102 +3020,90 @@ async function start(apiKey, apiUrl) {
3014
3020
  console.log(` ${checks.join(' ')}`);
3015
3021
  console.log('');
3016
3022
 
3017
- // ── Per-account inline login UI ──────────────────────────────
3018
- // Track login state per account for inline rendering
3023
+ // ── Phase 1: Login with per-account inline rendering ─────────────────────────
3024
+ const startupTw = process.stdout.columns || 90;
3025
+ const colNum = 4; // " #"
3026
+ const colSts = 3; // "ST"
3027
+ const colName = Math.min(24, Math.max(12, Math.floor(startupTw * 0.25)));
3028
+ const colGuild = Math.min(18, Math.max(8, Math.floor(startupTw * 0.2)));
3029
+ const colCmds = 8;
3030
+ const loginVis = colNum + colSts + colName + colGuild + colCmds + 10;
3031
+
3019
3032
  const loginStates = accounts.map((acc, i) => ({
3020
3033
  name: acc.label || acc.id || '?',
3021
3034
  done: false,
3022
3035
  failed: false,
3023
3036
  worker: null,
3024
- workerIdx: i,
3025
3037
  }));
3026
3038
 
3027
- // Column widths (visible chars)
3028
- const terminalW = process.stdout.columns || 90;
3029
- const colNum = 4; // " # "
3030
- const colSts = 3; // "ST "
3031
- const colName = Math.min(24, Math.max(12, Math.floor(terminalW * 0.25)));
3032
- const colGuild = Math.min(20, Math.max(8, Math.floor(terminalW * 0.2)));
3033
- const colCmds = 10; // " 20 cmds"
3034
- const totalVis = colNum + colSts + colName + colGuild + colCmds + 8;
3035
-
3036
- // Print header + all account lines (initial pending state)
3037
- console.log(` ${'─'.repeat(totalVis)}`);
3039
+ let loginLines = [];
3040
+ loginLines.push(` ${'─'.repeat(loginVis)}`);
3038
3041
  for (let i = 0; i < loginStates.length; i++) {
3039
3042
  const s = loginStates[i];
3040
- const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset} `;
3043
+ const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
3041
3044
  const name = s.name.substring(0, colName).padEnd(colName);
3042
- console.log(`${num}${c.dim}PN${c.reset} ${name} ${c.dim}${'···'.padEnd(colGuild)}${c.reset} ${c.dim}···${c.reset}`);
3045
+ const guild = c.dim + '···'.padEnd(colGuild) + c.reset;
3046
+ const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
3047
+ loginLines.push(` ${num} ${c.dim}··${c.reset} ${name} ${guild} ${cmds}`);
3043
3048
  }
3044
- console.log(` ${'─'.repeat(totalVis)}`);
3049
+ loginLines.push(` ${'─'.repeat(loginVis)}`);
3050
+ for (const l of loginLines) console.log(l);
3051
+ loginLines = null;
3045
3052
 
3046
- // Draw pending spinners
3053
+ let loginPending = new Array(accounts.length).fill(true);
3047
3054
  const drawLoginSpinners = () => {
3048
- for (let i = 0; i < loginStates.length; i++) {
3049
- const s = loginStates[i];
3050
- if (s.done || s.failed) continue;
3055
+ for (let i = 0; i < loginPending.length; i++) {
3056
+ if (!loginPending[i]) continue;
3051
3057
  const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3052
- const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset} `;
3053
- const name = s.name.substring(0, colName).padEnd(colName);
3054
- const guild = 'logging in...'.substring(0, colGuild).padEnd(colGuild);
3055
- process.stdout.write(`\x1b[${i + 2};0H\x1b[2K ${num}${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${c.dim}${guild}${c.reset} ${c.dim}···${c.reset}`);
3058
+ const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset}`;
3059
+ const name = loginStates[i].name.substring(0, colName).padEnd(colName);
3060
+ const guild = c.dim + 'logging in...'.substring(0, colGuild) + c.reset;
3061
+ const cmds = c.dim + '···'.padEnd(colCmds) + c.reset;
3062
+ process.stdout.write(`\r\x1b[2K ${num} ${rgb(139, 92, 246)}${spin}${c.reset} ${name} ${guild} ${cmds}\x1b[K`);
3056
3063
  }
3057
- process.stdout.write(`\x1b[${loginStates.length + 3};0H`);
3058
3064
  };
3059
3065
  const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
3060
3066
 
3061
- // Update a line when an account finishes
3062
3067
  const finalizeLoginLine = (idx, worker) => {
3068
+ if (!loginPending[idx]) return;
3069
+ loginPending[idx] = false;
3063
3070
  const s = loginStates[idx];
3064
- if (s.done || s.failed) return;
3065
3071
  s.done = true;
3066
3072
  s.worker = worker;
3067
3073
 
3068
- const num = `${c.dim}${(idx + 1).toString().padStart(colNum - 1)}${c.reset} `;
3074
+ const num = `${c.dim}${(idx + 1).toString().padStart(colNum - 1)}${c.reset}`;
3069
3075
  const name = (worker.username || s.name || '?').substring(0, colName).padEnd(colName);
3070
-
3071
3076
  let sts, guild, cmds;
3072
3077
  if (worker._tokenInvalid) {
3073
3078
  sts = `${rgb(239, 68, 68)}✗${c.reset}`;
3074
3079
  guild = 'INVALID'.padEnd(colGuild);
3075
- cmds = '···';
3080
+ cmds = '···'.padEnd(colCmds);
3076
3081
  s.failed = true;
3077
3082
  } else if (worker.channel) {
3078
3083
  sts = `${rgb(52, 211, 153)}✓${c.reset}`;
3079
3084
  const gn = (worker.channel.guild?.name || worker.channel.guild?.id || 'DM').substring(0, colGuild);
3080
3085
  guild = gn.padEnd(colGuild);
3081
- cmds = `${worker.stats?.commands || 0}cmds`;
3086
+ cmds = `${worker.stats?.commands || 0}`.padEnd(colCmds);
3082
3087
  } else {
3083
3088
  sts = `${rgb(251, 146, 60)}⏳${c.reset}`;
3084
3089
  guild = 'timeout'.padEnd(colGuild);
3085
- cmds = '···';
3090
+ cmds = '···'.padEnd(colCmds);
3086
3091
  }
3087
-
3088
- const line = ` ${num}${sts} ${name} ${guild} ${cmds}`;
3089
- process.stdout.write(`\x1b[${idx + 2};0H\x1b[2K${line}`);
3092
+ process.stdout.write(`\r\x1b[2K ${num} ${sts} ${name} ${c.dim}${guild}${c.reset} ${c.dim}${cmds}${c.reset}\x1b[K`);
3090
3093
  };
3091
3094
 
3092
- // Phase 1: Login in batches of 10
3093
3095
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
3094
3096
  const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
3095
3097
  const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
3096
- const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(LOGIN_GAP_MIN_MS, 150);
3097
-
3098
- const randomLoginGap = () => {
3099
- if (LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS) return LOGIN_GAP_MIN_MS;
3100
- return LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
3101
- };
3098
+ const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
3099
+ const randomLoginGap = () => LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS ? LOGIN_GAP_MIN_MS : LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
3102
3100
 
3103
3101
  const BATCH_SIZE = 10;
3104
3102
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
3105
3103
  if (shutdownCalled) break;
3106
3104
  const batch = accounts.slice(i, Math.min(i + BATCH_SIZE, accounts.length));
3107
-
3108
3105
  await Promise.all(batch.map(async (acc, idx) => {
3109
- if (idx > 0) {
3110
- const jitter = 100 + Math.floor(Math.random() * 500);
3111
- await new Promise(r => setTimeout(r, jitter));
3112
- }
3106
+ if (idx > 0) await new Promise(r => setTimeout(r, 100 + Math.floor(Math.random() * 500)));
3113
3107
  const worker = new AccountWorker(acc, i + idx);
3114
3108
  workers.push(worker);
3115
3109
  workerMap.set(acc.id, worker);
@@ -3117,216 +3111,143 @@ async function start(apiKey, apiUrl) {
3117
3111
  await worker.start();
3118
3112
  finalizeLoginLine(i + idx, worker);
3119
3113
  }));
3120
-
3121
- if (i + BATCH_SIZE < accounts.length) {
3122
- await new Promise(r => setTimeout(r, randomLoginGap()));
3123
- }
3124
-
3114
+ if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
3125
3115
  hintGC();
3126
3116
  }
3127
3117
 
3128
3118
  clearInterval(loginSpinnerInterval);
3129
- process.stdout.write(`\x1b[${loginStates.length + 3};0H`);
3130
-
3131
- // Final summary
3132
3119
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
3133
3120
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
3134
3121
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
3135
- console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Login complete${c.reset} ${rgb(52, 211, 153)}${loginDone}${c.reset}${c.dim}/${c.reset}${c.white}${accounts.length}${c.reset} ${c.dim}accounts connected${c.reset}`);
3122
+ console.log(`\r\x1b[2K ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Login complete${c.reset} ${rgb(52, 211, 153)}${loginDone}${c.reset}${c.dim}/${c.reset}${c.white}${accounts.length}${c.reset} ${c.dim}accounts connected${c.reset}`);
3136
3123
  console.log('');
3137
-
3138
3124
  if (invalidWorkers.length > 0) {
3139
3125
  log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
3140
- for (const w of invalidWorkers) {
3141
- log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
3142
- }
3126
+ for (const w of invalidWorkers) log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
3143
3127
  console.log('');
3144
3128
  }
3145
- if (timedOutWorkers.length > 0) {
3146
- log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
3147
- }
3129
+ if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
3148
3130
 
3149
- // Filter out workers with invalid tokens from grinding
3150
3131
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
3151
3132
 
3152
- // ── Phase 2: Inventory check with per-account inline rendering ─────────────────
3153
- const invStates = activeWorkers.map((w, i) => ({
3154
- name: w.username || w.account.label || '?',
3155
- idx: i,
3156
- done: false,
3157
- failed: false,
3158
- items: 0,
3159
- value: 0,
3160
- attempt: 0,
3161
- worker: w,
3162
- }));
3163
-
3164
- const tw2 = process.stdout.columns || 90;
3133
+ // ── Phase 2: Inventory check spinner for pending count, results inline ─────────
3165
3134
  const iColNum = 4;
3166
- const iColName = Math.min(22, Math.max(10, Math.floor(tw2 * 0.22)));
3135
+ const iColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
3167
3136
  const iColItems = 8;
3168
- const iColVal = 14;
3169
- const iColTries = 10;
3170
- const iTotalVis = iColNum + iColName + iColItems + iColVal + iColTries + 10;
3171
-
3172
- console.log(` ${'─'.repeat(iTotalVis)}`);
3173
- for (let i = 0; i < invStates.length; i++) {
3174
- const s = invStates[i];
3175
- const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset} `;
3176
- const name = s.name.substring(0, iColName).padEnd(iColName);
3177
- console.log(`${num}${c.dim}··${c.reset} ${name} ${c.dim}${'checking...'.padEnd(iColItems)}${c.reset} ${c.dim}${'··'.padEnd(iColVal)}${c.reset} ${c.dim}${''.padEnd(iColTries)}${c.reset}`);
3178
- }
3179
- console.log(` ${'─'.repeat(iTotalVis)}`);
3180
-
3181
- let invDone = 0, invFailed = 0;
3182
-
3183
- const drawInvSpinners = () => {
3184
- for (let i = 0; i < invStates.length; i++) {
3185
- const s = invStates[i];
3186
- if (s.done || s.failed) continue;
3187
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3188
- const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset} `;
3189
- const name = s.name.substring(0, iColName).padEnd(iColName);
3190
- const items = 'checking'.substring(0, iColItems).padEnd(iColItems);
3191
- const val = ''.padEnd(iColVal);
3192
- const tries = s.attempt > 0 ? `try ${s.attempt}/3` : '';
3193
- process.stdout.write(`\x1b[${i + 2};0H\x1b[2K ${num}${rgb(34, 211, 238)}${spin}${c.reset} ${name} ${c.dim}${items}${c.reset} ${c.dim}${val}${c.reset} ${c.dim}${tries}${c.reset}`);
3194
- }
3195
- process.stdout.write(`\x1b[${invStates.length + 3};0H`);
3196
- };
3197
- const invSpinnerInterval = setInterval(drawInvSpinners, 80);
3198
-
3199
- const finalizeInvLine = (idx, invRes) => {
3200
- const s = invStates[idx];
3201
- if (s.done || s.failed) return;
3202
- if (invRes?.ok) {
3203
- s.done = true;
3204
- invDone++;
3205
- } else {
3206
- s.failed = true;
3207
- invFailed++;
3208
- }
3209
-
3210
- const num = `${c.dim}${(idx + 1).toString().padStart(iColNum - 1)}${c.reset} `;
3211
- const name = s.name.substring(0, iColName).padEnd(iColName);
3212
- const items = `${invRes?.items?.length || 0}`.padEnd(iColItems);
3213
- const val = invRes?.ok
3214
- ? `${c.green}⏣${(invRes.totalValue || 0).toLocaleString()}${c.reset}`.padEnd(iColVal + 3)
3215
- : `${c.dim}··${c.reset}`.padEnd(iColVal);
3216
- const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
3217
-
3218
- const line = ` ${num}${sts} ${name} ${items} ${val}`;
3219
- process.stdout.write(`\x1b[${idx + 2};0H\x1b[2K${line}`);
3137
+ const iColVal = 16;
3138
+ const invVis = 7 + iColNum + iColName + iColItems + iColVal + 12;
3139
+
3140
+ console.log(` ${'─'.repeat(invVis)}`);
3141
+ for (let i = 0; i < activeWorkers.length; i++) {
3142
+ const w = activeWorkers[i];
3143
+ const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
3144
+ const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
3145
+ console.log(` ${num} ${c.dim}··${c.reset} ${name} ${c.dim}${'checking...'.padEnd(iColItems)}${c.reset} ${c.dim}${'···'.padEnd(iColVal)}${c.reset}`);
3146
+ }
3147
+ console.log(` ${'─'.repeat(invVis)}`);
3148
+
3149
+ let invDone = 0, invFailed = 0, invPending = activeWorkers.length;
3150
+ const drawInvProgress = () => {
3151
+ if (invPending === 0) return;
3152
+ const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3153
+ const pct = activeWorkers.length > 0 ? ((activeWorkers.length - invPending) / activeWorkers.length) : 0;
3154
+ const barW = Math.min(20, startupTw - 40);
3155
+ const filled = Math.round(pct * barW);
3156
+ const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3157
+ const pctStr = `${Math.round(pct * 100)}%`;
3158
+ process.stdout.write(`\r\x1b[2K ${rgb(34, 211, 238)}${spin}${c.reset} ${c.dim}Inventory...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${activeWorkers.length - invPending}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} ${c.dim}${pctStr}${c.reset} `);
3220
3159
  };
3160
+ const invSpinnerInterval = setInterval(drawInvProgress, 80);
3221
3161
 
3222
3162
  await Promise.all(activeWorkers.map(async (w, i) => {
3223
- try {
3224
- const invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3 });
3225
- finalizeInvLine(i, invRes);
3226
- } catch {
3227
- finalizeInvLine(i, { ok: false });
3228
- }
3163
+ const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset}`;
3164
+ const name = (w.username || w.account.label || '?').substring(0, iColName).padEnd(iColName);
3165
+ let invRes;
3166
+ try { invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
3167
+ catch { invRes = { ok: false }; }
3168
+ invPending--;
3169
+ const items = invRes?.ok ? (invRes.result?.items?.length || 0) : 0;
3170
+ const val = invRes?.ok ? (invRes.result?.totalValue || 0) : 0;
3171
+ const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
3172
+ const itemStr = `${items}`.padEnd(iColItems);
3173
+ const valStr = invRes?.ok ? `${c.green}⏣${val.toLocaleString()}${c.reset}` : `${c.dim}···${c.reset}`;
3174
+ process.stdout.write(`\r\x1b[2K ${num} ${sts} ${name} ${itemStr} ${valStr.padEnd(iColVal + 5)}\x1b[K`);
3175
+ if (invRes?.ok) invDone++; else invFailed++;
3229
3176
  }));
3230
3177
 
3231
3178
  clearInterval(invSpinnerInterval);
3232
- process.stdout.write(`\x1b[${invStates.length + 3};0H`);
3179
+ process.stdout.write(`\r\x1b[2K`);
3233
3180
 
3234
3181
  if (invFailed > 0) {
3235
3182
  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}${activeWorkers.length}${c.reset} ${c.dim}done, ${rgb(239, 68, 68)}${invFailed} failed${c.reset}`);
3236
3183
  log('error', `${c.red}Not starting grind loops — ${invFailed} accounts failed inventory.${c.reset}`);
3237
3184
  return;
3238
3185
  }
3239
-
3240
3186
  console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Inventory complete${c.reset} ${rgb(52, 211, 153)}${invDone}/${activeWorkers.length}${c.reset} ${c.dim}all clear${c.reset}`);
3241
3187
  console.log('');
3242
3188
 
3243
- // ── Phase 2.5: Balance check with inline per-account rendering ─────────────────
3244
- const balStates = activeWorkers.map((w, i) => ({
3245
- name: w.username || w.account.label || '?',
3246
- idx: i,
3247
- done: false,
3248
- wallet: 0,
3249
- bank: 0,
3250
- worker: w,
3251
- }));
3252
-
3189
+ // ── Phase 2.5: Balance check inline table, single spinner for progress ─────────
3253
3190
  const bColNum = 4;
3254
- const bColName = Math.min(22, Math.max(10, Math.floor(tw2 * 0.22)));
3255
- const bColWallet = 14;
3256
- const bColBank = 14;
3191
+ const bColName = Math.min(22, Math.max(12, Math.floor(startupTw * 0.22)));
3192
+ const bColWallet = 12;
3193
+ const bColBank = 12;
3257
3194
  const bColTotal = 14;
3258
3195
  const bColLs = 4;
3259
- const bTotalVis = bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 12;
3260
-
3261
- console.log(` ${'─'.repeat(bTotalVis)}`);
3262
- for (let i = 0; i < balStates.length; i++) {
3263
- const s = balStates[i];
3264
- const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset} `;
3265
- const name = s.name.substring(0, bColName).padEnd(bColName);
3266
- console.log(`${num}${c.dim}··${c.reset} ${name} ${c.dim}${'checking'.padEnd(bColWallet)}${c.reset} ${c.dim}${'··'.padEnd(bColBank)}${c.reset} ${c.dim}${'··'.padEnd(bColTotal)}${c.reset} ${c.dim}♥?${c.reset}`);
3267
- }
3268
- console.log(` ${'─'.repeat(bTotalVis)}`);
3269
-
3270
- let balDone = 0;
3271
-
3272
- const drawBalSpinners = () => {
3273
- for (let i = 0; i < balStates.length; i++) {
3274
- const s = balStates[i];
3275
- if (s.done) continue;
3276
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3277
- const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset} `;
3278
- const name = s.name.substring(0, bColName).padEnd(bColName);
3279
- process.stdout.write(`\x1b[${i + 2};0H\x1b[2K ${num}${rgb(251, 191, 36)}${spin}${c.reset} ${name} ${c.dim}${'checking'.padEnd(bColWallet)}${c.reset} ${c.dim}${'··'.padEnd(bColBank)}${c.reset} ${c.dim}${'··'.padEnd(bColTotal)}${c.reset} ${c.dim}♥?${c.reset}`);
3280
- }
3281
- process.stdout.write(`\x1b[${balStates.length + 3};0H`);
3196
+ const balVis = 7 + bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 14;
3197
+
3198
+ console.log(` ${'─'.repeat(balVis)}`);
3199
+ for (let i = 0; i < activeWorkers.length; i++) {
3200
+ const w = activeWorkers[i];
3201
+ const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
3202
+ const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
3203
+ console.log(` ${num} ${c.dim}··${c.reset} ${name} ${c.dim}${'checking'.padEnd(bColWallet)}${c.reset} ${c.dim}${'···'.padEnd(bColBank)}${c.reset} ${c.dim}${'···'.padEnd(bColTotal)}${c.reset} ${c.dim}♥?${c.reset}`);
3204
+ }
3205
+ console.log(` ${'─'.repeat(balVis)}`);
3206
+
3207
+ let balDone = 0, balPending = activeWorkers.length;
3208
+ const drawBalProgress = () => {
3209
+ if (balPending === 0) return;
3210
+ const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3211
+ const pct = activeWorkers.length > 0 ? ((activeWorkers.length - balPending) / activeWorkers.length) : 0;
3212
+ const barW = Math.min(20, startupTw - 40);
3213
+ const filled = Math.round(pct * barW);
3214
+ const bar = rgb(251, 191, 36) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(barW - filled) + c.reset;
3215
+ process.stdout.write(`\r\x1b[2K ${rgb(251, 191, 36)}${spin}${c.reset} ${c.dim}Balance...${c.reset} ${bar} ${c.bold}${rgb(52, 211, 153)}${activeWorkers.length - balPending}${c.reset}${c.dim}/${c.reset}${c.white}${activeWorkers.length}${c.reset} `);
3282
3216
  };
3283
- const balSpinnerInterval = setInterval(drawBalSpinners, 80);
3284
-
3285
- const finalizeBalLine = (idx, w) => {
3286
- const s = balStates[idx];
3287
- if (s.done) return;
3288
- s.done = true;
3289
- s.wallet = w.stats?.balance || 0;
3290
- s.bank = w.stats?.bankBalance || 0;
3291
- balDone++;
3217
+ const balSpinnerInterval = setInterval(drawBalProgress, 80);
3292
3218
 
3293
- const num = `${c.dim}${(idx + 1).toString().padStart(bColNum - 1)}${c.reset} `;
3294
- const name = (w.username || s.name || '?').substring(0, bColName).padEnd(bColName);
3219
+ await Promise.all(activeWorkers.map(async (w, i) => {
3220
+ try { await w.checkBalance(true); } catch {}
3221
+ balPending--;
3222
+ const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset}`;
3223
+ const name = (w.username || w.account.label || '?').substring(0, bColName).padEnd(bColName);
3224
+ const wallet = w.stats?.balance || 0;
3225
+ const bank = w.stats?.bankBalance || 0;
3295
3226
  const ls = w._lifesavers ?? '?';
3296
3227
  const lsColor = ls === 0 ? rgb(239, 68, 68) : ls <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3297
- const wallet = `${c.green}⏣${(s.wallet).toLocaleString()}${c.reset}`.padEnd(bColWallet + 3);
3298
- const bank = `${c.cyan}⏣${(s.bank).toLocaleString()}${c.reset}`.padEnd(bColBank + 3);
3299
- const total = `${c.bold}⏣${(s.wallet + s.bank).toLocaleString()}${c.reset}`.padEnd(bColTotal + 3);
3300
-
3301
- const line = ` ${num}${rgb(52, 211, 153)}✓${c.reset} ${name} ${wallet} ${bank} ${total} ${lsColor}♥${ls}${c.reset}`;
3302
- process.stdout.write(`\x1b[${idx + 2};0H\x1b[2K${line}`);
3303
- };
3304
-
3305
- await Promise.all(activeWorkers.map(async w => {
3306
- try {
3307
- await w.checkBalance(true); // silent: don't spam console during inline rendering
3308
- } catch {}
3309
- const idx = balStates.findIndex(s => s.worker === w);
3310
- if (idx >= 0) finalizeBalLine(idx, w);
3228
+ const walletStr = `${c.green}⏣${wallet.toLocaleString()}${c.reset}`;
3229
+ const bankStr = `${c.cyan}⏣${bank.toLocaleString()}${c.reset}`;
3230
+ const totalStr = `${c.bold}⏣${(wallet + bank).toLocaleString()}${c.reset}`;
3231
+ process.stdout.write(`\r\x1b[2K ${num} ${rgb(52, 211, 153)}✓${c.reset} ${name} ${walletStr.padEnd(bColWallet + 4)} ${bankStr.padEnd(bColBank + 4)} ${totalStr.padEnd(bColTotal + 3)} ${lsColor}♥${ls}${c.reset}\x1b[K`);
3232
+ balDone++;
3311
3233
  }));
3312
3234
 
3313
3235
  clearInterval(balSpinnerInterval);
3314
- process.stdout.write(`\x1b[${balStates.length + 3};0H`);
3236
+ process.stdout.write(`\r\x1b[2K`);
3315
3237
 
3316
- // Balance summary
3317
3238
  let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
3318
- for (const s of balStates) {
3319
- totalWallet += s.wallet;
3320
- totalBank += s.bank;
3321
- if (s.worker._lifesavers === 0) noLifesaverAccounts.push(s.name);
3239
+ for (const w of activeWorkers) {
3240
+ totalWallet += w.stats?.balance || 0;
3241
+ totalBank += w.stats?.bankBalance || 0;
3242
+ if (w._lifesavers === 0) noLifesaverAccounts.push(w.username || w.account.label);
3322
3243
  }
3323
3244
  console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Balance${c.reset} Total: ${c.bold}${c.green}⏣ ${(totalWallet + totalBank).toLocaleString()}${c.reset} ${c.dim}(wallet: ⏣ ${totalWallet.toLocaleString()} + bank: ⏣ ${totalBank.toLocaleString()})${c.reset}`);
3324
-
3325
3245
  if (noLifesaverAccounts.length > 0) {
3326
3246
  console.log(` ${rgb(239, 68, 68)}⚠${c.reset} ${c.bold}${c.red}WARNING: ${noLifesaverAccounts.length} account(s) have 0 LIFESAVERS!${c.reset} Crime/Search disabled for: ${noLifesaverAccounts.join(', ')}`);
3327
3247
  }
3328
3248
  console.log('');
3329
3249
 
3250
+
3330
3251
  // Phase 2.75: Check DM history for deaths/level-ups (sequential, fast)
3331
3252
  console.log(` ${rgb(139, 92, 246)}${BRAILLE_SPIN[0]}${c.reset} ${c.dim}Checking DM history...${c.reset}`);
3332
3253
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.21.0",
3
+ "version": "6.27.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"