dankgrinder 6.3.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.
Files changed (2) hide show
  1. package/lib/grinder.js +303 -185
  2. 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 + '──────' + c.reset;
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 char = SPARK_CHARS[idx] || '▁';
233
- return v >= max * 0.9 ? rgb(52, 211, 153) + char + c.reset :
234
- v <= min * 1.1 ? rgb(239, 68, 68) + char + c.reset :
235
- rgb(139, 92, 246) + char + c.reset;
236
+ 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(6);
288
- const MAX_LOGS = 6;
289
- const RENDER_THROTTLE_MS = 250;
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(20);
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, 78);
345
- const thinBar = c.dim + '─'.repeat(tw) + c.reset;
346
- const bar = rgb(139, 92, 246) + c.bold + '━'.repeat(tw) + c.reset;
347
- const doubleBar = rgb(139, 92, 246) + '╔' + '═'.repeat(tw - 2) + '╗' + c.reset;
348
- const doubleBarMid = rgb(139, 92, 246) + '╠' + '═'.repeat(tw - 2) + '╣' + c.reset;
349
- const doubleBarBot = rgb(139, 92, 246) + '╚' + '═'.repeat(tw - 2) + '╝' + c.reset;
350
-
351
- // Header with title, stats, and mode
352
- lines.push(doubleBar);
353
- const cmdCount = AccountWorker.COMMAND_MAP.length;
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 mode = CLUSTER_ENABLED ? `${rgb(34, 211, 238)}Cluster${c.reset}` : `${c.dim}Standalone${c.reset}`;
356
-
357
- // Animated spinner and gradient title
358
- const spinners = ['◐', '◓', '◑', '◒'];
359
- const spinner = spinners[Math.floor(Date.now() / 250) % 4];
360
- const animatedSpinner = `${rgb(52, 211, 153)}${spinner}${c.reset}`;
361
- const gradientTitle = gradientLine('DankGrinder', [192, 132, 252], [34, 211, 238]);
362
-
363
- // Network quality indicator (based on recent latency/errors)
364
- const netQuality = workers.length > 0
365
- ? workers.reduce((s, w) => s + (w._lastPing < 200 ? 1 : w._lastPing < 500 ? 0.5 : 0), 0) / workers.length
366
- : 1;
367
- const netIcon = netQuality > 0.8 ? `${rgb(52, 211, 153)}◉${c.reset}` :
368
- netQuality > 0.5 ? `${rgb(251, 191, 36)}◉${c.reset}` :
369
- `${rgb(239, 68, 68)}◉${c.reset}`;
370
- const netLabel = netQuality > 0.8 ? `${c.dim}good${c.reset}` :
371
- netQuality > 0.5 ? `${c.dim}fair${c.reset}` :
372
- `${c.dim}poor${c.reset}`;
373
-
374
- lines.push(` ${c.bold}${gradientTitle}${c.reset} ${c.dim}v${PKG_VERSION}${c.reset}`);
375
- lines.push(` ${mode} ${c.dim}·${c.reset} ${netIcon} ${netLabel} ${c.dim}·${c.reset} ${c.white}${cmdCount} commands${c.reset} ${c.dim}·${c.reset} ${animatedSpinner} ${rgb(52, 211, 153)}${activeCount}${c.reset}${c.dim}/${c.reset}${c.white}${workers.length}${c.reset} ${c.dim}live${c.reset}`);
376
-
377
- // Stats row 1: Balance & Earnings
378
- lines.push(doubleBarMid);
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 coinsPerHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
384
- const rateLabel = `${rgb(52, 211, 153)}${formatCoins(coinsPerHr)}/h${c.reset}`;
385
-
386
- // Stats row 2: Commands & Performance
387
- const cmdStr = `${rgb(96, 165, 250)}${totalCommands}${c.reset}${c.dim} cmds${c.reset}`;
388
- const rateStr = successRate >= 95
389
- ? `${rgb(52, 211, 153)}● ${successRate}%${c.reset}`
390
- : successRate >= 80 ? `${rgb(251, 191, 36)}● ${successRate}%${c.reset}`
391
- : `${rgb(239, 68, 68)}${successRate}%${c.reset}`;
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 cpmStr = `${rgb(34, 211, 238)}${cpmVal}${c.reset}${c.dim}/m${c.reset}`;
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
- // Stats row 3: Uptime & Memory
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 memPct = Math.min(100, (memMB / 1024) * 100);
399
- const memBarWidth = Math.floor((memPct / 100) * 10);
400
- const memBar = rgb(52, 211, 153) + '▅'.repeat(memBarWidth) + c.dim + '▅'.repeat(10 - memBarWidth) + c.reset;
401
- const memColor = memMB > 900 ? rgb(239, 68, 68) : memMB > 600 ? rgb(251, 191, 36) : rgb(52, 211, 153);
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
- // Sample earnings for sparkline every 10 seconds
405
- const now = Date.now();
406
- if (now - lastEarningsSample > 10000) {
407
- earningsHistory.push(totalCoins);
408
- lastEarningsSample = now;
409
- }
410
- const sparkline = drawSparkline(earningsHistory.toArray(), 16);
411
- const peakIndicator = isNewHigh ? `${rgb(255, 100, 100)} 🚀 NEW HIGH!${c.reset}` : '';
412
-
413
- lines.push(` ${liveIcon} ${balStr} ${c.dim}│${c.reset} ${earnStr} ${c.dim}(${c.reset}${rateLabel}${c.dim})${c.reset}${peakIndicator}`);
414
- lines.push(` ${cmdStr} ${c.dim}(${c.reset}${rateStr}${c.dim})${c.reset} ${c.dim}│${c.reset} ${cpmStr} ${c.dim}│${c.reset} ${upStr}`);
415
- lines.push(` ${c.dim}Peak:${c.reset} ${rgb(255, 215, 0)}⏣ ${formatCoins(sessionPeakCoins)}${c.reset} ${c.dim}│${c.reset} Trend: ${sparkline} ${c.dim}│${c.reset} ${memStr}`);
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 topEarnerIds = new Set(topEarners.map(w => w.account.id));
432
-
433
- // Column headers
434
- lines.push(` ${c.dim}#${c.reset} ${c.dim}Account${' '.repeat(nameWidth - 5)}${c.reset} ${c.dim}Balance${c.reset} ${c.dim}Earned${c.reset} ${c.dim}Status${c.reset}`);
435
- lines.push(` ${c.dim}${'─'.repeat(tw - 4)}${c.reset}`);
436
-
437
- const renderWorkerRow = (wk, index) => {
438
- const pos = `${c.dim}#${c.reset}${c.bold}${(index + 1).toString().padStart(2, ' ')}${c.reset}`;
439
- const rawStatus = (wk.lastStatus || 'idle').replace(RE_ANSI_STRIP, '');
440
- const last = rawStatus.substring(0, statusWidth);
441
-
442
- let dot, stateLabel;
443
- const isRecovering = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
444
- if (!wk.running) {
445
- dot = `${rgb(239, 68, 68)}○${c.reset}`;
446
- stateLabel = `${c.dim}offline${c.reset}`;
447
- } else if (isRecovering) {
448
- const sLeft = Math.ceil((wk._errorCooldownUntil - Date.now()) / 1000);
449
- dot = `${rgb(251, 191, 36)}↻${c.reset}`;
450
- stateLabel = `${rgb(251, 191, 36)}recovering #${wk._recoveryAttempts} (${sLeft}s)${c.reset}`;
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
- dot = `${rgb(239, 68, 68)}⏸${c.reset}`;
453
- stateLabel = `${rgb(239, 68, 68)}PAUSED${c.reset}`;
517
+ sts = `${red}⏸${c.reset}`;
518
+ statLabel = `${red}PAUSED${c.reset}`;
454
519
  } else if (wk.dashboardPaused) {
455
- dot = `${rgb(251, 191, 36)}⏸${c.reset}`;
456
- stateLabel = `${rgb(251, 191, 36)}paused${c.reset}`;
520
+ sts = `${yellow}⏸${c.reset}`;
521
+ statLabel = `${yellow}paused (user)${c.reset}`;
457
522
  } else if (wk.busy) {
458
- dot = `${rgb(251, 191, 36)}◉${c.reset}`;
459
- stateLabel = `${c.dim}${last}${c.reset}`;
523
+ sts = `${green}${getSpinner('pulse')}${c.reset}`;
524
+ statLabel = `${dim}${statTxt}${c.reset}`;
460
525
  } else {
461
- dot = `${rgb(52, 211, 153)}●${c.reset}`;
462
- stateLabel = `${c.dim}${last}${c.reset}`;
526
+ sts = `${green}●${c.reset}`;
527
+ statLabel = `${dim}${statTxt}${c.reset}`;
463
528
  }
464
529
 
465
- // Top earner medal
466
- let medal = ' ';
467
- if (topEarnerIds.has(wk.account.id)) {
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
- medal = rank === 0 ? `${rgb(255, 215, 0)}🥇${c.reset}` : rank === 1 ? `${rgb(192, 192, 192)}🥈${c.reset}` : `${rgb(205, 127, 50)}🥉${c.reset}`;
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, nameWidth)}${c.reset}`;
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
- ? `${rgb(251, 191, 36)}⏣${c.reset} ${c.white}${formatCoins(wk.stats.balance).padStart(8)}${c.reset}`
475
- : `${c.dim}⏣ -${c.reset}`;
476
-
477
- // Enhanced progress bar for earned coins
478
- const earnedNum = wk.stats.coins || 0;
479
- const earnedBarWidth = earnedNum > 0 ? Math.min(5, Math.max(1, Math.floor(Math.log10(earnedNum + 1)))) : 0;
480
- const earnedBar = earnedNum > 0
481
- ? `${rgb(52, 211, 153)}${'▰'.repeat(earnedBarWidth)}${c.dim}${'▱'.repeat(5 - earnedBarWidth)}${c.reset}`
482
- : `${c.dim}▱▱▱▱▱${c.reset}`;
483
- const earned = earnedNum > 0
484
- ? `${rgb(52, 211, 153)}+${formatCoins(earnedNum)}${c.reset} ${earnedBar}`
485
- : `${c.dim}+0${c.reset} ${earnedBar}`;
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 <= MAX_VISIBLE_WORKERS) {
491
- for (let i = 0; i < workers.length; i++) renderWorkerRow(workers[i], i);
556
+ if (workers.length <= MAX_VIS) {
557
+ for (let i = 0; i < workers.length; i++) renderRow(workers[i], i);
492
558
  } else {
493
- // Pagination: show first MAX_VISIBLE_WORKERS, summarize rest
494
- for (let i = 0; i < MAX_VISIBLE_WORKERS; i++) renderWorkerRow(workers[i], i);
495
- const remaining = workers.length - MAX_VISIBLE_WORKERS;
496
- let hiddenActive = 0, hiddenPaused = 0, hiddenRecovering = 0, hiddenOffline = 0;
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) hiddenOffline++;
500
- else if (w.paused || w.dashboardPaused) hiddenPaused++;
501
- else if (w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()) hiddenRecovering++;
502
- else hiddenActive++;
503
- }
504
- const parts = [`${c.dim}┃${c.reset} ${c.dim}... +${remaining} more${c.reset}`];
505
- if (hiddenActive > 0) parts.push(`${rgb(52, 211, 153)}● ${hiddenActive} active${c.reset}`);
506
- if (hiddenPaused > 0) parts.push(`${rgb(251, 191, 36)}⏸ ${hiddenPaused} paused${c.reset}`);
507
- if (hiddenRecovering > 0) parts.push(`${rgb(251, 191, 36)}↻ ${hiddenRecovering} recovering${c.reset}`);
508
- if (hiddenOffline > 0) parts.push(`${c.dim}○ ${hiddenOffline} offline${c.reset}`);
509
- lines.push(` ${parts.join(` ${c.dim}·${c.reset} `)}`);
510
- }
511
-
512
- // Recovery & status summary line with enhanced styling
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 (recoveringCount > 0 || pausedCount > 0 || totalRecoveries > 0 || totalDisconnects > 0) {
518
- const parts = [];
519
- if (recoveringCount > 0) parts.push(`${rgb(251, 191, 36)}↻ ${recoveringCount} recovering${c.reset}`);
520
- if (pausedCount > 0) parts.push(`${rgb(239, 68, 68)} ${pausedCount} paused${c.reset}`);
521
- if (totalRecoveries > 0) parts.push(`${c.dim}${totalRecoveries} auto-recovered${c.reset}`);
522
- if (totalDisconnects > 0) parts.push(`${c.dim}${totalDisconnects} reconnects${c.reset}`);
523
- lines.push(` ${c.dim}┃${c.reset} ${parts.join(` ${c.dim}·${c.reset} `)}`);
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 line with enhanced styling
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(` ${rgb(34, 211, 238)}╔${c.reset}${c.dim} Cluster: ${nodeShort} · ${claimedCount} claimed ${c.reset}${rgb(34, 211, 238)}╗${c.reset}`);
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
- // Log section
597
+ // ━━━ LIVE LOG FEED ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
534
598
  const logEntries = recentLogs.toArray();
535
599
  if (logEntries.length > 0) {
536
- lines.push(thinBar);
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(` ${c.dim}${entry}${c.reset}`);
604
+ lines.push(boxLine(`${dim}${entry}${c.reset}`, tw, accent));
539
605
  }
540
606
  }
541
607
 
542
- lines.push(doubleBarBot);
608
+ // ━━━ FOOTER ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
609
+ lines.push(boxBot(tw, accent));
543
610
 
544
- // Absolute cursor home — always draw from row 1
611
+ // Render
545
612
  process.stdout.write('\x1b[H');
546
613
  for (const line of lines) {
547
614
  process.stdout.write(c.clearLine + '\r' + line + '\n');
548
615
  }
549
- // Erase everything below the dashboard (clears ghost bars, trailing lines)
550
616
  process.stdout.write('\x1b[J');
551
617
  dashboardLines = lines.length;
552
618
  dashboardRendering = false;
@@ -2670,6 +2736,22 @@ async function start(apiKey, apiUrl) {
2670
2736
  console.log(` ${checks.join(' ')}`);
2671
2737
  console.log('');
2672
2738
 
2739
+ // ── Animated loading bar helper ──────────────────────────────
2740
+ const barW = Math.min(40, (process.stdout.columns || 80) - 30);
2741
+ let loginDone = 0;
2742
+ const drawLoginProgress = () => {
2743
+ const pct = accounts.length > 0 ? loginDone / accounts.length : 0;
2744
+ const filled = Math.round(pct * barW);
2745
+ const empty = barW - filled;
2746
+ const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
2747
+ const bar = rgb(139, 92, 246) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(empty) + c.reset;
2748
+ const pctStr = `${Math.round(pct * 100)}%`;
2749
+ process.stdout.write(`\r ${rgb(139, 92, 246)}${spin}${c.reset} ${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
+
2673
2755
  // Phase 1: Login all accounts (optimized for speed)
2674
2756
  const LOGIN_PROGRESS_EVERY = 10;
2675
2757
  // Reduced delays: 50-150ms between logins (faster startup for 1k+ accounts)
@@ -2701,18 +2783,24 @@ async function start(apiKey, apiUrl) {
2701
2783
  workers.push(worker);
2702
2784
  workerMap.set(acc.id, worker);
2703
2785
  await worker.start();
2786
+ loginDone++;
2704
2787
  }));
