dankgrinder 6.25.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
@@ -487,7 +487,7 @@ function renderDashboard() {
487
487
  lines.push(bEmpty);
488
488
 
489
489
  // ═══════════════════════════════════════════════════════════════
490
- // STATS PANEL — left: metrics (fixed labels) | 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);
@@ -497,7 +497,7 @@ function renderDashboard() {
497
497
  const elapsedHrs = (Date.now() - startTime) / 3_600_000;
498
498
  const perHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
499
499
 
500
- // ── Compute all metric values ──────────────────────────────────
500
+ // ── Compute metric values ─────────────────────────────────────
501
501
  const cpmVal = globalCmdRate.getRate().toFixed(1);
502
502
  const srColor = successRate >= 95 ? G : successRate >= 80 ? Y : R;
503
503
  const srBar = progressBar(successRate, 100, 10, successRate >= 95 ? [52, 211, 153] : successRate >= 80 ? [251, 191, 36] : [239, 68, 68]);
@@ -508,41 +508,33 @@ function renderDashboard() {
508
508
  const perHrSign = perHr >= 0 ? '+' : '';
509
509
  const newHighFlag = isNewHigh ? ` ${R}${c.bold}★ NEW HIGH ★${c.reset}` : '';
510
510
 
511
- // ── Big trend sparkline (right column) ──────────────────────
512
- const sparkW = Math.max(36, Math.floor(iw * 0.55));
511
+ // ── Big trend sparkline (takes ~40% of inner width) ─────────
512
+ const sparkW = Math.max(28, Math.floor(iw * 0.4));
513
513
  const spark = drawSparkline(earningsHistory.toArray(), sparkW);
514
+ const sparkLabel = `${A}~${c.reset} ${D}TREND${c.reset}`;
514
515
 
515
- // ── Build each row: left metrics (fixed) | right trend ───────
516
- const leftW = 36; // chars for left column content
517
-
518
- // Helper: build a stat row with fixed-width label
519
- const statRow = (icon, label, val) => {
520
- const raw = `${icon} ${label} ${val}`;
521
- const padded = raw + ' '.repeat(Math.max(0, leftW - raw.replace(RE, '').length));
522
- return padded;
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));
523
521
  };
524
522
 
525
- // Row 1: Balance
526
- const balVal = `${Au}⏣${c.reset}${c.bold} ${formatCoins(totalBalance)}${c.reset}`;
527
- const balRaw = `${Au}⟐${c.reset} ${D}BALANCE${c.reset} ${balVal}`;
528
- lines.push(bRow(` ${balRaw.padEnd(leftW + 4)}${A}~${c.reset} ${D}TREND${c.reset} ${spark}`));
529
-
530
- // Row 2: Earned
531
- const earnVal = `${perHrColor}${c.bold}${perHrSign}⏣ ${formatCoins(perHr)}/h${c.reset}`;
532
- const earnRaw = `${G}▲${c.reset} ${D}EARNED${c.reset} ${c.bold}${G}${perHrSign}⏣ ${formatCoins(totalCoins)}${c.reset}${newHighFlag}`;
533
- lines.push(bRow(` ${earnRaw.padEnd(leftW + 4)}${D}──┬──${c.reset} ${c.dim}earned over session${c.reset}`));
534
-
535
- // Row 3: Peak
536
- const peakRaw = `${O}★${c.reset} ${D}PEAK${c.reset} ${c.bold}${O}⏣ ${formatCoins(sessionPeakCoins)}${c.reset}`;
537
- lines.push(bRow(` ${peakRaw.padEnd(leftW + 4)}${D} │${c.reset} ${c.dim}⏣ ${formatCoins(Math.abs(perHr))}/h${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}`;
538
529
 
539
- // Row 4: Commands
540
- const cmdsRaw = `${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}`;
541
- lines.push(bRow(` ${cmdsRaw.padEnd(leftW + 4)}${D} │${c.reset}`));
530
+ // Build right column label
531
+ const rRate = `${perHrColor}${perHrSign}⏣${c.reset} ${formatCoins(Math.abs(perHr))}/h`;
542
532
 
543
- // Row 5: Memory
544
- const memRaw = `${D}≡${c.reset} ${D}MEM${c.reset} ${rgb(memCol[0], memCol[1], memCol[2])}${c.bold}${memMB}MB${c.reset} ${memBar}`;
545
- lines.push(bRow(` ${memRaw.padEnd(leftW + 4)}${D} │${c.reset}`));
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}`));
546
538
 
547
539
  lines.push(bEmpty);
548
540
 
@@ -3028,102 +3020,90 @@ async function start(apiKey, apiUrl) {
3028
3020
  console.log(` ${checks.join(' ')}`);
3029
3021
  console.log('');
3030
3022
 
3031
- // ── Per-account inline login UI ──────────────────────────────
3032
- // 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
+
3033
3032
  const loginStates = accounts.map((acc, i) => ({
3034
3033
  name: acc.label || acc.id || '?',
3035
3034
  done: false,
3036
3035
  failed: false,
3037
3036
  worker: null,
3038
- workerIdx: i,
3039
3037
  }));
3040
3038
 
3041
- // Column widths (visible chars)
3042
- const terminalW = process.stdout.columns || 90;
3043
- const colNum = 4; // " # "
3044
- const colSts = 3; // "ST "
3045
- const colName = Math.min(24, Math.max(12, Math.floor(terminalW * 0.25)));
3046
- const colGuild = Math.min(20, Math.max(8, Math.floor(terminalW * 0.2)));
3047
- const colCmds = 10; // " 20 cmds"
3048
- const totalVis = colNum + colSts + colName + colGuild + colCmds + 8;
3049
-
3050
- // Print header + all account lines (initial pending state)
3051
- console.log(` ${'─'.repeat(totalVis)}`);
3039
+ let loginLines = [];
3040
+ loginLines.push(` ${'─'.repeat(loginVis)}`);
3052
3041
  for (let i = 0; i < loginStates.length; i++) {
3053
3042
  const s = loginStates[i];
3054
- 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}`;
3055
3044
  const name = s.name.substring(0, colName).padEnd(colName);
3056
- 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}`);
3057
3048
  }
3058
- console.log(` ${'─'.repeat(totalVis)}`);
3049
+ loginLines.push(` ${'─'.repeat(loginVis)}`);
3050
+ for (const l of loginLines) console.log(l);
3051
+ loginLines = null;
3059
3052
 
3060
- // Draw pending spinners
3053
+ let loginPending = new Array(accounts.length).fill(true);
3061
3054
  const drawLoginSpinners = () => {
3062
- for (let i = 0; i < loginStates.length; i++) {
3063
- const s = loginStates[i];
3064
- if (s.done || s.failed) continue;
3055
+ for (let i = 0; i < loginPending.length; i++) {
3056
+ if (!loginPending[i]) continue;
3065
3057
  const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3066
- const num = `${c.dim}${(i + 1).toString().padStart(colNum - 1)}${c.reset} `;
3067
- const name = s.name.substring(0, colName).padEnd(colName);
3068
- const guild = 'logging in...'.substring(0, colGuild).padEnd(colGuild);
3069
- 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`);
3070
3063
  }
