dankgrinder 8.34.0 → 8.37.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 +34 -24
  2. package/lib/ui.js +194 -102
  3. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -2750,10 +2750,13 @@ 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.logEvent(`${c.red}E${c.reset} DEATH — ${w.username} has 0 lifesavers! Crime/search disabled`);
2759
+ ui.draw();
2757
2760
  } else {
2758
2761
  w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
2759
2762
  if (prev !== event.lifesaversLeft) {
@@ -2761,7 +2764,10 @@ async function start(apiKey, apiUrl, opts = {}) {
2761
2764
  w.setCooldown?.('search', 60);
2762
2765
  }
2763
2766
  if (event.lifesaversLeft <= 2) {
2767
+ w._alert = { type: 'lowls' };
2764
2768
  sendWebhook?.('LOW LIFESAVERS', `**${w.username}** has only **${event.lifesaversLeft}** lifesaver(s) left!`, 0xfbbf24);
2769
+ ui.logEvent(`${c.yellow}!${c.reset} ${w.username} — only ${event.lifesaversLeft} lifesavers!`);
2770
+ ui.draw();
2765
2771
  }
2766
2772
  }
2767
2773
  }
@@ -2770,6 +2776,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2770
2776
  if (event.type === 'levelup') {
2771
2777
  if (event.to > 0) {
2772
2778
  w._level = event.to;
2779
+ ui.logEvent(`${c.blue}↑${c.reset} ${w.username} leveled up to ${event.to}`);
2773
2780
  }
2774
2781
  }
2775
2782
  }
