dankgrinder 8.36.0 → 8.38.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 (3) hide show
  1. package/lib/grinder.js +26 -23
  2. package/lib/ui.js +156 -141
  3. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -2750,10 +2750,12 @@ async function start(apiKey, apiUrl, opts = {}) {
2750
2750
  const prev = w._lifesavers;
2751
2751
  w._lifesavers = event.lifesaversLeft;
2752
2752
  if (event.lifesaversLeft === 0) {
2753
- w.log?.('error', `DEATH in DMs! 0 lifesavers — disabling crime/search`);
2753
+ w.lastStatus = 'DEAD';
2754
+ w._alert = { type: 'death' };
2754
2755
  w.setCooldown?.('crime', 86400);
2755
2756
  w.setCooldown?.('search', 86400);
2756
2757
  sendWebhook?.('DEATH ALERT (DM)', `**${w.username}** died in DMs! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
2758
+ ui.log(`${c.red}E${c.reset} DEATH — ${w.username} has 0 lifesavers! Crime/search disabled`);
2757
2759
  } else {
2758
2760
  w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
2759
2761
  if (prev !== event.lifesaversLeft) {
@@ -2761,7 +2763,9 @@ async function start(apiKey, apiUrl, opts = {}) {
2761
2763
  w.setCooldown?.('search', 60);
2762
2764
  }
2763
2765
  if (event.lifesaversLeft <= 2) {
2766
+ w._alert = { type: 'lowls' };
2764
2767
  sendWebhook?.('LOW LIFESAVERS', `**${w.username}** has only **${event.lifesaversLeft}** lifesaver(s) left!`, 0xfbbf24);
2768
+ ui.log(`${c.yellow}!${c.reset} ${w.username} — only ${event.lifesaversLeft} lifesavers!`);
2765
2769
  }
2766
2770
  }
2767
2771
  }
@@ -2770,6 +2774,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2770
2774
  if (event.type === 'levelup') {
2771
2775
  if (event.to > 0) {
2772
2776
  w._level = event.to;
2777
+ ui.log(`${c.blue}↑${c.reset} ${w.username} leveled up to ${event.to}`);
2773
2778
  }
2774
2779
  }
2775
2780
  }
@@ -2783,8 +2788,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2783
2788
  const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
2784
2789
  const randomLoginGap = () => LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS ? LOGIN_GAP_MIN_MS : LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
2785
2790
 
2786
- ui.start(); // show dashboard before login
2787
- ui.printLogin(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
2791
+ ui.log(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
2788
2792
  const BATCH_SIZE = 10;
2789
2793
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
2790
2794
  if (shutdownCalled) break;
@@ -2799,21 +2803,20 @@ async function start(apiKey, apiUrl, opts = {}) {
2799
2803
  await worker.start();
2800
2804
  if (worker._tokenInvalid) {
2801
2805
  worker.lastStatus = 'invalid token';
2802
- ui.printLogin(` ${c.red}✗${c.reset} [${i + idx + 1}] ${acc.label || acc.id}: invalid token`);
2806
+ ui.log(`${c.red}E${c.reset} [${i + idx + 1}] ${acc.label || acc.id} invalid token`);
2803
2807
  } else if (worker.channel) {
2804
2808
  worker.lastStatus = 'ready';
2805
- ui.printLogin(` ${c.green}✓${c.reset} [${i + idx + 1}] ${worker.username}`);
2806
- ui.render();
2809
+ ui.log(`${c.green}·${c.reset} [${i + idx + 1}] ${worker.username} connected`);
2807
2810
  } else {
2808
2811
  worker.lastStatus = 'timeout';
2809
- ui.printLogin(` ${c.yellow}✗${c.reset} [${i + idx + 1}] ${acc.label || acc.id}: timeout`);
2812
+ ui.log(`${c.yellow}·${c.reset} [${i + idx + 1}] ${acc.label || acc.id} timeout`);
2810
2813
  }
2811
2814
  } catch (e) {
2812
- ui.printLogin(` ${c.red}✗${c.reset} [${i + idx + 1}] ERROR: ${e.message}`);
2815
+ ui.log(`${c.red}E${c.reset} [${i + idx + 1}] ERROR: ${e.message}`);
2813
2816
  }
2814
2817
  }));
2815
2818
  } catch (e) {
2816
- ui.printLogin(`${c.red}BATCH ERROR: ${e.message}${c.reset}`);
2819
+ ui.log(`${c.red}!${c.reset} BATCH ERROR: ${e.message}`);
2817
2820
  }
2818
2821
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
2819
2822
  hintGC();
@@ -2822,32 +2825,32 @@ async function start(apiKey, apiUrl, opts = {}) {
2822
2825
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
2823
2826
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
2824
2827
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
2825
- ui.printLogin(`${c.green}Login complete: ${loginDone}/${accounts.length} connected${c.reset}`);
2828
+ ui.log(`${c.green}·${c.reset} Login: ${loginDone}/${accounts.length} connected`);
2826
2829
  if (invalidWorkers.length > 0) {
2827
- for (const w of invalidWorkers) ui.printLogin(` ${c.red}FAIL${c.reset} invalid token: ${w.account.label || w.account.id}`);
2830
+ for (const w of invalidWorkers) ui.log(`${c.red}E${c.reset} invalid token: ${w.account.label || w.account.id}`);
2828
2831
  }
2829
2832
  if (timedOutWorkers.length > 0) {
2830
- ui.printLogin(` ${c.yellow}WARN${c.reset} ${timedOutWorkers.length} timed out (will retry in background)`);
2833
+ ui.log(`${c.yellow}~${c.reset} ${timedOutWorkers.length} timed out (will retry in background)`);
2831
2834
  }
2832
- ui.clearLoginLines();
2835
+ ui.draw();
2833
2836
 
2834
2837
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
2835
2838
 
2836
2839
  // ── Phase 2: Inventory check ────────────────────────────────────
2837
- ui.printLogin(`${c.dim}Checking inventory...${c.reset}`);
2840
+ ui.log(`${c.dim}Checking inventory...${c.reset}`);
2838
2841
  let invFailed = 0;
2839
2842
  await Promise.all(activeWorkers.map(async (w) => {
2840
2843
  try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
2841
2844
  catch { invFailed++; }
2842
2845
  }));
2843
2846
  if (invFailed > 0) {
2844
- ui.printLogin(`${c.red}Inventory failed for ${invFailed} accounts${c.reset}`);
2847
+ ui.log(`${c.red}!${c.reset} Inventory failed for ${invFailed} accounts`);
2845
2848
  } else {
2846
- ui.printLogin(`${c.green}Inventory OK${c.reset}`);
2849
+ ui.log(`${c.green}·${c.reset} Inventory OK`);
2847
2850
  }
2848
2851
 
2849
2852
  // ── Phase 2.5: Balance check ───────────────────────────────────
2850
- ui.printLogin(`${c.dim}Checking balances...${c.reset}`);
2853
+ ui.log(`${c.dim}Checking balances...${c.reset}`);
2851
2854
  for (const w of activeWorkers) {
2852
2855
  try { await w.checkBalance(true); } catch {}
2853
2856
  }
@@ -2856,10 +2859,10 @@ async function start(apiKey, apiUrl, opts = {}) {
2856
2859
  totalCoins += w.stats?.balance || 0;
2857
2860
  totalCoins += w.stats?.bankBalance || 0;
2858
2861
  }
2859
- ui.printLogin(`Balances: total ${c.green}+⏣${totalCoins.toLocaleString()}${c.reset} across ${activeWorkers.length} accounts`);
2862
+ ui.log(`${c.blue}$${c.reset} Balances: ${c.green}+⏣${totalCoins.toLocaleString()}${c.reset} across ${activeWorkers.length} accounts`);
2860
2863
 
2861
2864
  // ── Phase 2.75: DM history check ────────────────────────────────
2862
- ui.printLogin(`${c.dim}Checking DM history...${c.reset}`);
2865
+ ui.log(`${c.dim}Checking DM history...${c.reset}`);
2863
2866
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
2864
2867
  for (const w of activeWorkers) {
2865
2868
  try {
@@ -2877,18 +2880,18 @@ async function start(apiKey, apiUrl, opts = {}) {
2877
2880
  }
2878
2881
  } catch {}
2879
2882
  }
2880
- if (dmNoLs.length > 0) ui.printLogin(` ${c.yellow}WARN${c.reset} No lifesavers: ${dmNoLs.join(', ')}`);
2883
+ if (dmNoLs.length > 0) ui.log(`${c.red}E${c.reset} No lifesavers: ${dmNoLs.join(', ')}`);
2881
2884
  const parts = [];
2882
2885
  if (dmDeaths > 0) parts.push(`${dmDeaths} deaths`);
2883
2886
  if (dmLevelUps > 0) parts.push(`${dmLevelUps} level-ups`);
2884
- ui.printLogin(`DM check: ${parts.length > 0 ? parts.join(', ') : c.green + 'clean' + c.reset}`);
2885
- ui.clearLoginLines();
2887
+ ui.log(`${c.green}·${c.reset} DM: ${parts.length > 0 ? parts.join(', ') : c.green + 'clean' + c.reset}`);
2886
2888
 
2887
2889
  // ── Phase 3: Start grind loops ───────────────────────────────────
2890
+ ui.log(`${c.green}·${c.reset} Starting ${activeWorkers.length} grind loops...`);
2888
2891
  for (const w of activeWorkers) {
2889
2892
  if (!shutdownCalled) w.grindLoop();
2890
2893
  }
2891
- ui.render();
2894
+ ui.draw();
2892
2895
 
2893
2896
  // Cluster heartbeat — lets other nodes see this node is alive
2894
2897
  if (CLUSTER_ENABLED) {
package/lib/ui.js CHANGED
@@ -1,186 +1,201 @@
1
1
  /**
2
- * CLI Live Dashboard
3
- * Renders a terminal table that updates in place every 10s.
4
- * Shows per-account status, earned coins, commands, and session uptime.
2
+ * CLI Live Dashboard — one table, append-only events below.
3
+ * Draws once after login. Events stream below without touching the table.
5
4
  */
6
5
 
7
6
  let _startTime = Date.now();
8
7
  let _workers = [];
9
8
  let _isShuttingDown = () => false;
9
+ let _version = '0.0.0';
10
+
11
+ // ── Big ASCII art banner ──────────────────────────────────────
12
+ const BANNER = [
13
+ ' ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗',
14
+ ' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║',
15
+ ' ██║ ██║██║ ██║██╔██╗ ██║██║ ███╗█████╗ ██║ ██║██╔██╗ ██║',
16
+ ' ██║ ██║██║ ██║██║╚██╗██║██║ ██║██╔══╝ ██║ ██║██║╚██╗██║',
17
+ ' ██████╔╝╚██████╔╝██║ ╚████║╚██████╔╝███████╗╚██████╔╝██║ ╚████║',
18
+ ' ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝',
19
+ ' ██╗██╗ ██╗███╗ ██╗ ██████╗ ██╗ ███████╗██████╗ ',
20
+ ' ██║██║ ██║████╗ ██║██╔═══██╗██║ ██╔════╝██╔══██╗',
21
+ ' ███████╗███████╗ ██║██║ ██║██╔██╗ ██║██║ ██║██║ █████╗ ██████╔╝',
22
+ ' ╚════██║╚════██║ ██║██║ ██║██║╚██╗██║██║ ██║██║ ██╔══╝ ██╔══██╗',
23
+ ' ██║ ██║ ██║╚██████╔╝██║ ╚████║╚██████╔╝███████╗███████╗██║ ██║',
24
+ ' ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝',
25
+ ];
26
+
27
+ // Gradient each banner line cyan→pink
28
+ function gradientLine(text, r1, g1, b1, r2, g2, b2) {
29
+ let out = '';
30
+ for (let i = 0; i < text.length; i++) {
31
+ const t = text.length <= 1 ? 0 : i / (text.length - 1);
32
+ const lr = Math.round(r1 + (r2 - r1) * t);
33
+ const lg = Math.round(g1 + (g2 - g1) * t);
34
+ const lb = Math.round(b1 + (b2 - b1) * t);
35
+ out += `\x1b[38;2;${lr};${lg};${lb}m${text[i]}`;
36
+ }
37
+ return out + '\x1b[0m';
38
+ }
39
+
40
+ // ── Dark-theme palette ────────────────────────────────────────
41
+ const PALETTE = [
42
+ '\x1b[38;2;77;212;238m', // cyan
43
+ '\x1b[38;2;255;194;77m', // amber
44
+ '\x1b[38;2;130;210;100m', // lime
45
+ '\x1b[38;2;255;120;200m', // pink
46
+ '\x1b[38;2;120;180;255m', // sky
47
+ '\x1b[38;2;255;180;80m', // orange
48
+ '\x1b[38;2;180;130;255m', // lavender
49
+ '\x1b[38;2;100;255;180m', // teal
50
+ '\x1b[38;2;255;150;100m', // peach
51
+ '\x1b[38;2;150;255;200m', // mint
52
+ '\x1b[38;2;255;255;120m', // yellow
53
+ '\x1b[38;2;200;150;255m', // violet
54
+ ];
55
+ function wc(idx) { return PALETTE[idx % PALETTE.length]; }
56
+
57
+ // ── ANSI helpers ──────────────────────────────────────────────
58
+ const c = {
59
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
60
+ green: '\x1b[38;2;80;255;120m',
61
+ red: '\x1b[38;2;255;80;100m',
62
+ yellow: '\x1b[38;2;255;220;80m',
63
+ cyan: '\x1b[38;2;80;220;255m',
64
+ blue: '\x1b[38;2;100;160;255m',
65
+ };
66
+ const DIM = c.dim;
67
+
68
+ function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
69
+ function padR(s, n) { return trunc(s, n).padEnd(n); }
70
+ function padL(s, n) { return String(s).padStart(n); }
10
71
 
11
- function formatUptime() {
72
+ function fmtUptime() {
12
73
  const s = Math.floor((Date.now() - _startTime) / 1000);
13
74
  if (s < 60) return `${s}s`;
14
75
  const m = Math.floor(s / 60);
15
76
  const h = Math.floor(m / 60);
16
77
  const d = Math.floor(h / 24);
17
- if (d > 0) return `${d}d ${h % 24}h`;
18
- if (h > 0) return `${h}h ${m % 60}m`;
78
+ if (d > 0) return `${d}d${h % 24}h`;
79
+ if (h > 0) return `${h}h${m % 60}m`;
19
80
  return `${m}m`;
20
81
  }
21
82
 
22
- // ANSI helpers
23
- const c = {
24
- reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
25
- green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m',
26
- cyan: '\x1b[36m', blue: '\x1b[34m',
27
- };
28
- const DIM = c.dim;
29
-
30
- function rgb(r, g, b) { return `\x1b[38;2;${r};${g};${b}m`; }
31
- function lerp(a, b, t) { return Math.round(a + (b - a) * t); }
32
- function gradientLine(text, from, to) {
33
- const fr = Array.isArray(from) ? from[0] : 128;
34
- const fg = Array.isArray(from) ? from[1] : 128;
35
- const fb = Array.isArray(from) ? from[2] : 128;
36
- const tr = Array.isArray(to) ? to[0] : 255;
37
- const tg = Array.isArray(to) ? to[1] : 255;
38
- const tb = Array.isArray(to) ? to[2] : 255;
39
- let out = '';
40
- for (let i = 0; i < text.length; i++) {
41
- const t = text.length <= 1 ? 0 : i / (text.length - 1);
42
- out += `\x1b[38;2;${lerp(fr, tr, t)};${lerp(fg, tg, t)};${lerp(fb, tb, t)}m${text[i]}`;
83
+ function icon(w) {
84
+ if (!w.running || !w.channel) return `${c.red}E${c.reset}`;
85
+ if (w.paused || w.dashboardPaused) return `${c.yellow}P${c.reset}`;
86
+ if (w._alert) {
87
+ if (w._alert.type === 'death') return `${c.red}!${c.reset}`;
88
+ if (w._alert.type === 'lowls') return `${c.yellow}!${c.reset}`;
43
89
  }
44
- return out + c.reset;
90
+ if (w._rateLimitHits > (w._prevRateLimits || 0)) return `${c.cyan}~${c.reset}`;
91
+ return `${c.green}·${c.reset}`;
45
92
  }
46
93
 
47
- let _lineCount = 0;
48
- let _interval = null;
49
- let _loginLines = 0; // number of login progress lines to clear
50
-
51
- function _statusIcon(w) {
52
- if (!w.running || !w.channel) return `${c.red}✗${c.reset}`;
53
- if (w.paused || w.dashboardPaused) return `${c.yellow}⏸${c.reset}`;
54
- if (w.busy || w._invRunning || w._sellRunning) return `${c.cyan}⚙${c.reset}`;
55
- if (Date.now() < w.globalCooldownUntil) return `${c.blue}⏳${c.reset}`;
56
- return `${c.green}●${c.reset}`;
94
+ function sortedWorkers() {
95
+ return [..._workers].sort((a, b) => {
96
+ if (!a.channel !== !b.channel) return !a.channel ? 1 : -1;
97
+ const aA = a.running && a.channel && !a.paused && !a.dashboardPaused;
98
+ const bA = b.running && b.channel && !b.paused && !b.dashboardPaused;
99
+ if (aA !== bA) return bA ? 1 : -1;
100
+ return (b.stats.commands || 0) - (a.stats.commands || 0);
101
+ });
57
102
  }
58
103
 
59
- function _render(extraLines = []) {
60
- const workers = _workers;
104
+ // ── Draw full table (call once after login) ──────────────────
105
+ function draw() {
61
106
  const W = Math.min(process.stdout.columns || 120, 120);
62
- const lines = [];
63
-
64
- lines.push(`${c.bold}${'─'.repeat(W)}${c.reset}`);
65
- lines.push(
66
- ` ${c.bold}#${c.reset} ${c.bold}Account${c.reset}${' '.repeat(Math.max(1, 20 - 7))}` +
67
- `${c.bold}Status${c.reset}${' '.repeat(Math.max(1, 14 - 6))}` +
68
- `${c.bold}Earned${c.reset}${' '.repeat(Math.max(1, 12 - 5))}` +
69
- `${c.bold}Cmds${c.reset}${' '.repeat(Math.max(1, 7 - 4))}` +
70
- `${c.bold}OK%${c.reset}`
107
+ const hr = '─'.repeat(W);
108
+ const running = _workers.filter(w => w.running && w.channel && !w.paused && !w.dashboardPaused).length;
109
+ const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
110
+
111
+ console.log('\x1b[2J\x1b[H');
112
+ console.log('');
113
+
114
+ // Big banner with gradient
115
+ for (const line of BANNER) {
116
+ console.log(gradientLine(line, 77, 212, 238, 255, 92, 147));
117
+ }
118
+
119
+ console.log('');
120
+ console.log(
121
+ ` ${c.bold}v${_version}${c.reset} ` +
122
+ `${c.green}●${c.reset}online ` +
123
+ `${fmtUptime()} ` +
124
+ `${c.green}·${c.reset}${running} ` +
125
+ `${c.yellow}~${c.reset}${paused} ` +
126
+ `${DIM}Ctrl+C${c.reset}`
71
127
  );
72
- lines.push(`${c.bold}${'─'.repeat(W)}${c.reset}`);
73
-
74
- for (let i = 0; i < workers.length; i++) {
75
- const w = workers[i];
76
- const name = (w.username || '?').substring(0, 20);
77
- const status = (w.lastStatus || 'idle').substring(0, 14).padEnd(14);
78
- const earned = w.stats.coins > 0 ? `+⏣${w.stats.coins.toLocaleString()}` : DIM + '' + c.reset;
79
- const cmds = String(w.stats.commands || 0);
128
+ console.log(` ${hr}`);
129
+ console.log(
130
+ ` ${c.bold}#${c.reset} ${c.bold}${padR('St', 2)}${c.reset} ` +
131
+ `${c.bold}${padR('Account', 18)}${c.reset} ` +
132
+ `${c.bold}${padR('Last Command', 20)}${c.reset} ` +
133
+ `${c.bold}${padL('Cmds', 5)}${c.reset} ` +
134
+ `${c.bold}${padL('OK%', 3)}${c.reset} ` +
135
+ `${c.bold}${padL('Earned', 8)}${c.reset}`
136
+ );
137
+ console.log(` ${hr}`);
138
+
139
+ const sorted = sortedWorkers();
140
+ for (let si = 0; si < sorted.length; si++) {
141
+ const w = sorted[si];
142
+ const wi = _workers.indexOf(w);
143
+ const col = wc(wi);
144
+ const earned = w.stats.coins > 0 ? `${c.green}+${w.stats.coins.toLocaleString()}${c.reset}` : DIM + '—' + c.reset;
145
+ const cmds = w.stats.commands || 0;
80
146
  const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
81
147
 
82
- lines.push(
83
- ` ${DIM}${String(i + 1).padStart(2)}${c.reset} ${name.padEnd(20)} ${status}` +
84
- `${String(earned).padStart(12)} ${cmds.padStart(6)} ${String(rate).padStart(3)}%`
148
+ process.stdout.write(
149
+ ` ${icon(w)} ` +
150
+ `${padL(wi + 1, 2)} ` +
151
+ `${col}${padR(w.username || '?', 18)}${c.reset} ` +
152
+ `${DIM}${padR(w.lastStatus || 'idle', 20)}${c.reset} ` +
153
+ `${padL(cmds, 5)} ` +
154
+ `${padL(rate, 3)}% ` +
155
+ `${earned.padStart(8)}\n`
85
156
  );
86
157
  }
87
158
 
88
- for (const el of extraLines) lines.push(el);
89
-
90
- lines.push(`${c.bold}${'─'.repeat(W)}${c.reset}`);
159
+ console.log(` ${hr}`);
91
160
 
92
- // Totals
93
- let totalCoins = 0, totalCmds = 0;
94
- for (const w of workers) {
161
+ let totalCoins = 0, totalCmds = 0, totalOk = 0;
162
+ for (const w of _workers) {
95
163
  totalCoins += w.stats.coins || 0;
96
164
  totalCmds += w.stats.commands || 0;
165
+ totalOk += w.stats.successes || 0;
97
166
  }
167
+ const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
98
168
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
99
- lines.push(
100
- ` ${c.bold}Total${c.reset} ` +
101
- `${totalCoins > 0 ? c.green + '+⏣' + totalCoins.toLocaleString() + c.reset : DIM + '—' + c.reset}` +
102
- ` ${totalCmds} cmds ${formatUptime()} ${memMB}MB`
169
+
170
+ console.log(
171
+ ` ${c.bold}Σ${c.reset} ` +
172
+ `${DIM}${padL(_workers.length, 2)} acc${c.reset} ` +
173
+ `${' '.repeat(18)}` +
174
+ `${DIM}${padL(totalCmds, 5)} cmds ${rate}% ` +
175
+ `${totalCoins > 0 ? c.green + '+' + totalCoins.toLocaleString() + c.reset : DIM + '—' + c.reset}` +
176
+ `${' '.repeat(Math.max(0, 8 - String(totalCoins).length - 1))}${DIM}${fmtUptime()} | ${memMB}MB${c.reset}`
103
177
  );
104
- lines.push('');
105
-
106
- // Redraw in place — also clear any login progress lines above
107
- const clearLines = _lineCount + _loginLines;
108
- if (clearLines > 0) {
109
- process.stdout.write(`\x1b[${clearLines}A`);
110
- for (let i = 0; i < clearLines; i++) {
111
- process.stdout.write(`\x1b[2K\r${i < clearLines - 1 ? '\x1b[1A' : ''}`);
112
- }
113
- }
114
- process.stdout.write(lines.join('\n') + '\n');
115
- _lineCount = lines.length - 1;
116
- _loginLines = 0;
178
+ console.log(` ${hr}`);
179
+ console.log('');
117
180
  }
118
181
 
119
- function _clear() {
120
- const clearLines = _lineCount + _loginLines;
121
- if (clearLines > 0) {
122
- process.stdout.write(`\x1b[${clearLines}A`);
123
- for (let i = 0; i < clearLines; i++) {
124
- process.stdout.write(`\x1b[2K\r${i < clearLines - 1 ? '\x1b[1A' : ''}`);
125
- }
126
- _lineCount = 0;
127
- _loginLines = 0;
128
- }
182
+ // ── Append event log line ────────────────────────────────────
183
+ function log(msg) {
184
+ const now = new Date();
185
+ const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
186
+ process.stdout.write(` ${DIM}[${ts}]${c.reset} ${msg}\n`);
129
187
  }
130
188
 
131
189
  // ── Public API ────────────────────────────────────────────────
132
-
133
190
  function init({ workers, isShuttingDown }) {
134
191
  _startTime = Date.now();
135
192
  _workers = workers;
136
193
  _isShuttingDown = isShuttingDown || (() => false);
194
+ _version = '0.0.0';
137
195
  }
138
196
 
139
- function drawBanner(version) {
140
- const title = `DANKGRINDER v${version}`;
141
- const gradient = gradientLine(title, [77, 142, 255], [255, 92, 147]);
142
- console.log('');
143
- console.log(` ${gradient}${c.reset}`);
144
- console.log(` ${DIM}Ctrl+C to stop${c.reset}`);
145
- console.log('');
146
- }
147
-
148
- // Show dashboard and start the 10s refresh interval
149
- function start() {
150
- if (_workers.length > 30) return;
151
- _lineCount = 0;
152
- _loginLines = 0;
153
- _render();
154
- _interval = setInterval(() => {
155
- if (_isShuttingDown()) {
156
- clearInterval(_interval);
157
- _interval = null;
158
- return;
159
- }
160
- _render();
161
- }, 10_000);
162
- }
163
-
164
- // Immediately re-render the dashboard (use after worker status changes)
165
- function render() {
166
- if (_isShuttingDown()) return;
167
- _render();
168
- }
169
-
170
- // Print a login progress line and count it so it gets cleared on next render
171
- function printLogin(msg) {
172
- console.log(msg);
173
- _loginLines++;
174
- }
175
-
176
- // Clear login lines and re-render final state
177
- function clearLoginLines() {
178
- _render();
179
- }
180
-
181
- function stop() {
182
- if (_interval) { clearInterval(_interval); _interval = null; }
183
- _clear();
184
- }
197
+ function drawBanner(version) { _version = version; }
198
+ function start() {}
199
+ function stop() { process.stdout.write(c.reset + '\n'); }
185
200
 
186
- module.exports = { init, drawBanner, start, render, printLogin, clearLoginLines, stop };
201
+ module.exports = { init, drawBanner, start, draw, log, workerColor: wc, stop };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.36.0",
3
+ "version": "8.38.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"