3071
- process.stdout.write(`\x1b[${loginStates.length + 3};0H`);
3072
3064
  };
3073
3065
  const loginSpinnerInterval = setInterval(drawLoginSpinners, 80);
3074
3066
 
3075
- // Update a line when an account finishes
3076
3067
  const finalizeLoginLine = (idx, worker) => {
3068
+ if (!loginPending[idx]) return;
3069
+ loginPending[idx] = false;
3077
3070
  const s = loginStates[idx];
3078
- if (s.done || s.failed) return;
3079
3071
  s.done = true;
3080
3072
  s.worker = worker;
3081
3073
 
3082
- 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}`;
3083
3075
  const name = (worker.username || s.name || '?').substring(0, colName).padEnd(colName);
3084
-
3085
3076
  let sts, guild, cmds;
3086
3077
  if (worker._tokenInvalid) {
3087
3078
  sts = `${rgb(239, 68, 68)}✗${c.reset}`;
3088
3079
  guild = 'INVALID'.padEnd(colGuild);
3089
- cmds = '···';
3080
+ cmds = '···'.padEnd(colCmds);
3090
3081
  s.failed = true;
3091
3082
  } else if (worker.channel) {
3092
3083
  sts = `${rgb(52, 211, 153)}✓${c.reset}`;
3093
3084
  const gn = (worker.channel.guild?.name || worker.channel.guild?.id || 'DM').substring(0, colGuild);
3094
3085
  guild = gn.padEnd(colGuild);
3095
- cmds = `${worker.stats?.commands || 0}cmds`;
3086
+ cmds = `${worker.stats?.commands || 0}`.padEnd(colCmds);
3096
3087
  } else {
3097
3088
  sts = `${rgb(251, 146, 60)}⏳${c.reset}`;
3098
3089
  guild = 'timeout'.padEnd(colGuild);
3099
- cmds = '···';
3090
+ cmds = '···'.padEnd(colCmds);
3100
3091
  }
3101
-
3102
- const line = ` ${num}${sts} ${name} ${guild} ${cmds}`;
3103
- 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`);
3104
3093
  };
