dankgrinder 7.60.0 → 7.63.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.
@@ -4,12 +4,14 @@
4
4
  * Handles sell listings with optional partial sales and privacy settings.
5
5
  *
6
6
  * Dank Memer market post format:
7
- * pls market post for_coins <listing_type> <qty> <item> <price> <days> <allow_partial> <is_private> <partial_min>
7
+ * pls market post for_coins <listing_type> <qty> <item> <price>
8
+ * <allow_partial> <is_private> <partial_min> <days>
8
9
  *
9
10
  * listing_type: "sell" | "auction"
10
11
  * allow_partial: "true" | "false"
11
12
  * is_private: "true" | "false"
12
- * partial_min: only meaningful when allow_partial is true
13
+ * partial_min: integer 1
14
+ * days: 1-7
13
15
  */
14
16
 
15
17
  const {
@@ -48,17 +50,19 @@ async function runMarketPost({
48
50
  return { result: 'no item specified', coins: 0 };
49
51
  }
50
52
 
53
+ // Dank Memer expects: <qty> <item> <price> <allow_partial> <is_private> <days> <partial_min>
54
+ // Note: allow_partial and is_private come BEFORE days (verified via test-market.js)
51
55
  const cmdParts = [
52
56
  'pls', 'market', 'post',
53
- 'for_coins', // price type: "for_coins" fixed price
54
- 'sell', // listing type: "sell" | "auction"
55
- String(quantity), // quantity
56
- itemName, // item name
57
- String(pricePerItem * quantity), // TOTAL price (price per item × quantity)
58
- String(days), // days (1-7)
59
- allowPartial ? 'true' : 'false', // allow partial
60
- isPrivate ? 'true' : 'false', // is private
61
- String(allowPartial ? partialMin : 0), // partial minimum (only meaningful when allowPartial is true)
57
+ 'for_coins', // price type: fixed price
58
+ 'sell', // listing type
59
+ String(quantity), // quantity
60
+ itemName, // item name
61
+ String(pricePerItem * quantity), // TOTAL price
62
+ allowPartial ? 'true' : 'false', // allow partial
63
+ isPrivate ? 'true' : 'false', // is private
64
+ String(Math.max(1, partialMin || 1)), // partial minimum (always ≥1)
65
+ String(days), // days (1-7)
62
66
  ];
63
67
 
64
68
  const cmdString = cmdParts.join(' ');
@@ -84,7 +88,7 @@ async function runMarketPost({
84
88
 
85
89
  // Check for common errors
86
90
  const lowerText = text.toLowerCase();
87
- if (lowerText.includes('you don\'t have') || lowerText.includes('not enough') || lowerText.includes('invalid item') || lowerText.includes('partial minimum can\'t') || lowerText.includes('set allow_partial')) {
91
+ if (lowerText.includes('you don\'t have') || lowerText.includes('not enough') || lowerText.includes('invalid item') || lowerText.includes('partial minimum can\'t') || lowerText.includes('set allow_partial') || lowerText.includes('allow_partial is incorrect') || lowerText.includes('allow_partial')) {
88
92
  return { result: text.substring(0, 80) || 'error listing item', coins: 0 };
89
93
  }
90
94
 
package/lib/grinder.js CHANGED
@@ -375,11 +375,18 @@ function scheduleRender() {
375
375
  }
376
376
  }
377
377
 
378
+ // ══════════════════════════════════════════════════════════════
379
+ // ═ CLI Dashboard — clean, modern, stable rendering
380
+ // ═ Structure: [3-line header] [accounts table] [live feed] [footer]
381
+ // ║ Uses full-width box borders, ANSI cursor positioning,
382
+ // ═ per-row ANSI-aware padding for stable display.
383
+ // ══════════════════════════════════════════════════════════════
378
384
  function renderDashboard() {
379
385
  if (!dashboardStarted || workers.length === 0 || dashboardRendering || shutdownCalled) return;
380
386
  dashboardRendering = true;
381
387
  lastRenderTime = Date.now();
382
388
 
389
+ // ── Aggregate session stats ─────────────────────────────────────
383
390
  totalBalance = 0; totalCoins = 0; totalCommands = 0;
384
391
  let totalErrors = 0;
385
392
  let totalLosses = 0;
@@ -397,183 +404,89 @@ function renderDashboard() {
397
404
  setTimeout(() => { isNewHigh = false; }, 3000);
398
405
  }
399
406
 
400
- // ── Layout: use FULL terminal width ──
401
- const lines = [];
402
407
  const tw = Math.max(process.stdout.columns || 80, 60);
403
- const iw = tw - 4; // inner width (inside box borders)
404
-
405
- // Color shortcuts (local to this function)
406
- const A = rgb(139, 92, 246); // accent purple
407
- const G = rgb(52, 211, 153); // green
408
- const B = rgb(96, 165, 250); // blue
409
- const P = rgb(236, 72, 153); // pink
410
- const Au = rgb(255, 215, 0); // gold
411
- const O = rgb(251, 146, 60); // orange
412
- const Cy = rgb(34, 211, 238); // cyan
413
- const R = rgb(239, 68, 68); // red
414
- const Y = rgb(251, 191, 36); // yellow
408
+ const RE = /\x1b\[[0-9;]*m/g;
409
+ const vis = (s) => String(s).replace(RE, '').length;
410
+ const pad = (content, width) => content + ' '.repeat(Math.max(0, width - vis(content)));
411
+
412
+ // Color shortcuts
413
+ const A = rgb(139, 92, 246); // purple accent
414
+ const G = rgb(52, 211, 153); // green
415
+ const B = rgb(96, 165, 250); // blue
416
+ const Au = rgb(255, 215, 0); // gold
417
+ const O = rgb(251, 146, 60); // orange
418
+ const Cy = rgb(34, 211, 238); // cyan
419
+ const R = rgb(239, 68, 68); // red
420
+ const Y = rgb(251, 191, 36); // yellow
415
421
  const W = c.white;
416
422
  const D = c.dim;
417
- const RE = /\x1b\[[0-9;]*m/g;
418
-
419
- // ── Box drawing (scales to tw) ──
420
- const bTop = A + '╔' + '═'.repeat(tw - 2) + '╗' + c.reset;
421
- const bMid = A + '╟' + '─'.repeat(tw - 2) + '╢' + c.reset;
422
- const bSep = A + '╠' + '═'.repeat(tw - 2) + '╣' + c.reset;
423
- const bBot = A + '╚' + '═'.repeat(tw - 2) + '╝' + c.reset;
424
- const bRow = (content) => {
425
- const vis = content.replace(RE, '').length;
426
- const pad = Math.max(0, iw - vis);
427
- return A + '║' + c.reset + ' ' + content + ' '.repeat(pad) + ' ' + A + '║' + c.reset;
428
- };
429
- const bEmpty = bRow('');
423
+ const _ = c.reset;
430
424
 
431
- // ═══════════════════════════════════════════════════════════════
432
- // HEADER
433
- // ═══════════════════════════════════════════════════════════════
434
- lines.push(bTop);
435
- lines.push(bEmpty);
425
+ // ── Build rows array ──────────────────────────────────────────
426
+ const rows = [];
436
427
 
428
+ // ── HEADER ────────────────────────────────────────────────────
437
429
  const spin = getSpinner('braille');
438
-
439
- // Title — big gradient banner
440
- const titleLines = [
441
- '██████╗ █████╗ ███╗ ██╗██╗ ██╗ ██████╗ ██████╗ ██╗███╗ ██╗██████╗ ███████╗██████╗',
442
- '██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝ ██╔════╝ ██╔══██╗██║████╗ ██║██╔══██╗██╔════╝██╔══██╗',
443
- '██║ ██║███████║██╔██╗ ██║█████╔╝ ██║ ███╗██████╔╝██║██╔██╗ ██║██║ ██║█████╗ ██████╔╝',
444
- '██║ ██║██╔══██║██║╚██╗██║██╔═██╗ ██║ ██║██╔══██╗██║██║╚██╗██║██║ ██║██╔══╝ ██╔══██╗',
445
- '██████╔╝██║ ██║██║ ╚████║██║ ██╗ ╚██████╔╝██║ ██║██║██║ ╚████║██████╔╝███████╗██║ ██║',
446
- '╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝',
447
- ];
448
- // Check terminal width — fall back to compact title if too narrow
449
- const termW = (process.stdout.columns || 120) - 6; // account for box borders
450
- const useBigTitle = termW >= 92;
451
- if (useBigTitle) {
452
- for (let i = 0; i < titleLines.length; i++) {
453
- const t = i / (titleLines.length - 1);
454
- const from = t < 0.5
455
- ? [lerp(192, 139, t * 2), lerp(132, 92, t * 2), lerp(252, 246, t * 2)]
456
- : [lerp(139, 34, (t - 0.5) * 2), lerp(92, 211, (t - 0.5) * 2), lerp(246, 238, (t - 0.5) * 2)];
457
- lines.push(bRow(` ${c.bold}${gradientLine(titleLines[i], from, [52, 211, 153])}${c.reset}`));
458
- }
459
- } else {
460
- const titleGrad = gradientText(' D A N K G R I N D E R ', [192, 132, 252], [52, 211, 153]);
461
- lines.push(bRow(` ${c.bold}${titleGrad}${c.reset} ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset}`));
462
- }
463
-
464
- lines.push(bRow(` ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset} ${Y}◷${c.reset} ${D}UP${c.reset} ${c.bold}${Y}${formatUptime()}${c.reset}`));
465
-
466
- // Subtitle info
467
430
  const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
468
431
  const invalidCount = workers.filter(w => w._tokenInvalid).length;
469
432
  const pausedCount = workers.filter(w => w.paused || w.dashboardPaused).length;
470
433
  const recovCount = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
471
- const mode = CLOUD_MODE ? `${Cy}CLOUD${c.reset}` : (CLUSTER_ENABLED ? `${Cy}CLUSTER${c.reset}` : `${D}Standalone${c.reset}`);
472
- const cmdCount = AccountWorker.COMMAND_MAP.length;
473
-
474
434
  const netQ = workers.length > 0
475
435
  ? workers.reduce((s, w) => s + (w._lastPing < 200 ? 1 : w._lastPing < 500 ? 0.5 : 0), 0) / workers.length : 1;
476
- const netDot = netQ > 0.8 ? `${G}●${c.reset}` : netQ > 0.5 ? `${Y}●${c.reset}` : `${R}●${c.reset}`;
477
- const netLbl = netQ > 0.8 ? `${G}Good${c.reset}` : netQ > 0.5 ? `${Y}Fair${c.reset}` : `${R}Poor${c.reset}`;
478
-
479
- const infoParts = [
480
- mode,
481
- `${netDot} ${netLbl}`,
482
- `${B}${cmdCount}${c.reset} ${D}commands${c.reset}`,
483
- `${G}${activeCount}${c.reset}${D}/${c.reset}${W}${workers.length}${c.reset} ${D}active${c.reset}`,
484
- ];
485
- if (invalidCount > 0) infoParts.push(`${R}${invalidCount} invalid${c.reset}`);
486
- if (pausedCount > 0) infoParts.push(`${Y}${pausedCount} paused${c.reset}`);
487
- if (recovCount > 0) infoParts.push(`${O}${recovCount} recovering${c.reset}`);
488
- lines.push(bRow(` ${infoParts.join(` ${D}·${c.reset} `)}`));
489
-
490
- lines.push(bEmpty);
491
-
492
- // ═══════════════════════════════════════════════════════════════
493
- // STATS PANEL — left: all metrics | right: big trend + rate
494
- // ═══════════════════════════════════════════════════════════════
495
- lines.push(bSep);
496
- lines.push(bEmpty);
497
-
498
- const now = Date.now();
499
- if (now - lastEarningsSample > 8000) { earningsHistory.push(totalCoins); lastEarningsSample = now; }
436
+ const netDot = netQ > 0.8 ? `${G}●${_}` : netQ > 0.5 ? `${Y}●${_}` : `${R}●${_}`;
437
+ const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
500
438
  const elapsedHrs = (Date.now() - startTime) / 3_600_000;
501
439
  const perHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
502
-
503
- // ── Compute metric values ─────────────────────────────────────
504
440
  const cpmVal = globalCmdRate.getRate().toFixed(1);
505
- const srColor = successRate >= 95 ? G : successRate >= 80 ? Y : R;
506
- const srBar = progressBar(successRate, 100, 10, successRate >= 95 ? [52, 211, 153] : successRate >= 80 ? [251, 191, 36] : [239, 68, 68]);
507
- const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
508
- const memCol = memMB > 900 ? [239, 68, 68] : memMB > 600 ? [251, 191, 36] : [52, 211, 153];
509
- const memBar = progressBar(memMB, 1024, 10, memCol, [40, 40, 55]);
510
- const perHrColor = perHr >= 0 ? G : R;
511
- const perHrSign = perHr >= 0 ? '+' : '';
512
- const newHighFlag = isNewHigh ? ` ${R}${c.bold}★ NEW HIGH ★${c.reset}` : '';
513
-
514
- // ── Big trend sparkline (takes ~40% of inner width) ─────────
515
- const sparkW = Math.max(28, Math.floor(iw * 0.4));
516
- const spark = drawSparkline(earningsHistory.toArray(), sparkW);
517
- const sparkLabel = `${A}~${c.reset} ${D}TREND${c.reset}`;
518
-
519
- // ── Left metric rows (each left-aligned, ANSI-aware padding) ─
520
- // Helper: ANSI-strip-aware pad — strip ANSI then pad the visible content
521
- const padRow = (content, totalVis) => {
522
- const vis = content.replace(RE, '').length;
523
- return content + ' '.repeat(Math.max(0, totalVis - vis));
524
- };
441
+ const now = Date.now();
442
+ if (now - lastEarningsSample > 8000) { earningsHistory.push(totalCoins); lastEarningsSample = now; }
525
443
 
526
- const leftTotal = iw - sparkW - 10; // reserve space for spark + gap
527
- const lRow1 = `${Au}⟐${c.reset} ${D}BALANCE${c.reset} ${c.bold}${Au}⏣${c.reset} ${formatCoins(totalBalance)}`;
528
- const lRow2 = `${G}▲${c.reset} ${D}EARNED${c.reset} ${c.bold}${G}${perHrSign}⏣${c.reset} ${formatCoins(totalCoins)}${newHighFlag}`;
529
- const lRow3 = `${O}★${c.reset} ${D}PEAK${c.reset} ${c.bold}${O}⏣${c.reset} ${formatCoins(sessionPeakCoins)}`;
530
- 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}`;
531
- const lRow5 = `${D}≡${c.reset} ${D}MEM${c.reset} ${rgb(memCol[0], memCol[1], memCol[2])}${c.bold}${memMB}MB${c.reset} ${memBar}`;
532
-
533
- // Build right column label
534
- const rRate = `${perHrColor}${perHrSign}⏣${c.reset} ${formatCoins(Math.abs(perHr))}/h`;
535
-
536
- lines.push(bRow(` ${padRow(lRow1, leftTotal)} ${sparkLabel} ${spark}`));
537
- lines.push(bRow(` ${padRow(lRow2, leftTotal)} ${D}────────${c.reset} ${c.dim}earned${c.reset}`));
538
- lines.push(bRow(` ${padRow(lRow3, leftTotal)} ${D} ${c.reset} ${rRate}`));
539
- lines.push(bRow(` ${padRow(lRow4, leftTotal)} ${D} ${c.reset}`));
540
- lines.push(bRow(` ${padRow(lRow5, leftTotal)} ${D} ${c.reset}`));
541
-
542
- lines.push(bEmpty);
543
-
544
- // ═══════════════════════════════════════════════════════════════
545
- // ACCOUNTS TABLE (sorted by original index, proper alignment)
546
- // ═══════════════════════════════════════════════════════════════
547
- lines.push(bSep);
548
-
549
- // Column widths scale with terminal
550
- const colNum = 4; // " 1 "
551
- const colSts = 3; // ""
552
- const colMedal = 4; // " 1st" or " "
553
- const colBal = 12; // "⏣ 999.9M "
554
- const colEarn = 10; // "+999.9K "
555
- const colBar = 8; // "████░░░░"
556
- const fixedW = colNum + colSts + colMedal + colBal + colEarn + colBar + 16; // 16 for spacing
557
- const colName = Math.max(10, Math.min(22, Math.floor((iw - fixedW) * 0.45)));
558
- const colActivity = Math.max(10, iw - fixedW - colName);
559
-
560
- // Header
561
- const hNum = `${D}${'#'.padStart(colNum)}${c.reset}`;
562
- const hSts = `${D}${'ST'.padEnd(colSts)}${c.reset}`;
563
- const hMedal = `${D}${'RNK'.padEnd(colMedal)}${c.reset}`;
564
- const hName = `${gradientText('Account', [139, 92, 246], [96, 165, 250])}${''.padEnd(Math.max(0, colName - 7))}`;
565
- const hBal = `${D}${'Balance'.padEnd(colBal)}${c.reset}`;
566
- const hEarn = `${D}${'Earned'.padEnd(colEarn)}${c.reset}`;
567
- const hBar = `${D}${''.padEnd(colBar)}${c.reset}`;
568
- const hAct = `${D}Activity${c.reset}`;
569
-
570
- lines.push(bRow(` ${hNum} ${hSts} ${hMedal} ${hName} ${hBal} ${hEarn} ${hBar} ${hAct}`));
571
- lines.push(bRow(` ${D}${'─'.repeat(iw - 2)}${c.reset}`));
572
-
573
- // Sort workers by original index for consistent display
574
- const sortedWorkers = [...workers].sort((a, b) => a.idx - b.idx);
444
+ const topLine = `${A}┌${'─'.repeat(tw - 2)}┐${_}`;
445
+ rows.push(topLine);
446
+
447
+ const title = `${c.bold}${gradientText(' DANK GRINDER ', [139, 92, 246], [52, 211, 153])}${_}`;
448
+ const modeLabel = CLOUD_MODE ? `${Cy}CLOUD${_}` : (CLUSTER_ENABLED ? `${Cy}CLUSTER${_}` : `${D}local${_}`);
449
+ const row1Content = `${title} ${D}v${PKG_VERSION}${_} ${G}${spin}${_}` +
450
+ ` ${D}up${_} ${Y}${formatUptime()}${_}` +
451
+ ` ${netDot} ${D}net${_}` +
452
+ ` ${Au}⏣${_}${W}${formatCoins(totalBalance)}${_} ${D}bal${_}` +
453
+ ` ${G}${perHr >= 0 ? '+' : ''}⏣${formatCoins(perHr)}${_}/h${_}` +
454
+ ` ${G}${activeCount}${_}/${W}${workers.length}${_} ${D}active${_}` +
455
+ (invalidCount > 0 ? ` ${R}${invalidCount} invalid${_}` : '') +
456
+ (pausedCount > 0 ? ` ${Y}${pausedCount} paused${_}` : '') +
457
+ (recovCount > 0 ? ` ${O}${recovCount} recov${_}` : '') +
458
+ ` ${D}${totalCommands}${_} cmds` +
459
+ ` ${D}mem ${memMB}MB${_}`;
460
+ rows.push(`${A}│${_} ${pad(row1Content, tw - 4)} ${A}│${_}`);
461
+ rows.push(`${A}└${'─'.repeat(tw - 2)}┘${_}`);
462
+
463
+ // ── ACCOUNTS TABLE ────────────────────────────────────────────
464
+ // Columns: #, S, Name, Balance, Lvl, LS, Earned, Activity
465
+ const colNum = 3; // "#1"
466
+ const colSts = 2; // "●"
467
+ const colName = Math.max(14, Math.min(20, Math.floor(tw * 0.22)));
468
+ const colBal = 9; // "1.2M"
469
+ const colLvl = 5; // "L273"
470
+ const colLs = 4; // "♥37"
471
+ const colEarn = 8; // "+500K"
472
+ const colAct = Math.max(6, tw - colNum - colSts - colName - colBal - colLvl - colLs - colEarn - 16);
473
+ const gap = 2;
474
+ const colGap = ' '.repeat(gap);
475
+
476
+ const hNum = `${D}${pad('#', colNum)}${_}`;
477
+ const hSts = `${D}${pad('S', colSts)}${_}`;
478
+ const hName = `${gradientText(pad('Account', colName), [139, 92, 246], [96, 165, 250])}${_}`;
479
+ const hBal = `${D}${pad('Balance', colBal)}${_}`;
480
+ const hLvl = `${D}${pad('Lvl', colLvl)}${_}`;
481
+ const hLs = `${D}${pad('LS', colLs)}${_}`;
482
+ const hEarn = `${D}${pad('Earned', colEarn)}${_}`;
483
+ const hAct = `${D}${pad('Activity', colAct)}${_}`;
484
+
485
+ rows.push(`${A}┌${''.repeat(tw - 2)}┐${_}`);
486
+ rows.push(`${A}│${_} ${hNum}${colGap}${hSts}${colGap}${hName}${colGap}${hBal}${colGap}${hLvl}${colGap}${hLs}${colGap}${hEarn}${colGap}${hAct} ${A}│${_}`);
487
+ rows.push(`${A}├${'─'.repeat(tw - 2)}┤${_}`);
575
488
 
576
- // Top 3 earners
489
+ const sortedWorkers = [...workers].sort((a, b) => a.idx - b.idx);
577
490
  const topEarners = [...workers]
578
491
  .filter(w => (w.stats.coins || 0) > 0)
579
492
  .sort((a, b) => (b.stats.coins || 0) - (a.stats.coins || 0))
@@ -581,166 +494,90 @@ function renderDashboard() {
581
494
  const topIds = new Map();
582
495
  topEarners.forEach((w, i) => topIds.set(w.account.id, i));
583
496
 
584
- const MAX_VIS = 30;
585
- const visibleWorkers = sortedWorkers.slice(0, MAX_VIS);
497
+ // Adapt row count to available terminal height
498
+ const maxRows = Math.max(4, Math.min(sortedWorkers.length, Math.floor((process.stdout.rows || 24) - 14)));
499
+ const visibleWorkers = sortedWorkers.slice(0, maxRows);
586
500
 
587
501
  for (const wk of visibleWorkers) {
588
- const origNum = (wk.idx + 1).toString().padStart(colNum);
589
- const rawStat = (wk.lastStatus || 'ready').replace(RE, '');
590
- const activityText = rawStat.substring(0, colActivity);
591
-
592
- // ── Status icon ──
593
- let stsIcon, actLabel;
594
502
  const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
595
- if (wk._tokenInvalid) {
596
- stsIcon = `${R}✗${c.reset}`;
597
- actLabel = `${R}TOKEN INVALID${c.reset}`;
598
- } else if (!wk.running) {
599
- stsIcon = `${D}○${c.reset}`;
600
- actLabel = `${D}offline${c.reset}`;
601
- } else if (isRecov) {
602
- const sL = Math.ceil((wk._errorCooldownUntil - Date.now()) / 1000);
603
- stsIcon = `${O}${getSpinner('braille')}${c.reset}`;
604
- actLabel = `${O}recovering (${sL}s)${c.reset}`;
605
- } else if (wk.paused) {
606
- stsIcon = `${R}⏸${c.reset}`;
607
- actLabel = `${R}PAUSED${c.reset}`;
608
- } else if (wk.dashboardPaused) {
609
- stsIcon = `${Y}⏸${c.reset}`;
610
- actLabel = `${Y}paused${c.reset}`;
611
- } else if (wk.busy) {
612
- stsIcon = `${G}${getSpinner('pulse')}${c.reset}`;
613
- actLabel = `${D}${activityText}${c.reset}`;
614
- } else {
615
- stsIcon = `${G}●${c.reset}`;
616
- actLabel = `${D}${activityText}${c.reset}`;
617
- }
618
-
619
- // ── Medal (fixed 3-char visible width + 1 space) ──
620
- let medalStr;
621
- if (topIds.has(wk.account.id)) {
622
- const rank = topIds.get(wk.account.id);
623
- if (rank === 0) medalStr = `${Au}1st${c.reset} `;
624
- else if (rank === 1) medalStr = `${rgb(192, 192, 192)}2nd${c.reset} `;
625
- else medalStr = `${rgb(205, 127, 50)}3rd${c.reset} `;
626
- } else {
627
- medalStr = ' '; // 4 chars: 3 empty + 1 space
628
- }
503
+ let stsIcon;
504
+ if (wk._tokenInvalid) stsIcon = `${R}✗${_}`;
505
+ else if (!wk.running) stsIcon = `${D}○${_}`;
506
+ else if (isRecov) stsIcon = `${O}${getSpinner('braille').substring(0, 1)}${_}`;
507
+ else if (wk.paused) stsIcon = `${R}⏸${_}`;
508
+ else if (wk.dashboardPaused) stsIcon = `${Y}⏸${_}`;
509
+ else if (wk.busy) stsIcon = `${G}${getSpinner('pulse').substring(0, 1)}${_}`;
510
+ else stsIcon = `${G}●${_}`;
629
511
 
630
- // ── Name (fixed visible width, padded) ──
631
512
  const rawName = (wk.username || wk.account.label || '?').substring(0, colName);
632
- const nameStr = `${wk.color}${c.bold}${rawName.padEnd(colName)}${c.reset}`;
633
-
634
- // ── Balance (fixed visible width) ──
635
- let balStr;
636
- if (wk.stats.balance > 0) {
637
- balStr = `${Au}⏣${c.reset}${W}${formatCoins(wk.stats.balance).padStart(colBal - 2)}${c.reset}`;
638
- } else {
639
- balStr = `${D}⏣${' '.repeat(colBal - 3)}-${c.reset}`;
640
- }
641
-
642
- // ── Lifesaver indicator ──
513
+ const nameStr = `${wk.color}${rawName}${_}`;
514
+ const balStr = wk.stats.balance > 0 ? `${Au}⏣${_}${W}${formatCoins(wk.stats.balance)}${_}` : `${D}⏣-${_}`;
515
+ const lvl = wk._level || 0;
516
+ const lvlStr = lvl > 0 ? `${Cy}L${lvl}${_}` : `${D}L???${_}`;
643
517
  const ls = wk._lifesavers;
644
518
  let lsStr;
645
- if (ls === 0) lsStr = `${R}♥0${c.reset}`;
646
- else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${c.reset}`;
647
- else if (ls != null) lsStr = `${G}♥${ls}${c.reset}`;
519
+ if (ls === 0) lsStr = `${R}♥${ls}${_}`;
520
+ else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${_}`;
521
+ else if (ls != null) lsStr = `${G}♥${ls}${_}`;
648
522
  else {
649
- // Unknown — pulse to show it's still being determined
650
523
  const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
651
- lsStr = `${D}${pulse}♥?${c.reset}`;
524
+ lsStr = `${D}${pulse}♥?${_}`;
652
525
  }
526
+ const earnNum = wk.stats.coins || 0;
527
+ const earnStr = earnNum > 0 ? `${G}+${formatCoins(earnNum)}${_}` : `${D}────${_}`;
528
+ const rawAct = (wk.lastStatus || 'ready').replace(RE, '').substring(0, colAct);
529
+ const actStr = `${D}${pad(rawAct, colAct)}${_}`;
530
+ const numStr = `${D}${pad(String(wk.idx + 1), colNum)}${_}`;
653
531
 
654
- // ── Level indicator (fixed width so value changes don't jitter) ──
655
- const lvl = wk._level || 0;
656
- const lvlStr = lvl > 0 ? `${Cy}L${String(lvl).padStart(3)}${c.reset}` : `${D}L???${c.reset}`;
532
+ rows.push(`${A}│${_} ${numStr}${colGap}${pad(stsIcon, colSts)}${colGap}${pad(nameStr, colName)}${colGap}${pad(balStr, colBal)}${colGap}${pad(lvlStr, colLvl)}${colGap}${pad(lsStr, colLs)}${colGap}${pad(earnStr, colEarn)}${colGap}${actStr} ${A}│${_}`);
533
+ }
657
534
 
658
- // ── Earned (fixed visible width) ──
659
- const earnNum = wk.stats.coins || 0;
660
- let earnStr;
661
- if (earnNum > 0) {
662
- earnStr = `${G}+${formatCoins(earnNum).padEnd(colEarn - 1)}${c.reset}`;
663
- } else if (earnNum < 0) {
664
- earnStr = `${R}${formatCoins(earnNum).padEnd(colEarn)}${c.reset}`;
665
- } else {
666
- earnStr = `${D}${'-'.padEnd(colEarn)}${c.reset}`;
667
- }
668
-
669
- // ── Progress bar (fixed width) ──
670
- const earnBarFill = earnNum > 0 ? Math.min(colBar, Math.max(1, Math.floor(Math.log10(earnNum + 1)))) : 0;
671
- const earnBar = progressBar(earnBarFill, colBar, colBar, [52, 211, 153], [40, 40, 55]);
672
-
673
- lines.push(bRow(` ${D}${origNum}${c.reset} ${stsIcon} ${medalStr}${nameStr} ${balStr} ${lvlStr} ${lsStr} ${earnStr} ${earnBar} ${actLabel}`));
674
- }
675
-
676
- // Overflow summary
677
- if (sortedWorkers.length > MAX_VIS) {
678
- const rest = sortedWorkers.length - MAX_VIS;
679
- let ha = 0, hp = 0, hr = 0, ho = 0, hi = 0;
680
- for (let i = MAX_VIS; i < sortedWorkers.length; i++) {
681
- const w = sortedWorkers[i];
682
- if (w._tokenInvalid) hi++;
683
- else if (!w.running) ho++;
684
- else if (w.paused || w.dashboardPaused) hp++;
685
- else if (w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()) hr++;
686
- else ha++;
687
- }
688
- const parts = [`${D}+${rest} more${c.reset}`];
689
- if (ha > 0) parts.push(`${G}${ha} active${c.reset}`);
690
- if (hp > 0) parts.push(`${Y}${hp} paused${c.reset}`);
691
- if (hr > 0) parts.push(`${O}${hr} recovering${c.reset}`);
692
- if (hi > 0) parts.push(`${R}${hi} invalid${c.reset}`);
693
- if (ho > 0) parts.push(`${D}${ho} offline${c.reset}`);
694
- lines.push(bRow(` ${parts.join(` ${D}·${c.reset} `)}`));
695
- }
696
-
697
- // ═══════════════════════════════════════════════════════════════
698
- // HEALTH BAR
699
- // ═══════════════════════════════════════════════════════════════
700
- const totalRecoveries = workers.reduce((s, w) => s + (w._totalRecoveries || 0), 0);
701
- const totalDisconnects = workers.reduce((s, w) => s + (w._disconnectCount || 0), 0);
702
- if (recovCount > 0 || pausedCount > 0 || invalidCount > 0 || totalRecoveries > 0 || totalDisconnects > 0) {
703
- lines.push(bMid);
704
- const hParts = [];
705
- if (invalidCount > 0) hParts.push(`${R}✗ ${invalidCount} invalid${c.reset}`);
706
- if (recovCount > 0) hParts.push(`${O}${getSpinner('braille')} ${recovCount} recovering${c.reset}`);
707
- if (pausedCount > 0) hParts.push(`${Y}⏸ ${pausedCount} paused${c.reset}`);
708
- if (totalRecoveries > 0) hParts.push(`${D}${totalRecoveries} auto-healed${c.reset}`);
709
- if (totalDisconnects > 0) hParts.push(`${D}${totalDisconnects} reconnects${c.reset}`);
710
- lines.push(bRow(` ${D}HEALTH${c.reset} ${hParts.join(` ${D}·${c.reset} `)}`));
711
- }
712
-
713
- // Cluster
714
- if (CLUSTER_ENABLED) {
715
- const nodeShort = NODE_ID.substring(0, 12);
716
- const claimedCount = workers.filter(w => w.running).length;
717
- lines.push(bRow(` ${Cy}CLUSTER${c.reset} ${D}node:${c.reset} ${Cy}${nodeShort}${c.reset} ${D}claimed:${c.reset} ${W}${claimedCount}${c.reset}`));
535
+ if (sortedWorkers.length > maxRows) {
536
+ const rest = sortedWorkers.length - maxRows;
537
+ rows.push(`${A}│${_} ${D}+${rest} more accounts${_}${' '.repeat(Math.max(0, tw - 22 - rest.toString().length))}${A}│${_}`);
718
538
  }
539
+ rows.push(`${A}└${'─'.repeat(tw - 2)}┘${_}`);
719
540
 
720
- // ═══════════════════════════════════════════════════════════════
721
- // LIVE LOG FEED
722
- // ═══════════════════════════════════════════════════════════════
541
+ // ── LIVE FEED ─────────────────────────────────────────────────
723
542
  const logEntries = recentLogs.toArray();
724
543
  if (logEntries.length > 0) {
725
- lines.push(bSep);
726
- lines.push(bRow(` ${gradientText('LIVE FEED', [139, 92, 246], [52, 211, 153])} ${G}${getSpinner('pulse')}${c.reset}`));
727
- lines.push(bMid);
544
+ rows.push(`${A}┌${'─'.repeat(tw - 2)}┐${_}`);
545
+ rows.push(`${A}│${_} ${gradientText(' LIVE FEED ', [139, 92, 246], [52, 211, 153])}${_} ${G}${getSpinner('pulse')}${_}${' '.repeat(Math.max(0, tw - 22))}${A}│${_}`);
546
+ rows.push(`${A}├${'─'.repeat(tw - 2)}┤${_}`);
728
547
  for (const entry of logEntries) {
729
- lines.push(bRow(` ${D}${entry}${c.reset}`));
548
+ let lineText;
549
+ if (typeof entry === 'string') {
550
+ lineText = entry;
551
+ } else if (entry && typeof entry === 'object') {
552
+ const ts = entry.ts ? new Date(entry.ts).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
553
+ const user = entry.username ? String(entry.username) : '';
554
+ const cmd = entry.command ? `[${entry.command}]` : '';
555
+ const resp = entry.response || '';
556
+ lineText = `${D}${ts}${_} ${entry.color || D}${user}${_} ${D}${cmd}${_} ${resp}`;
557
+ } else {
558
+ lineText = String(entry);
559
+ }
560
+ rows.push(`${A}│${_} ${D}${pad(lineText, tw - 4)}${_} ${A}│${_}`);
730
561
  }
562
+ rows.push(`${A}└${'─'.repeat(tw - 2)}┘${_}`);
731
563
  }
732
564
 
733
- // ═══════════════════════════════════════════════════════════════
734
- lines.push(bEmpty);
735
- lines.push(bBot);
565
+ // ── FOOTER ────────────────────────────────────────────────────
566
+ rows.push(`${A}┌${'─'.repeat(tw - 2)}┐${_}`);
567
+ rows.push(`${A}│${_} ${modeLabel} ${D}P=pause R=resume S=status Q=quit${_}${' '.repeat(Math.max(0, tw - 48))}${A}│${_}`);
568
+ rows.push(`${A}└${'─'.repeat(tw - 2)}┘${_}`);
736
569
 
737
- // ── Flush to terminal ──
570
+ // ── Flush ─────────────────────────────────────────────────────
738
571
  process.stdout.write('\x1b[H');
739
- for (const line of lines) {
740
- process.stdout.write(c.clearLine + '\r' + line + '\n');
572
+ for (const row of rows) {
573
+ process.stdout.write(c.clearLine + '\r' + row + '\n');
574
+ }
575
+ // Erase any leftover lines from previous render
576
+ const clearDown = Math.max(0, dashboardLines - rows.length);
577
+ if (clearDown > 0) {
578
+ process.stdout.write(`\x1b[${clearDown}B\x1b[2K`);
741
579
  }
742
- process.stdout.write('\x1b[J');
743
- dashboardLines = lines.length;
580
+ dashboardLines = rows.length;
744
581
  dashboardRendering = false;
745
582
  }
746
583
 
@@ -1816,7 +1653,8 @@ class AccountWorker {
1816
1653
  if (currentLevel > 0) {
1817
1654
  await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
1818
1655
  this._level = currentLevel;
1819
- this.log('info', `DM level: ${c.bold}${currentLevel}${c.reset}`);
1656
+ // Only log to terminal during startup — after dashboardStarted, go to live feed
1657
+ if (dashboardStarted) this.log('info', `DM level: ${c.bold}${currentLevel}${c.reset}`);
1820
1658
  }
1821
1659
  if (lastLifesaverCount >= 0) {
1822
1660
  await redis.set(`dkg:lifesavers:${this.account.id}`, String(lastLifesaverCount), 'EX', 86400);
@@ -1824,7 +1662,7 @@ class AccountWorker {
1824
1662
  if (lastLifesaverCount === 0) {
1825
1663
  await redis.set(`raw:alert:no-lifesaver:${dm.id}`, '1', 'EX', 86400);
1826
1664
  await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
1827
- this.log('error', `${c.red}0 LIFESAVERS! Crime/Search will be disabled.${c.reset}`);
1665
+ if (dashboardStarted) this.log('error', `${c.red}0 LIFESAVERS! Crime/Search will be disabled.${c.reset}`);
1828
1666
  }
1829
1667
  }
1830
1668
  }
@@ -1837,7 +1675,7 @@ class AccountWorker {
1837
1675
  }
1838
1676
  }
1839
1677
  }
1840
- this.log('debug', `DM check failed after ${maxRetries} attempts: ${lastError.message}`);
1678
+ if (dashboardStarted) this.log('debug', `DM check failed after ${maxRetries} attempts: ${lastError.message}`);
1841
1679
  return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
1842
1680
  }
1843
1681
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "7.60.0",
3
+ "version": "7.63.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"