2705
2788
 
2706
2789
  // Small gap between batches
2707
2790
  if (i + BATCH_SIZE < accounts.length) {
2708
2791
  const gapMs = randomLoginGap();
2709
- log('info', `${c.dim}Logged in ${Math.min(i + BATCH_SIZE, accounts.length)}/${accounts.length}...${c.reset}`);
2710
2792
  await new Promise(r => setTimeout(r, gapMs));
2711
2793
  }
2712
2794
 
2713
2795
  hintGC();
2714
2796
  }
2715
2797
 
2798
+ clearInterval(progressInterval);
2799
+ // Clear the progress line and show done
2800
+ process.stdout.write(`\r${c.clearLine}`);
2801
+ console.log(` ${rgb(52, 211, 153)}✓${c.reset} ${c.bold}Login complete${c.reset} ${rgb(52, 211, 153)}${loginDone}/${accounts.length}${c.reset} ${dim}accounts connected${c.reset}`);
2802
+ console.log('');
2803
+
2716
2804
  // Login summary: show invalid tokens clearly
2717
2805
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
2718
2806
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
@@ -2732,12 +2820,24 @@ async function start(apiKey, apiUrl) {
2732
2820
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
2733
2821
 
2734
2822
  // Phase 2: Run inventory on ALL valid accounts in parallel (must complete before grinding)
2735
- log('info', `${c.dim}Checking inventory for ${activeWorkers.length} accounts...${c.reset}`);
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}`);
2736
2824
 
2737
- // Parallel inventory checks with single-line progress
2825
+ // Animated inventory progress
2738
2826
  let invDone = 0;
2739
2827
  let invFailed = 0;
2740
2828
  const total = activeWorkers.length;
2829
+ const invBarW = Math.min(40, (process.stdout.columns || 80) - 30);
2830
+
2831
+ const drawInvProgress = () => {
2832
+ const pct = total > 0 ? invDone / total : 0;
2833
+ const filled = Math.round(pct * invBarW);
2834
+ const empty = invBarW - filled;
2835
+ const spin = BRAILLE_SPIN[Math.floor(Date.now() / 80) % BRAILLE_SPIN.length];
2836
+ const bar = rgb(34, 211, 238) + '█'.repeat(filled) + rgb(50, 50, 70) + '░'.repeat(empty) + c.reset;
2837
+ process.stdout.write(`\r ${rgb(34, 211, 238)}${spin}${c.reset} ${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
+ };
2839
+
2840
+ const invProgressInterval = setInterval(drawInvProgress, 80);
2741
2841
 