@@ -2783,7 +2790,8 @@ async function start(apiKey, apiUrl, opts = {}) {
2783
2790
  const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
2784
2791
  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
2792
 
2786
- console.log(`Logging in ${accounts.length} accounts...`);
2793
+ ui.draw(); // draw initial table
2794
+ ui.logEvent(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
2787
2795
  const BATCH_SIZE = 10;
2788
2796
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
2789
2797
  if (shutdownCalled) break;
@@ -2792,25 +2800,27 @@ async function start(apiKey, apiUrl, opts = {}) {
2792
2800
  await Promise.all(batch.map(async (acc, idx) => {
2793
2801
  try {
2794
2802
  if (idx > 0) await new Promise(r => setTimeout(r, 100 + Math.floor(Math.random() * 500)));
2795
- console.log(` [${i + idx + 1}] starting: ${acc.label || acc.id}`);
2796
2803
  const worker = new AccountWorker(acc, i + idx);
2797
2804
  workers.push(worker);
2798
2805
  workerMap.set(acc.id, worker);
2799
2806
  await worker.start();
2800
- console.log(` [${i + idx + 1}] done: ${acc.label || acc.id}`);
2801
2807
  if (worker._tokenInvalid) {
2802
- console.log(` [${i + idx + 1}] FAIL - invalid token: ${acc.label || acc.id}`);
2808
+ worker.lastStatus = 'invalid token';
2809
+ ui.logEvent(`${c.red}E${c.reset} [${i + idx + 1}] ${acc.label || acc.id} — invalid token`);
2803
2810
  } else if (worker.channel) {
2804
- console.log(` [${i + idx + 1}] OK - ${worker.username}`);
2811
+ worker.lastStatus = 'ready';
2812
+ ui.logEvent(`${c.green}·${c.reset} [${i + idx + 1}] ${worker.username} connected`);
2813
+ ui.draw();
2805
2814
  } else {
2806
- console.log(` [${i + idx + 1}] TIMEOUT`);
2815
+ worker.lastStatus = 'timeout';
2816
+ ui.logEvent(`${c.yellow}·${c.reset} [${i + idx + 1}] ${acc.label || acc.id} — timeout`);
2807
2817
  }
2808
2818
  } catch (e) {
2809
- console.log(` [${i + idx + 1}] ERROR: ${e.message}`);
2819
+ ui.logEvent(`${c.red}E${c.reset} [${i + idx + 1}] ERROR: ${e.message}`);
2810
2820
  }
2811
2821
  }));
2812
2822
  } catch (e) {
2813
- console.log(`BATCH ERROR: ${e.message}`);
2823
+ ui.logEvent(`${c.red}!${c.reset} BATCH ERROR: ${e.message}`);
2814
2824
  }
2815
2825
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
2816
2826
  hintGC();
@@ -2819,31 +2829,32 @@ async function start(apiKey, apiUrl, opts = {}) {
2819
2829
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
2820
2830
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
2821
2831
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
2822
- console.log(`Login complete: ${loginDone}/${accounts.length} connected`);
2832
+ ui.logEvent(`${c.green}·${c.reset} Login: ${loginDone}/${accounts.length} connected`);
2823
2833
  if (invalidWorkers.length > 0) {
2824
- for (const w of invalidWorkers) console.log(` FAIL - invalid token: ${w.account.label || w.account.id}`);
2834
+ for (const w of invalidWorkers) ui.logEvent(`${c.red}E${c.reset} invalid token: ${w.account.label || w.account.id}`);
2825
2835
  }
2826
2836
  if (timedOutWorkers.length > 0) {
2827
- console.log(` WARN - ${timedOutWorkers.length} timed out (will retry in background)`);
2837
+ ui.logEvent(`${c.yellow}~${c.reset} ${timedOutWorkers.length} timed out (will retry in background)`);
2828
2838
  }
2839
+ ui.draw();
2829
2840
 
2830
2841
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
2831
2842
 
2832
2843
  // ── Phase 2: Inventory check ────────────────────────────────────
2833
- console.log('Checking inventory...');
2844
+ ui.logEvent(`${c.dim}Checking inventory...${c.reset}`);
2834
2845
  let invFailed = 0;
2835
2846
  await Promise.all(activeWorkers.map(async (w) => {
2836
2847
  try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
2837
2848
  catch { invFailed++; }
2838
2849
  }));
2839
2850
  if (invFailed > 0) {
2840
- console.log(`Inventory failed for ${invFailed} accounts. Not starting grind loops.`);
2841
- return;
2851
+ ui.logEvent(`${c.red}!${c.reset} Inventory failed for ${invFailed} accounts`);
2852
+ } else {
2853
+ ui.logEvent(`${c.green}·${c.reset} Inventory OK`);
2842
2854
  }
2843
- console.log('Inventory check complete');
2844
2855
 
2845
2856
  // ── Phase 2.5: Balance check ───────────────────────────────────
2846
- console.log('Checking balances...');
2857
+ ui.logEvent(`${c.dim}Checking balances...${c.reset}`);
2847
2858
  for (const w of activeWorkers) {
2848
2859
  try { await w.checkBalance(true); } catch {}
2849
2860
  }
@@ -2852,10 +2863,10 @@ async function start(apiKey, apiUrl, opts = {}) {
2852
2863
  totalCoins += w.stats?.balance || 0;
2853
2864
  totalCoins += w.stats?.bankBalance || 0;
2854
2865
  }
2855
- console.log(`Balances: total ${totalCoins.toLocaleString()} coins across ${activeWorkers.length} accounts`);
2866
+ ui.logEvent(`${c.blue}$${c.reset} Balances: ${c.green}+⏣${totalCoins.toLocaleString()}${c.reset} across ${activeWorkers.length} accounts`);
2856
2867
 
2857
2868
  // ── Phase 2.75: DM history check ────────────────────────────────
2858
- console.log('Checking DM history...');
2869
+ ui.logEvent(`${c.dim}Checking DM history...${c.reset}`);
2859
2870
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
2860
2871
  for (const w of activeWorkers) {
2861
2872
  try {
@@ -2873,19 +2884,18 @@ async function start(apiKey, apiUrl, opts = {}) {
2873
2884
  }
2874
2885
  } catch {}
2875
2886
  }
2876
- if (dmNoLs.length > 0) console.log(` WARN - No lifesavers: ${dmNoLs.join(', ')}`);
2887
+ if (dmNoLs.length > 0) ui.logEvent(`${c.red}E${c.reset} No lifesavers: ${dmNoLs.join(', ')}`);
2877
2888
  const parts = [];
2878
2889
  if (dmDeaths > 0) parts.push(`${dmDeaths} deaths`);
2879
2890
  if (dmLevelUps > 0) parts.push(`${dmLevelUps} level-ups`);
2880
- console.log(`DM check: ${parts.length > 0 ? parts.join(', ') : 'clean'}`);
2891
+ ui.logEvent(`${c.green}·${c.reset} DM: ${parts.length > 0 ? parts.join(', ') : c.green + 'clean' + c.reset}`);
2881
2892
 
2882
2893
  // ── Phase 3: Start grind loops ───────────────────────────────────
2883
- console.log(`Starting ${activeWorkers.length} grind loops...`);
2894
+ ui.logEvent(`${c.green}·${c.reset} Starting ${activeWorkers.length} grind loops...`);
2884
2895
  for (const w of activeWorkers) {
2885
2896
  if (!shutdownCalled) w.grindLoop();
2886
2897
  }
2887
- ui.start();
2888
- console.log(`${c.dim}All grind loops started. | Ctrl+C to stop${c.reset}`);
2898
+ ui.draw();
2889
2899
 
2890
2900
  // Cluster heartbeat — lets other nodes see this node is alive
2891
2901
  if (CLUSTER_ENABLED) {
package/lib/ui.js CHANGED
@@ -1,119 +1,219 @@
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 — compact, colorful, append-only.
3
+ * Shows top-N active accounts that fit in the window.
4
+ * Each account has its own color. Events logged below.
5
5
  */
6
6
 
7
7
  let _startTime = Date.now();
8
8
  let _workers = [];
9
9
  let _isShuttingDown = () => false;
10
10
 
11
- function formatUptime() {
11
+ // ── Dark-theme palette (light/bright colors on dark bg) ───────
12
+ const PALETTE = [
13
+ '\x1b[38;2;77;212;238m', // cyan
14
+ '\x1b[38;2;255;194;77m', // amber
15
+ '\x1b[38;2;130;210;100m', // lime
16
+ '\x1b[38;2;255;120;200m', // pink
17
+ '\x1b[38;2;120;180;255m', // sky
18
+ '\x1b[38;2;255;180;80m', // orange
19
+ '\x1b[38;2;180;130;255m', // lavender
20
+ '\x1b[38;2;100;255;180m', // teal
21
+ '\x1b[38;2;255;150;100m', // peach
22
+ '\x1b[38;2;150;255;200m', // mint
23
+ '\x1b[38;2;255;255;120m', // yellow
24
+ '\x1b[38;2;200;150;255m', // violet
25
+ ];
26
+
27
+ function workerColor(idx) { return PALETTE[idx % PALETTE.length]; }
28
+
29
+ // ── ANSI helpers ──────────────────────────────────────────────
30
+ const c = {
31
+ reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
32
+ green: '\x1b[38;2;80;255;120m',
33
+ red: '\x1b[38;2;255;80;100m',
34
+ yellow: '\x1b[38;2;255;220;80m',
35
+ cyan: '\x1b[38;2;80;220;255m',
36
+ blue: '\x1b[38;2;100;160;255m',
37
+ gray: '\x1b[38;2;120;130;150m',
38
+ white: '\x1b[38;2;220;220;240m',
39
+ };
40
+ const DIM = c.dim;
41
+
42
+ // ── Uptime ───────────────────────────────────────────────────
43
+ function fmtUptime() {
12
44
  const s = Math.floor((Date.now() - _startTime) / 1000);
13
45
  if (s < 60) return `${s}s`;
14
46
  const m = Math.floor(s / 60);
15
47
  const h = Math.floor(m / 60);
16
48
  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`;
49
+ if (d > 0) return `${d}d${h % 24}h`;
50
+ if (h > 0) return `${h}h${m % 60}m`;
19
51
  return `${m}m`;
20
52
  }
21
53
 
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;
54
+ // ── Column widths based on terminal width ────────────────────
55
+ function colWidths(totalW) {
56
+ const minW = 80;
57
+ const w = Math.max(totalW, minW);
58
+ // Columns: # | Status | Account | LastCmd | Cmds | OK% | Earned
59
+ // Reserve: 1+2+1 + 1+3+1 + 1+16+1 + 1+20+1 + 1+5+1 + 1+3+1 + 1+12+1 = ~72 fixed
60
+ const fixed = 74; // 1+2+1 + 1+3+1 + 1+16+1 + 1+20+1 + 1+5+1 + 1+3+1 + 1+12+1
61
+ const extra = Math.max(0, w - fixed);
62
+ return {
63
+ status: 3,
64
+ num: 2,
65
+ name: 16,
66
+ lastCmd: 20 + Math.floor(extra * 0.5),
67
+ cmds: 5,
68
+ ok: 3,
69
+ earned: 12,
70
+ };
71
+ }
29
72
 
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]}`;
73
+ // ── Status icon ─────────────────────────────────────────────
74
+ function icon(w) {
75
+ if (!w.running || !w.channel) return `${c.red}E${c.reset}`;
76
+ if (w.paused || w.dashboardPaused) return `${c.yellow}P${c.reset}`;
77
+ if (w._alert) {
78
+ if (w._alert.type === 'death') return `${c.red}!${c.reset}`;
79
+ if (w._alert.type === 'lowls') return `${c.yellow}!${c.reset}`;
43
80
  }
44
- return out + c.reset;
81
+ if (w._rateLimitHits > (w._prevRateLimits || 0)) return `${c.cyan}~${c.reset}`;
82
+ return `${c.green}·${c.reset}`;
45
83
  }
46
84
 
47
- let _lineCount = 0;
48
- let _interval = null;
49
-
50
- function _statusIcon(w) {
51
- if (!w.running || !w.channel) return `${c.red}✗${c.reset}`;
52
- if (w.paused || w.dashboardPaused) return `${c.yellow}⏸${c.reset}`;
53
- if (w.busy || w._invRunning || w._sellRunning) return `${c.cyan}⚙${c.reset}`;
54
- if (Date.now() < w.globalCooldownUntil) return `${c.blue}⏳${c.reset}`;
55
- return `${c.green}●${c.reset}`;
85
+ // ── Truncate with ellipsis ───────────────────────────────────
86
+ function trunc(str, len) {
87
+ str = String(str || '');
88
+ if (str.length <= len) return str;
89
+ return str.substring(0, len - 1) + '…';
56
90
  }
57
91
 
58
- function _render() {
59
- const workers = _workers;
92
+ // ── Draw header ─────────────────────────────────────────────
93
+ function drawHeader(version) {
94
+ const rows = 8; // banner(3) + 1gap + header(1) + hr(1) + N rows + total(1) + gap(1)
95
+ const availRows = process.stdout.rows || 40;
96
+ const visible = Math.min(_workers.length, Math.max(5, availRows - rows));
60
97
  const W = Math.min(process.stdout.columns || 120, 120);
61
- const lines = [];
62
-
63
- lines.push(`${c.bold}${'─'.repeat(W)}${c.reset}`);
64
- lines.push(
65
- ` ${c.bold}#${c.reset} ${c.bold}Account${c.reset}${' '.repeat(Math.max(1, 20 - 7))}` +
66
- `${c.bold}Status${c.reset}${' '.repeat(Math.max(1, 14 - 6))}` +
67
- `${c.bold}Earned${c.reset}${' '.repeat(Math.max(1, 12 - 5))}` +
68
- `${c.bold}Cmds${c.reset}${' '.repeat(Math.max(1, 7 - 4))}` +
69
- `${c.bold}OK%${c.reset}`
98
+
99
+ const cols = colWidths(W);
100
+ const hr = '─'.repeat(W);
101
+
102
+ const running = _workers.filter(w => w.running && w.channel).length;
103
+ const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
104
+ const active = running + paused;
105
+
106
+ // Top bar
107
+ console.log('');
108
+ console.log(` ${c.bold}DANK${c.reset}${c.blue}GRINDER${c.reset} ${DIM}v${version}${c.reset} ${c.green}●${c.reset}online ${DIM}|${c.reset} ${fmtUptime()} ${c.green}·${c.reset}${running} ${c.yellow}~${c.reset}${paused} ${c.dim}Ctrl+C${c.reset}`);
109
+
110
+ // Account summary if many
111
+ if (_workers.length > visible) {
112
+ console.log(` ${c.dim}${_workers.length} accounts | showing top ${visible} by activity | scroll: web dashboard${c.reset}`);
113
+ }
114
+ console.log('');
115
+
116
+ // Column headers
117
+ console.log(
118
+ ` ${c.bold}#${c.reset} ` +
119
+ `${c.bold}${trunc('Status', cols.status).padEnd(cols.status)}${c.reset} ` +
120
+ `${c.bold}${trunc('Account', cols.name).padEnd(cols.name)}${c.reset} ` +
121
+ `${c.bold}${trunc('Last Command', cols.lastCmd).padEnd(cols.lastCmd)}${c.reset} ` +
122
+ `${c.bold}${trunc('Cmds', cols.cmds).padStart(cols.cmds)}${c.reset} ` +
123
+ `${c.bold}${trunc('OK%', cols.ok).padStart(cols.ok)}${c.reset} ` +
124
+ `${c.bold}${trunc('Earned', cols.earned).padStart(cols.earned)}${c.reset}`
70
125
  );
71
- lines.push(`${c.bold}${'─'.repeat(W)}${c.reset}`);
72
-
73
- for (let i = 0; i < workers.length; i++) {
74
- const w = workers[i];
75
- const name = (w.username || '?').substring(0, 20);
76
- const status = (w.lastStatus || 'idle').substring(0, 14).padEnd(14);
77
- const earned = w.stats.coins > 0 ? `+⏣${w.stats.coins.toLocaleString()}` : DIM + '—' + c.reset;
78
- const cmds = String(w.stats.commands || 0);
79
- const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
80
-
81
- lines.push(
82
- ` ${DIM}${String(i + 1).padStart(2)}${c.reset} ${name.padEnd(20)} ${status}` +
83
- `${String(earned).padStart(12)} ${cmds.padStart(6)} ${String(rate).padStart(3)}%`
126
+ console.log(` ${hr}`);
127
+
128
+ return { visible, W, cols };
129
+ }
130
+
131
+ // ── Sort: running first, then by activity ──────────────────
132
+ function sortedWorkers(visible) {
133
+ return [..._workers]
134
+ .sort((a, b) => {
135
+ // Invalid/timeout at bottom
136
+ if (!a.channel && !!b.channel) return 1;
137
+ if (!b.channel && !!a.channel) return -1;
138
+ // Running/active first
139
+ const aActive = a.running && a.channel && !a.paused && !a.dashboardPaused;
140
+ const bActive = b.running && b.channel && !b.paused && !b.dashboardPaused;
141
+ if (aActive !== bActive) return bActive ? 1 : -1;
142
+ // Sort by total activity
143
+ const aScore = (a.stats.commands || 0) + (a.stats.coins || 0);
144
+ const bScore = (b.stats.commands || 0) + (b.stats.coins || 0);
145
+ return bScore - aScore;
146
+ })
147
+ .slice(0, visible);
148
+ }
149
+
150
+ // ── Draw all rows ───────────────────────────────────────────
151
+ function drawRows({ visible, W, cols }) {
152
+ const hr = '─'.repeat(W);
153
+ const sorted = sortedWorkers(visible);
154
+
155
+ for (let si = 0; si < sorted.length; si++) {
156
+ const w = sorted[si];
157
+ const wi = _workers.indexOf(w); // original index for color
158
+ const col = workerColor(wi);
159
+
160
+ const earned = w.stats.coins > 0
161
+ ? `${c.green}+${w.stats.coins.toLocaleString()}${c.reset}`
162
+ : DIM + '—' + c.reset;
163
+ const cmds = w.stats.commands || 0;
164
+ const rate = w.stats.commands > 0
165
+ ? Math.round((w.stats.successes / w.stats.commands) * 100)
166
+ : 0;
167
+
168
+ process.stdout.write(
169
+ ` ${icon(w)} ` +
170
+ `${DIM}${String(wi + 1).padStart(cols.num)}${c.reset} ` +
171
+ `${col}${trunc(w.username || '?', cols.name).padEnd(cols.name)}${c.reset} ` +
172
+ `${DIM}${trunc(w.lastStatus || 'idle', cols.lastCmd).padEnd(cols.lastCmd)}${c.reset} ` +
173
+ `${String(cmds).padStart(cols.cmds)} ` +
174
+ `${String(rate).padStart(cols.ok)}% ` +
175
+ `${earned.padStart(cols.earned)}\n`
84
176
  );
85
177
  }
86
178
 
87
- lines.push(`${c.bold}${'─'.repeat(W)}${c.reset}`);
179
+ console.log(` ${hr}`);
88
180
 
89
181
  // Totals
90
- let totalCoins = 0, totalCmds = 0;
91
- for (const w of workers) {
182
+ let totalCoins = 0, totalCmds = 0, totalOk = 0;
183
+ for (const w of _workers) {
92
184
  totalCoins += w.stats.coins || 0;
93
185
  totalCmds += w.stats.commands || 0;
186
+ totalOk += w.stats.successes || 0;
94
187
  }
188
+ const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
95
189
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
96
- lines.push(
97
- ` ${c.bold}Total${c.reset} ` +
98
- `${totalCoins > 0 ? c.green + '+⏣' + totalCoins.toLocaleString() + c.reset : DIM + '—' + c.reset}` +
99
- ` ${totalCmds} cmds ${formatUptime()} ${memMB}MB`
190
+ const extra = Math.max(0, W - 74);
191
+ const namePad = 16 + Math.floor(extra * 0.5);
192
+
193
+ console.log(
194
+ ` ${c.bold}Σ${c.reset} ` +
195
+ `${DIM}${' '.repeat(cols.num)}${c.reset} ` +
196
+ `${c.bold}${_workers.length} accounts${c.reset}` +
197
+ `${' '.repeat(Math.max(0, namePad - 12))}` +
198
+ `${String(totalCmds).padStart(cols.cmds)} ` +
199
+ `${String(rate).padStart(cols.ok)}% ` +
200
+ `${totalCoins > 0 ? c.green + '+' + totalCoins.toLocaleString() + c.reset : DIM + '—' + c.reset}` +
201
+ `${' '.repeat(Math.max(0, cols.earned - String(totalCoins).length - 1))}` +
202
+ ` ${DIM}${fmtUptime()} | ${memMB}MB${c.reset}`
100
203
  );
101
- lines.push('');
102
-
103
- // Redraw in place
104
- process.stdout.write(`\x1b[${_lineCount}A`);
105
- process.stdout.write(lines.join('\n') + '\n');
106
- _lineCount = lines.length - 1;
204
+ console.log('');
107
205
  }
108
206
 
109
- function _clear() {
110
- if (_lineCount > 0) {
111
- process.stdout.write(`\x1b[${_lineCount}A`);
112
- for (let i = 0; i < _lineCount; i++) {
113
- process.stdout.write(`\x1b[2K\r${i < _lineCount - 1 ? '\x1b[1A' : ''}`);
114
- }
115
- _lineCount = 0;
116
- }
207
+ // ── Event log ────────────────────────────────────────────────
208
+ let _eventCount = 0;
209
+ const MAX_EVENTS = 6;
210
+
211
+ function logEvent(msg) {
212
+ const now = new Date();
213
+ const ts = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
214
+ process.stdout.write(` ${DIM}[${ts}]${c.reset} ${msg}\n`);
215
+ _eventCount++;
216
+ if (_eventCount > MAX_EVENTS) _eventCount = MAX_EVENTS;
117
217
  }
118
218
 
119
219
  // ── Public API ────────────────────────────────────────────────
@@ -122,34 +222,26 @@ function init({ workers, isShuttingDown }) {
122
222
  _startTime = Date.now();
123
223
  _workers = workers;
124
224
  _isShuttingDown = isShuttingDown || (() => false);
225
+ _eventCount = 0;
125
226
  }
126
227
 
127
- function drawBanner(version) {
128
- const title = `DANKGRINDER v${version}`;
129
- const gradient = gradientLine(title, [77, 142, 255], [255, 92, 147]);
130
- console.log('');
131
- console.log(` ${gradient}${c.reset}`);
132
- console.log(` ${DIM}Ctrl+C to stop${c.reset}`);
133
- console.log('');
134
- }
228
+ function drawBanner(version) {}
229
+
230
+ let _interval = null;
135
231
 
136
232
  function start() {
137
- if (_workers.length > 30) return; // too many accounts — stay quiet
138
- _lineCount = 0;
139
- _render();
140
- _interval = setInterval(() => {
141
- if (_isShuttingDown()) {
142
- clearInterval(_interval);
143
- _interval = null;
144
- return;
145
- }
146
- _render();
147
- }, 10_000);
233
+ // No periodic refresh dashboard re-draws on each event
234
+ }
235
+
236
+ function draw() {
237
+ if (_isShuttingDown()) return;
238
+ const info = drawHeader(require('../package.json').version);
239
+ drawRows(info);
148
240
  }
149
241
 
150
242
  function stop() {
151
243
  if (_interval) { clearInterval(_interval); _interval = null; }
152
- _clear();
244
+ process.stdout.write(c.reset + '\n');
153
245
  }
154
246
 
155
- module.exports = { init, drawBanner, start, stop };
247
+ module.exports = { init, drawBanner, start, draw, logEvent, workerColor, stop };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.34.0",
3
+ "version": "8.37.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"