dankgrinder 6.6.0 → 6.8.1

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 +214 -146
  2. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -393,222 +393,290 @@ function renderDashboard() {
393
393
  setTimeout(() => { isNewHigh = false; }, 3000);
394
394
  }
395
395
 
396
+ // ── Layout: use FULL terminal width ──
396
397
  const lines = [];
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
398
+ const tw = Math.max(process.stdout.columns || 80, 60);
399
+ const iw = tw - 4; // inner width (inside box borders)
400
+
401
+ // Color shortcuts (local to this function)
402
+ const A = rgb(139, 92, 246); // accent purple
403
+ const G = rgb(52, 211, 153); // green
404
+ const B = rgb(96, 165, 250); // blue
405
+ const P = rgb(236, 72, 153); // pink
406
+ const Au = rgb(255, 215, 0); // gold
407
+ const O = rgb(251, 146, 60); // orange
408
+ const Cy = rgb(34, 211, 238); // cyan
409
+ const R = rgb(239, 68, 68); // red
410
+ const Y = rgb(251, 191, 36); // yellow
411
+ const W = c.white;
412
+ const D = c.dim;
413
+ const RE = /\x1b\[[0-9;]*m/g;
414
+
415
+ // ── Box drawing (scales to tw) ──
416
+ const bTop = A + '╔' + '═'.repeat(tw - 2) + '╗' + c.reset;
417
+ const bMid = A + '╟' + '─'.repeat(tw - 2) + '╢' + c.reset;
418
+ const bSep = A + '╠' + '═'.repeat(tw - 2) + '╣' + c.reset;
419
+ const bBot = A + '╚' + '═'.repeat(tw - 2) + '╝' + c.reset;
420
+ const bRow = (content) => {
421
+ const vis = content.replace(RE, '').length;
422
+ const pad = Math.max(0, iw - vis);
423
+ return A + '║' + c.reset + ' ' + content + ' '.repeat(pad) + ' ' + A + '║' + c.reset;
424
+ };
425
+ const bEmpty = bRow('');
426
+
427
+ // ═══════════════════════════════════════════════════════════════
428
+ // HEADER
429
+ // ═══════════════════════════════════════════════════════════════
430
+ lines.push(bTop);
431
+ lines.push(bEmpty);
432
+
433
+ // Title with animated spinner
416
434
  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));
435
+ const titleGrad = gradientText(' D A N K G R I N D E R ', [192, 132, 252], [52, 211, 153]);
436
+ lines.push(bRow(` ${c.bold}${titleGrad}${c.reset} ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset}`));
420
437
 
421
- // Info bar
438
+ // Subtitle info
422
439
  const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
440
+ const invalidCount = workers.filter(w => w._tokenInvalid).length;
423
441
  const pausedCount = workers.filter(w => w.paused || w.dashboardPaused).length;
424
442
  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}`;
443
+ const mode = CLUSTER_ENABLED ? `${Cy}CLUSTER${c.reset}` : `${D}Standalone${c.reset}`;
426
444
  const cmdCount = AccountWorker.COMMAND_MAP.length;
427
445
 
428
- // Net quality
429
446
  const netQ = workers.length > 0
430
447
  ? 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}`;
448
+ const netDot = netQ > 0.8 ? `${G}●${c.reset}` : netQ > 0.5 ? `${Y}●${c.reset}` : `${R}●${c.reset}`;
449
+ const netLbl = netQ > 0.8 ? `${G}Good${c.reset}` : netQ > 0.5 ? `${Y}Fair${c.reset}` : `${R}Poor${c.reset}`;
450
+
451
+ const infoParts = [
452
+ mode,
453
+ `${netDot} ${netLbl}`,
454
+ `${B}${cmdCount}${c.reset} ${D}commands${c.reset}`,
455
+ `${G}${activeCount}${c.reset}${D}/${c.reset}${W}${workers.length}${c.reset} ${D}active${c.reset}`,
456
+ ];
457
+ if (invalidCount > 0) infoParts.push(`${R}${invalidCount} invalid${c.reset}`);
458
+ if (pausedCount > 0) infoParts.push(`${Y}${pausedCount} paused${c.reset}`);
459
+ if (recovCount > 0) infoParts.push(`${O}${recovCount} recovering${c.reset}`);
460
+ lines.push(bRow(` ${infoParts.join(` ${D}·${c.reset} `)}`));
433
461
 
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));
462
+ lines.push(bEmpty);
435
463
 