3105
3094
 
3106
- // Phase 1: Login in batches of 10
3107
3095
  const parsedGapMin = Number.parseInt(String(process.env.LOGIN_GAP_MIN_MS || '50'), 10);
3108
3096
  const parsedGapMax = Number.parseInt(String(process.env.LOGIN_GAP_MAX_MS || '150'), 10);
3109
3097
  const LOGIN_GAP_MIN_MS = Number.isFinite(parsedGapMin) && parsedGapMin >= 0 ? parsedGapMin : 50;
3110
- const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(LOGIN_GAP_MIN_MS, 150);
3111
-
3112
- const randomLoginGap = () => {
3113
- if (LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS) return LOGIN_GAP_MIN_MS;
3114
- return LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
3115
- };
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));
3116
3100
 
3117
3101
  const BATCH_SIZE = 10;
3118
3102
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
3119
3103
  if (shutdownCalled) break;
3120
3104
  const batch = accounts.slice(i, Math.min(i + BATCH_SIZE, accounts.length));
3121
-
3122
3105
  await Promise.all(batch.map(async (acc, idx) => {
3123
- if (idx > 0) {
3124
- const jitter = 100 + Math.floor(Math.random() * 500);
3125
- await new Promise(r => setTimeout(r, jitter));
3126
- }
3106
+ if (idx > 0) await new Promise(r => setTimeout(r, 100 + Math.floor(Math.random() * 500)));
3127
3107
  const worker = new AccountWorker(acc, i + idx);
3128
3108
  workers.push(worker);
3129
3109
  workerMap.set(acc.id, worker);
@@ -3131,216 +3111,143 @@ async function start(apiKey, apiUrl) {
3131
3111
  await worker.start();
3132
3112
  finalizeLoginLine(i + idx, worker);
3133
3113
  }));
3134
-
3135
- if (i + BATCH_SIZE < accounts.length) {
3136
- await new Promise(r => setTimeout(r, randomLoginGap()));
3137
- }
3138
-
3114
+ if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
3139
3115
  hintGC();
3140
3116
  }
3141
3117
 
3142
3118
  clearInterval(loginSpinnerInterval);