2742
2842
  await Promise.all(activeWorkers.map(async (w, i) => {
2743
2843
  try {
@@ -2754,16 +2854,20 @@ async function start(apiKey, apiUrl) {
2754
2854
  }
2755
2855
  }));
2756
2856
 
2757
- // Final summary
2758
- log('success', `Inventory: ${invDone}/${total} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}` : ''}`);
2857
+ clearInterval(invProgressInterval);
2858
+ process.stdout.write(`\r${c.clearLine}`);
2759
2859
 
2860
+ // Final summary
2760
2861
  if (invFailed > 0) {
2761
- log('error', `${c.red}Inventory phase incomplete: ${invDone}/${activeWorkers.length} complete, ${invFailed} failed/incomplete. Not starting grind loops.${c.reset}`);
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}`);
2762
2864
  return;
2763
2865
  }
2764
2866
 
2765
- const invSummaryColor = invFailed > 0 ? c.yellow : c.green;
2766
- log('success', `${c.dim}Inventory phase complete: ${invSummaryColor}${invDone}/${activeWorkers.length}${c.reset}${c.dim} done${invFailed > 0 ? `, ${c.yellow}${invFailed} failed${c.reset}${c.dim}` : ''}. Starting grind loops...${c.reset}`);
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('');
2767
2871
 