436
- // ━━━ STATS SECTION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
437
- const midLine = accent + BOX.tee + BOX.h.repeat(tw - 2) + BOX.teeR + c.reset;
438
- lines.push(midLine);
464
+ // ═══════════════════════════════════════════════════════════════
465
+ // STATS PANEL
466
+ // ═══════════════════════════════════════════════════════════════
467
+ lines.push(bSep);
468
+ lines.push(bEmpty);
439
469
 
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));
470
+ // Earnings sparkline data
471
+ const now = Date.now();
472
+ if (now - lastEarningsSample > 8000) { earningsHistory.push(totalCoins); lastEarningsSample = now; }
473
+ const sparkW = Math.min(30, Math.floor(iw * 0.3));
474
+ const spark = drawSparkline(earningsHistory.toArray(), sparkW);
445
475
 
446
- // Earnings card with sparkline
476
+ // Row 1: Balance + Earned
447
477
  const elapsedHrs = (Date.now() - startTime) / 3_600_000;
448
478
  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));
479
+ const peakFlag = isNewHigh ? ` ${R}${c.bold}* NEW HIGH *${c.reset}` : '';
480
+
481
+ lines.push(bRow(` ${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} ${D}(${c.reset}${G}${formatCoins(perHr)}/h${c.reset}${D})${c.reset}${peakFlag}`));
457
482
 
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));
483
+ // Row 2: Peak + Trend sparkline
484
+ lines.push(bRow(` ${O}★${c.reset} ${D}PEAK${c.reset} ${c.bold}${O}⏣ ${formatCoins(sessionPeakCoins)}${c.reset} ${A}~${c.reset} ${D}TREND${c.reset} ${spark}`));
461
485
 
462
- // Performance metrics line
486
+ // Row 3: Commands + Success + Rate + Uptime
463
487
  const cpmVal = globalCmdRate.getRate().toFixed(1);
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));
488
+ const srColor = successRate >= 95 ? G : successRate >= 80 ? Y : R;
489
+ const srBarW = Math.min(15, Math.floor(iw * 0.12));
490
+ const srBar = progressBar(successRate, 100, srBarW, successRate >= 95 ? [52, 211, 153] : successRate >= 80 ? [251, 191, 36] : [239, 68, 68]);
491
+ lines.push(bRow(` ${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} ${Y}◷${c.reset} ${D}UP${c.reset} ${c.bold}${Y}${formatUptime()}${c.reset}`));
468
492
 
469
- // Memory line
493
+ // Row 4: Memory
470
494
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
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));
475
-
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));
495
+ const memCol = memMB > 900 ? [239, 68, 68] : memMB > 600 ? [251, 191, 36] : [52, 211, 153];
496
+ const memBarW = Math.min(20, Math.floor(iw * 0.15));
497
+ const memBar = progressBar(memMB, 1024, memBarW, memCol, [40, 40, 55]);
498
+ lines.push(bRow(` ${D}≡${c.reset} ${D}MEM${c.reset} ${rgb(memCol[0], memCol[1], memCol[2])}${c.bold}${memMB}MB${c.reset} ${memBar}`));
499
+
500
+ lines.push(bEmpty);
501
+
502
+ // ═══════════════════════════════════════════════════════════════
503
+ // ACCOUNTS TABLE (sorted by original index, proper alignment)
504
+ // ═══════════════════════════════════════════════════════════════
505
+ lines.push(bSep);
506
+
507
+ // Column widths scale with terminal
508
+ const colNum = 4; // " 1 "
509
+ const colSts = 3; // "● "
510
+ const colMedal = 4; // " 1st" or " "
511
+ const colBal = 12; // "⏣ 999.9M "
512
+ const colEarn = 10; // "+999.9K "
513
+ const colBar = 8; // "████░░░░"
514
+ const fixedW = colNum + colSts + colMedal + colBal + colEarn + colBar + 16; // 16 for spacing
515
+ const colName = Math.max(10, Math.min(22, Math.floor((iw - fixedW) * 0.45)));
516
+ const colActivity = Math.max(10, iw - fixedW - colName);
517
+
518
+ // Header
519
+ const hNum = `${D}${'#'.padStart(colNum)}${c.reset}`;
520
+ const hSts = `${D}${'ST'.padEnd(colSts)}${c.reset}`;
521
+ const hMedal = `${D}${'RNK'.padEnd(colMedal)}${c.reset}`;
522
+ const hName = `${gradientText('Account', [139, 92, 246], [96, 165, 250])}${''.padEnd(Math.max(0, colName - 7))}`;
523
+ const hBal = `${D}${'Balance'.padEnd(colBal)}${c.reset}`;
524
+ const hEarn = `${D}${'Earned'.padEnd(colEarn)}${c.reset}`;
525
+ const hBar = `${D}${''.padEnd(colBar)}${c.reset}`;
526
+ const hAct = `${D}Activity${c.reset}`;
527
+
528
+ lines.push(bRow(` ${hNum} ${hSts} ${hMedal} ${hName} ${hBal} ${hEarn} ${hBar} ${hAct}`));
529
+ lines.push(bRow(` ${D}${'─'.repeat(iw - 2)}${c.reset}`));
530
+
531
+ // Sort workers by original index for consistent display
532
+ const sortedWorkers = [...workers].sort((a, b) => a.idx - b.idx);
486
533
 