3143
- process.stdout.write(`\x1b[${loginStates.length + 3};0H`);
3144
-
3145
- // Final summary
3146
3119
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
3147
3120
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
3148
3121
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
3149
- 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}`);
3150
3123
  console.log('');
3151
-
3152
3124
  if (invalidWorkers.length > 0) {
3153
3125
  log('warn', `${rgb(239, 68, 68)}${invalidWorkers.length} account(s) have INVALID tokens:${c.reset}`);
3154
- for (const w of invalidWorkers) {
3155
- log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
3156
- }
3126
+ for (const w of invalidWorkers) log('error', ` ✗ ${w.account.label || w.account.id} — token is invalid or expired`);
3157
3127
  console.log('');
3158
3128
  }
3159
- if (timedOutWorkers.length > 0) {
3160
- log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
3161
- }
3129
+ if (timedOutWorkers.length > 0) log('warn', `${timedOutWorkers.length} account(s) timed out during login (will retry in background)`);
3162
3130
 
3163
- // Filter out workers with invalid tokens from grinding
3164
3131
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
3165
3132
 
3166
- // ── Phase 2: Inventory check with per-account inline rendering ─────────────────
3167
- const invStates = activeWorkers.map((w, i) => ({
3168
- name: w.username || w.account.label || '?',
3169
- idx: i,
3170
- done: false,
3171
- failed: false,
3172
- items: 0,
3173
- value: 0,
3174
- attempt: 0,
3175
- worker: w,
3176
- }));
3177
-
3178
- const tw2 = process.stdout.columns || 90;
3133
+ // ── Phase 2: Inventory check spinner for pending count, results inline ─────────
3179
3134
  const iColNum = 4;
3180
- 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)));
3181
3136
  const iColItems = 8;
3182
- const iColVal = 14;
3183
- const iColTries = 10;
3184
- const iTotalVis = iColNum + iColName + iColItems + iColVal + iColTries + 10;
3185
-
3186
- console.log(` ${'─'.repeat(iTotalVis)}`);
3187
- for (let i = 0; i < invStates.length; i++) {
3188
- const s = invStates[i];
3189
- const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset} `;
3190
- const name = s.name.substring(0, iColName).padEnd(iColName);
3191
- 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}`);
3192
- }
3193
- console.log(` ${'─'.repeat(iTotalVis)}`);
3194
-
3195
- let invDone = 0, invFailed = 0;
3196
-
3197
- const drawInvSpinners = () => {
3198
- for (let i = 0; i < invStates.length; i++) {
3199
- const s = invStates[i];
3200
- if (s.done || s.failed) continue;
3201
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3202
- const num = `${c.dim}${(i + 1).toString().padStart(iColNum - 1)}${c.reset} `;
3203
- const name = s.name.substring(0, iColName).padEnd(iColName);
3204
- const items = 'checking'.substring(0, iColItems).padEnd(iColItems);
3205
- const val = ''.padEnd(iColVal);
3206
- const tries = s.attempt > 0 ? `try ${s.attempt}/3` : '';
3207
- 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}`);
3208
- }
3209
- process.stdout.write(`\x1b[${invStates.length + 3};0H`);
3210
- };
3211
- const invSpinnerInterval = setInterval(drawInvSpinners, 80);
3212
-
3213
- const finalizeInvLine = (idx, invRes) => {
3214
- const s = invStates[idx];
3215
- if (s.done || s.failed) return;
3216
- if (invRes?.ok) {
3217
- s.done = true;
3218
- invDone++;
3219
- } else {
3220
- s.failed = true;
3221
- invFailed++;
3222
- }
3223
-
3224
- const num = `${c.dim}${(idx + 1).toString().padStart(iColNum - 1)}${c.reset} `;
3225
- const name = s.name.substring(0, iColName).padEnd(iColName);
3226
- const items = `${invRes?.result?.items?.length || 0}`.padEnd(iColItems);
3227
- const val = invRes?.ok
3228
- ? `${c.green}⏣${(invRes.result?.totalValue || 0).toLocaleString()}${c.reset}`.padEnd(iColVal + 3)
3229
- : `${c.dim}··${c.reset}`.padEnd(iColVal);
3230
- const sts = invRes?.ok ? `${rgb(52, 211, 153)}✓${c.reset}` : `${rgb(239, 68, 68)}✗${c.reset}`;
3231
-
3232
- const line = ` ${num}${sts} ${name} ${items} ${val}`;
3233
- 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} `);
3234
3159
  };
3160
+ const invSpinnerInterval = setInterval(drawInvProgress, 80);
3235
3161
 
3236
3162
  await Promise.all(activeWorkers.map(async (w, i) => {
3237
- try {
3238
- const invRes = await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true });
3239
- finalizeInvLine(i, invRes);
3240
- } catch {
3241
- finalizeInvLine(i, { ok: false });
3242
- }
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++;
3243
3176
  }));
3244
3177
 
3245
3178
  clearInterval(invSpinnerInterval);
3246
- process.stdout.write(`\x1b[${invStates.length + 3};0H`);
3179
+ process.stdout.write(`\r\x1b[2K`);
3247
3180
 
3248
3181
  if (invFailed > 0) {
3249
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}`);
3250
3183
  log('error', `${c.red}Not starting grind loops — ${invFailed} accounts failed inventory.${c.reset}`);
3251
3184
  return;
3252
3185
  }