2768
2872
  // Phase 3: Start all grind loops (only for valid workers)
2769
2873
  for (const w of activeWorkers) {
@@ -2780,6 +2884,13 @@ async function start(apiKey, apiUrl) {
2780
2884
  process.stdout.write(c.hide);
2781
2885
  dashboardLines = 0;
2782
2886
 
2887
+ // Re-render on terminal resize so layout adapts to window size
2888
+ process.stdout.on('resize', () => {
2889
+ process.stdout.write('\x1b[2J\x1b[H');
2890
+ dashboardLines = 0;
2891
+ scheduleRender();
2892
+ });
2893
+
2783
2894
  setInterval(() => scheduleRender(), 1000);
2784
2895
  scheduleRender();
2785
2896
 
@@ -2928,10 +3039,17 @@ function setupKeyboardShortcuts() {
2928
3039
  process.stdin.resume();
2929
3040
  process.stdin.setEncoding('utf8');
2930
3041
 
2931
- // Modern styled keyboard shortcuts with box drawing
2932
- console.log(`\n ${rgb(139, 92, 246)}╭${c.reset}${c.dim}${'─'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╮${c.reset}`);
2933
- console.log(` ${rgb(139, 92, 246)}│${c.reset} ${c.bold}Shortcuts:${c.reset} ${c.dim}p${c.reset}=pause ${c.dim}r${c.reset}=resume ${c.dim}s${c.reset}=status ${c.dim}q${c.reset}=quit ${c.dim}?${c.reset}=help ${rgb(139, 92, 246)}│${c.reset}`);
2934
- console.log(` ${rgb(139, 92, 246)}╰${c.reset}${c.dim}${'─'.repeat(50)}${c.reset}${rgb(139, 92, 246)}╯${c.reset}`);
3042
+ // Premium styled keyboard shortcuts with gradient box
3043
+ const accent = rgb(139, 92, 246);
3044
+ const dim = c.dim;
3045
+ const kw = 60;
3046
+ console.log('');
3047
+ console.log(` ${accent}╭${'─'.repeat(kw)}╮${c.reset}`);
3048
+ console.log(` ${accent}│${c.reset} ${gradientText('KEYBOARD SHORTCUTS', [192, 132, 252], [52, 211, 153])}${' '.repeat(kw - 20)}${accent}│${c.reset}`);
3049
+ console.log(` ${accent}├${'─'.repeat(kw)}┤${c.reset}`);
3050
+ console.log(` ${accent}│${c.reset} ${rgb(96, 165, 250)}P${c.reset} ${dim}Pause all${c.reset} ${rgb(52, 211, 153)}R${c.reset} ${dim}Resume all${c.reset} ${rgb(251, 191, 36)}S${c.reset} ${dim}Status${c.reset} ${rgb(239, 68, 68)}Q${c.reset} ${dim}Quit${c.reset}${' '.repeat(Math.max(0, kw - 54))}${accent}│${c.reset}`);
3051
+ console.log(` ${accent}╰${'─'.repeat(kw)}╯${c.reset}`);
3052
+ console.log('');
2935
3053
 
2936
3054
  process.stdin.on('data', (key) => {
2937
3055
  const k = key.toString().toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.3.0",
3
+ "version": "6.5.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"