487
534
  // Top 3 earners
488
535
  const topEarners = [...workers]
489
- .filter(w => w.running && (w.stats.coins || 0) > 0)
536
+ .filter(w => (w.stats.coins || 0) > 0)
490
537
  .sort((a, b) => (b.stats.coins || 0) - (a.stats.coins || 0))
491
538
  .slice(0, 3);
492
- const topIds = new Set(topEarners.map(w => w.account.id));
539
+ const topIds = new Map();
540
+ topEarners.forEach((w, i) => topIds.set(w.account.id, i));
493
541
 
494
542
  const MAX_VIS = 30;
543
+ const visibleWorkers = sortedWorkers.slice(0, MAX_VIS);
495
544
 
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);
545
+ for (const wk of visibleWorkers) {
546
+ const origNum = (wk.idx + 1).toString().padStart(colNum);
547
+ const rawStat = (wk.lastStatus || 'idle').replace(RE, '');
548
+ const activityText = rawStat.substring(0, colActivity);
502
549
 
503
- // Status indicator with animations
504
- let sts, statLabel;
550
+ // ── Status icon ──
551
+ let stsIcon, actLabel;
505
552
  const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
506
553
  if (wk._tokenInvalid) {
507
- sts = `${red}✗${c.reset}`;
508
- statLabel = `${red}TOKEN INVALID${c.reset}`;
554
+ stsIcon = `${R}✗${c.reset}`;
555
+ actLabel = `${R}TOKEN INVALID${c.reset}`;
509
556
  } else if (!wk.running) {
510
- sts = `${dim}○${c.reset}`;
511
- statLabel = `${dim}offline${c.reset}`;
557
+ stsIcon = `${D}○${c.reset}`;
558
+ actLabel = `${D}offline${c.reset}`;
512
559
  } else if (isRecov) {
513
560
  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}`;
561
+ stsIcon = `${O}${getSpinner('braille')}${c.reset}`;
562
+ actLabel = `${O}recovering (${sL}s)${c.reset}`;
516
563
  } else if (wk.paused) {
517
- sts = `${red}⏸${c.reset}`;
518
- statLabel = `${red}PAUSED${c.reset}`;
564
+ stsIcon = `${R}⏸${c.reset}`;
565
+ actLabel = `${R}PAUSED${c.reset}`;
519
566
  } else if (wk.dashboardPaused) {
520
- sts = `${yellow}⏸${c.reset}`;
521
- statLabel = `${yellow}paused (user)${c.reset}`;
567
+ stsIcon = `${Y}⏸${c.reset}`;
568
+ actLabel = `${Y}paused${c.reset}`;
522
569
  } else if (wk.busy) {
523
- sts = `${green}${getSpinner('pulse')}${c.reset}`;
524
- statLabel = `${dim}${statTxt}${c.reset}`;
570
+ stsIcon = `${G}${getSpinner('pulse')}${c.reset}`;
571
+ actLabel = `${D}${activityText}${c.reset}`;
525
572
  } else {
526
- sts = `${green}●${c.reset}`;
527
- statLabel = `${dim}${statTxt}${c.reset}`;
573
+ stsIcon = `${G}●${c.reset}`;
574
+ actLabel = `${D}${activityText}${c.reset}`;
528
575
  }
529
576
 
530
- // Medal for top earners
531
- let medal = ' ';
577
+ // ── Medal (fixed 3-char visible width + 1 space) ──
578
+ let medalStr;
532
579
  if (topIds.has(wk.account.id)) {
533
- const rank = topEarners.findIndex(e => e.account.id === wk.account.id);
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}`;
580
+ const rank = topIds.get(wk.account.id);
581
+ if (rank === 0) medalStr = `${Au}1st${c.reset} `;
582
+ else if (rank === 1) medalStr = `${rgb(192, 192, 192)}2nd${c.reset} `;
583
+ else medalStr = `${rgb(205, 127, 50)}3rd${c.reset} `;
584
+ } else {
585
+ medalStr = ' '; // 4 chars: 3 empty + 1 space
536
586
  }