3253
-
3254
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}`);
3255
3187
  console.log('');
3256
3188
 
3257
- // ── Phase 2.5: Balance check with inline per-account rendering ─────────────────
3258
- const balStates = activeWorkers.map((w, i) => ({
3259
- name: w.username || w.account.label || '?',
3260
- idx: i,
3261
- done: false,
3262
- wallet: 0,
3263
- bank: 0,
3264
- worker: w,
3265
- }));
3266
-
3189
+ // ── Phase 2.5: Balance check inline table, single spinner for progress ─────────
3267
3190
  const bColNum = 4;
3268
- const bColName = Math.min(22, Math.max(10, Math.floor(tw2 * 0.22)));
3269
- const bColWallet = 14;
3270
- 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;
3271
3194
  const bColTotal = 14;
3272
3195
  const bColLs = 4;
3273
- const bTotalVis = bColNum + bColName + bColWallet + bColBank + bColTotal + bColLs + 12;
3274
-
3275
- console.log(` ${'─'.repeat(bTotalVis)}`);
3276
- for (let i = 0; i < balStates.length; i++) {
3277
- const s = balStates[i];
3278
- const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset} `;
3279
- const name = s.name.substring(0, bColName).padEnd(bColName);
3280
- 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}`);
3281
- }
3282
- console.log(` ${'─'.repeat(bTotalVis)}`);
3283
-
3284
- let balDone = 0;
3285
-
3286
- const drawBalSpinners = () => {
3287
- for (let i = 0; i < balStates.length; i++) {
3288
- const s = balStates[i];
3289
- if (s.done) continue;
3290
- const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
3291
- const num = `${c.dim}${(i + 1).toString().padStart(bColNum - 1)}${c.reset} `;
3292
- const name = s.name.substring(0, bColName).padEnd(bColName);
3293
- 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}`);
3294
- }
3295
- 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} `);
3296
3216
  };
3297
- const balSpinnerInterval = setInterval(drawBalSpinners, 80);
3298
-
3299
- const finalizeBalLine = (idx, w) => {
3300
- const s = balStates[idx];
3301
- if (s.done) return;
3302
- s.done = true;
3303
- s.wallet = w.stats?.balance || 0;
3304
- s.bank = w.stats?.bankBalance || 0;
3305
- balDone++;
3217
+ const balSpinnerInterval = setInterval(drawBalProgress, 80);
3306
3218
 
3307
- const num = `${c.dim}${(idx + 1).toString().padStart(bColNum - 1)}${c.reset} `;
3308
- 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;
3309
3226
  const ls = w._lifesavers ?? '?';
3310
3227
  const lsColor = ls === 0 ? rgb(239, 68, 68) : ls <= 2 ? rgb(251, 191, 36) : rgb(52, 211, 153);
3311
- const wallet = `${c.green}⏣${(s.wallet).toLocaleString()}${c.reset}`.padEnd(bColWallet + 3);
3312
- const bank = `${c.cyan}⏣${(s.bank).toLocaleString()}${c.reset}`.padEnd(bColBank + 3);
3313
- const total = `${c.bold}⏣${(s.wallet + s.bank).toLocaleString()}${c.reset}`.padEnd(bColTotal + 3);
3314
-
3315
- const line = ` ${num}${rgb(52, 211, 153)}✓${c.reset} ${name} ${wallet} ${bank} ${total} ${lsColor}♥${ls}${c.reset}`;
3316
- process.stdout.write(`\x1b[${idx + 2};0H\x1b[2K${line}`);
3317
- };
3318
-
3319
- await Promise.all(activeWorkers.map(async w => {
3320
- try {
3321
- await w.checkBalance(true); // silent: don't spam console during inline rendering
3322
- } catch {}
3323
- const idx = balStates.findIndex(s => s.worker === w);
3324
- 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++;
3325
3233
  }));
3326
3234
 
3327
3235
  clearInterval(balSpinnerInterval);
3328
- process.stdout.write(`\x1b[${balStates.length + 3};0H`);
3236
+ process.stdout.write(`\r\x1b[2K`);
3329
3237
 
3330
- // Balance summary
3331
3238
  let totalWallet = 0, totalBank = 0, noLifesaverAccounts = [];
3332
- for (const s of balStates) {
3333
- totalWallet += s.wallet;
3334
- totalBank += s.bank;
3335
- 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);
3336
3243
  }
3337
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}`);
3338
-
3339
3245
  if (noLifesaverAccounts.length > 0) {
3340
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(', ')}`);
3341
3247
  }
3342
3248
  console.log('');
3343
3249
 
3250
+
3344
3251
  // Phase 2.75: Check DM history for deaths/level-ups (sequential, fast)
3345
3252
  console.log(` ${rgb(139, 92, 246)}${BRAILLE_SPIN[0]}${c.reset} ${c.dim}Checking DM history...${c.reset}`);
3346
3253
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.25.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"