537
587
 
538
- const name = `${wk.color}${c.bold}${(wk.username || '?').substring(0, nameW).padEnd(nameW)}${c.reset}`;
588
+ // ── Name (fixed visible width, padded) ──
589
+ const rawName = (wk.username || wk.account.label || '?').substring(0, colName);
590
+ const nameStr = `${wk.color}${c.bold}${rawName.padEnd(colName)}${c.reset}`;
539
591
 
540
- // Balance with icon
541
- const bal = wk.stats.balance > 0
542
- ? `${gold}⏣${c.reset}${white}${formatCoins(wk.stats.balance).padStart(9)}${c.reset}`
543
- : `${dim}⏣ -${c.reset}`;
592
+ // ── Balance (fixed visible width) ──
593
+ let balStr;
594
+ if (wk.stats.balance > 0) {
595
+ balStr = `${Au}⏣${c.reset}${W}${formatCoins(wk.stats.balance).padStart(colBal - 2)}${c.reset}`;
596
+ } else {
597
+ balStr = `${D}⏣${' '.repeat(colBal - 3)}-${c.reset}`;
598
+ }
544
599
 
545
- // Earnings with mini progress
600
+ // ── Earned (fixed visible width) ──
546
601
  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)}`;
602
+ let earnStr;
603
+ if (earnNum > 0) {
604
+ earnStr = `${G}+${formatCoins(earnNum).padEnd(colEarn - 1)}${c.reset}`;
605
+ } else if (earnNum < 0) {
606
+ earnStr = `${R}${formatCoins(earnNum).padEnd(colEarn)}${c.reset}`;
607
+ } else {
608
+ earnStr = `${D}${'-'.padEnd(colEarn)}${c.reset}`;
609
+ }
552
610
 
553
- lines.push(boxLine(`${num} ${sts}${medal.padEnd(medal.length > 1 ? 3 : 1)} ${name} ${bal} ${earned} ${statLabel}`, tw, accent));
554
- };
611
+ // ── Progress bar (fixed width) ──
612
+ const earnBarFill = earnNum > 0 ? Math.min(colBar, Math.max(1, Math.floor(Math.log10(earnNum + 1)))) : 0;
613
+ const earnBar = progressBar(earnBarFill, colBar, colBar, [52, 211, 153], [40, 40, 55]);
555
614
 
556
- if (workers.length <= MAX_VIS) {
557
- for (let i = 0; i < workers.length; i++) renderRow(workers[i], i);
558
- } else {
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++) {
563
- const w = workers[i];
564
- if (!w.running) ho++;
615
+ lines.push(bRow(` ${D}${origNum}${c.reset} ${stsIcon} ${medalStr}${nameStr} ${balStr} ${earnStr} ${earnBar} ${actLabel}`));
616
+ }
617
+
618
+ // Overflow summary
619
+ if (sortedWorkers.length > MAX_VIS) {
620
+ const rest = sortedWorkers.length - MAX_VIS;
621
+ let ha = 0, hp = 0, hr = 0, ho = 0, hi = 0;
622
+ for (let i = MAX_VIS; i < sortedWorkers.length; i++) {
623
+ const w = sortedWorkers[i];
624
+ if (w._tokenInvalid) hi++;
625
+ else if (!w.running) ho++;
565
626
  else if (w.paused || w.dashboardPaused) hp++;
566
627
  else if (w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()) hr++;
567
628
  else ha++;
568
629
  }
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));
630
+ const parts = [`${D}+${rest} more${c.reset}`];
631
+ if (ha > 0) parts.push(`${G}${ha} active${c.reset}`);
632
+ if (hp > 0) parts.push(`${Y}${hp} paused${c.reset}`);
633
+ if (hr > 0) parts.push(`${O}${hr} recovering${c.reset}`);
634
+ if (hi > 0) parts.push(`${R}${hi} invalid${c.reset}`);
635
+ if (ho > 0) parts.push(`${D}${ho} offline${c.reset}`);
636
+ lines.push(bRow(` ${parts.join(` ${D}·${c.reset} `)}`));
575
637
  }
576
638
 
577
- // ━━━ HEALTH SUMMARY ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
639
+ // ═══════════════════════════════════════════════════════════════
640
+ // HEALTH BAR
641
+ // ═══════════════════════════════════════════════════════════════
578
642
  const totalRecoveries = workers.reduce((s, w) => s + (w._totalRecoveries || 0), 0);
579
643
  const totalDisconnects = workers.reduce((s, w) => s + (w._disconnectCount || 0), 0);
580
- if (recovCount > 0 || pausedCount > 0 || totalRecoveries > 0 || totalDisconnects > 0) {
581
- lines.push(boxLine(`${dim}${'─'.repeat(tw - 6)}${c.reset}`, tw, accent));
644
+ if (recovCount > 0 || pausedCount > 0 || invalidCount > 0 || totalRecoveries > 0 || totalDisconnects > 0) {
645
+ lines.push(bMid);
582
646
  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));
647
+ if (invalidCount > 0) hParts.push(`${R} ${invalidCount} invalid${c.reset}`);
648
+ if (recovCount > 0) hParts.push(`${O}${getSpinner('braille')} ${recovCount} recovering${c.reset}`);
649
+ if (pausedCount > 0) hParts.push(`${Y}${pausedCount} paused${c.reset}`);
650
+ if (totalRecoveries > 0) hParts.push(`${D}${totalRecoveries} auto-healed${c.reset}`);
651
+ if (totalDisconnects > 0) hParts.push(`${D}${totalDisconnects} reconnects${c.reset}`);
652
+ lines.push(bRow(` ${D}HEALTH${c.reset} ${hParts.join(` ${D}·${c.reset} `)}`));
588
653
  }
589
654
 
590
- // Cluster info
655
+ // Cluster
591
656
  if (CLUSTER_ENABLED) {
592
657
  const nodeShort = NODE_ID.substring(0, 12);
593
658
  const claimedCount = workers.filter(w => w.running).length;
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));
659
+ lines.push(bRow(` ${Cy}CLUSTER${c.reset} ${D}node:${c.reset} ${Cy}${nodeShort}${c.reset} ${D}claimed:${c.reset} ${W}${claimedCount}${c.reset}`));
595
660
  }
596
661
 
597
- // ━━━ LIVE LOG FEED ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
662
+ // ═══════════════════════════════════════════════════════════════
663
+ // LIVE LOG FEED
664
+ // ═══════════════════════════════════════════════════════════════
598
665
  const logEntries = recentLogs.toArray();
599
666
  if (logEntries.length > 0) {
600
- lines.push(midLine);
601
- const logTitle = gradientText(' LIVE FEED ', [139, 92, 246], [52, 211, 153]);
602
- lines.push(boxLine(`${logTitle}`, tw, accent));
667
+ lines.push(bSep);
668
+ lines.push(bRow(` ${gradientText('LIVE FEED', [139, 92, 246], [52, 211, 153])} ${G}${getSpinner('pulse')}${c.reset}`));
669
+ lines.push(bMid);
603
670
  for (const entry of logEntries) {
604
- lines.push(boxLine(`${dim}${entry}${c.reset}`, tw, accent));
671
+ lines.push(bRow(` ${D}${entry}${c.reset}`));
605
672
  }
606
673
  }
607
674
 
608
- // ━━━ FOOTER ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
609
- lines.push(boxBot(tw, accent));
675
+ // ═══════════════════════════════════════════════════════════════
676
+ lines.push(bEmpty);
677
+ lines.push(bBot);
610
678
 
611
- // Render
679
+ // ── Flush to terminal ──
612
680
  process.stdout.write('\x1b[H');
613
681
  for (const line of lines) {
614
682
  process.stdout.write(c.clearLine + '\r' + line + '\n');
@@ -626,9 +694,9 @@ function log(type, msg, label) {
626
694
  };
627
695
  const tagRaw = label ? label.replace(/\x1b\[[0-9;]*m/g, '').substring(0, 12) : '';
628
696
  const stripped = msg.replace(/\x1b\[[0-9;]*m/g, '');
629
- const tw = Math.min(process.stdout.columns || 80, 76);
697
+ const tw = Math.max(process.stdout.columns || 80, 60);
630
698
  if (dashboardStarted) {
631
- const maxLen = tw - 4;
699
+ const maxLen = tw - 8;
632
700
  const entry = `${time} ${icons[type] || '·'} ${tagRaw ? tagRaw + ' ' : ''}${stripped}`;
633
701
  recentLogs.push(entry.substring(0, maxLen));
634
702
  scheduleRender();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "6.6.0",
3
+ "version": "6.8.1",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"