dankgrinder 8.36.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 +30 -23
  2. package/lib/ui.js +191 -130
  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,8 +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
- ui.start(); // show dashboard before login
2787
- ui.printLogin(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
2793
+ ui.draw(); // draw initial table
2794
+ ui.logEvent(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
2788
2795
  const BATCH_SIZE = 10;
2789
2796
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
2790
2797
  if (shutdownCalled) break;
@@ -2799,21 +2806,21 @@ async function start(apiKey, apiUrl, opts = {}) {
2799
2806
  await worker.start();
2800
2807
  if (worker._tokenInvalid) {
2801
2808
  worker.lastStatus = 'invalid token';
2802
- ui.printLogin(` ${c.red}✗${c.reset} [${i + idx + 1}] ${acc.label || acc.id}: 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
2811
  worker.lastStatus = 'ready';
2805
- ui.printLogin(` ${c.green}✓${c.reset} [${i + idx + 1}] ${worker.username}`);
2806
- ui.render();
2812
+ ui.logEvent(`${c.green}·${c.reset} [${i + idx + 1}] ${worker.username} connected`);
2813
+ ui.draw();
2807
2814
  } else {
2808
2815
  worker.lastStatus = 'timeout';
2809
- ui.printLogin(` ${c.yellow}✗${c.reset} [${i + idx + 1}] ${acc.label || acc.id}: timeout`);
2816
+ ui.logEvent(`${c.yellow}·${c.reset} [${i + idx + 1}] ${acc.label || acc.id} timeout`);
2810
2817
  }
2811
2818
  } catch (e) {
2812
- ui.printLogin(` ${c.red}✗${c.reset} [${i + idx + 1}] ERROR: ${e.message}`);
2819
+ ui.logEvent(`${c.red}E${c.reset} [${i + idx + 1}] ERROR: ${e.message}`);
2813
2820
  }
2814
2821
  }));
2815
2822
  } catch (e) {
2816
- ui.printLogin(`${c.red}BATCH ERROR: ${e.message}${c.reset}`);
2823
+ ui.logEvent(`${c.red}!${c.reset} BATCH ERROR: ${e.message}`);
2817
2824
  }
2818
2825
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
2819
2826
  hintGC();
@@ -2822,32 +2829,32 @@ async function start(apiKey, apiUrl, opts = {}) {
2822
2829
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
2823
2830
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
2824
2831
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
2825
- ui.printLogin(`${c.green}Login complete: ${loginDone}/${accounts.length} connected${c.reset}`);
2832
+ ui.logEvent(`${c.green}·${c.reset} Login: ${loginDone}/${accounts.length} connected`);
2826
2833
  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}`);
2834
+ for (const w of invalidWorkers) ui.logEvent(`${c.red}E${c.reset} invalid token: ${w.account.label || w.account.id}`);
2828
2835
  }
2829
2836
  if (timedOutWorkers.length > 0) {
2830
- ui.printLogin(` ${c.yellow}WARN${c.reset} ${timedOutWorkers.length} timed out (will retry in background)`);
2837
+ ui.logEvent(`${c.yellow}~${c.reset} ${timedOutWorkers.length} timed out (will retry in background)`);
2831
2838
  }
2832
- ui.clearLoginLines();
2839
+ ui.draw();
2833
2840
 
2834
2841
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
2835
2842
 
2836
2843
  // ── Phase 2: Inventory check ────────────────────────────────────
2837
- ui.printLogin(`${c.dim}Checking inventory...${c.reset}`);
2844
+ ui.logEvent(`${c.dim}Checking inventory...${c.reset}`);
2838
2845
  let invFailed = 0;
2839
2846
  await Promise.all(activeWorkers.map(async (w) => {
2840
2847
  try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
2841
2848
  catch { invFailed++; }
2842
2849
  }));
2843
2850
  if (invFailed > 0) {
2844
- ui.printLogin(`${c.red}Inventory failed for ${invFailed} accounts${c.reset}`);
2851
+ ui.logEvent(`${c.red}!${c.reset} Inventory failed for ${invFailed} accounts`);
2845
2852
  } else {
2846
- ui.printLogin(`${c.green}Inventory OK${c.reset}`);
2853
+ ui.logEvent(`${c.green}·${c.reset} Inventory OK`);
2847
2854
  }
2848
2855
 
2849
2856
  // ── Phase 2.5: Balance check ───────────────────────────────────
2850
- ui.printLogin(`${c.dim}Checking balances...${c.reset}`);
2857
+ ui.logEvent(`${c.dim}Checking balances...${c.reset}`);
2851
2858
  for (const w of activeWorkers) {
2852
2859
  try { await w.checkBalance(true); } catch {}
2853
2860
  }
@@ -2856,10 +2863,10 @@ async function start(apiKey, apiUrl, opts = {}) {
2856
2863
  totalCoins += w.stats?.balance || 0;
2857
2864
  totalCoins += w.stats?.bankBalance || 0;
2858
2865
  }
2859
- ui.printLogin(`Balances: total ${c.green}+⏣${totalCoins.toLocaleString()}${c.reset} across ${activeWorkers.length} accounts`);
2866
+ ui.logEvent(`${c.blue}$${c.reset} Balances: ${c.green}+⏣${totalCoins.toLocaleString()}${c.reset} across ${activeWorkers.length} accounts`);
2860
2867
 
2861
2868
  // ── Phase 2.75: DM history check ────────────────────────────────
2862
- ui.printLogin(`${c.dim}Checking DM history...${c.reset}`);
2869
+ ui.logEvent(`${c.dim}Checking DM history...${c.reset}`);
2863
2870
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
2864
2871
  for (const w of activeWorkers) {
2865
2872
  try {
@@ -2877,18 +2884,18 @@ async function start(apiKey, apiUrl, opts = {}) {
2877
2884
  }
2878
2885
  } catch {}
2879
2886
  }
2880
- if (dmNoLs.length > 0) ui.printLogin(` ${c.yellow}WARN${c.reset} No lifesavers: ${dmNoLs.join(', ')}`);
2887
+ if (dmNoLs.length > 0) ui.logEvent(`${c.red}E${c.reset} No lifesavers: ${dmNoLs.join(', ')}`);
2881
2888
  const parts = [];
2882
2889
  if (dmDeaths > 0) parts.push(`${dmDeaths} deaths`);
2883
2890
  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();
2891
+ ui.logEvent(`${c.green}·${c.reset} DM: ${parts.length > 0 ? parts.join(', ') : c.green + 'clean' + c.reset}`);
2886
2892
 
2887
2893
  // ── Phase 3: Start grind loops ───────────────────────────────────
2894
+ ui.logEvent(`${c.green}·${c.reset} Starting ${activeWorkers.length} grind loops...`);
2888
2895
  for (const w of activeWorkers) {
2889
2896
  if (!shutdownCalled) w.grindLoop();
2890
2897
  }
2891
- ui.render();
2898
+ ui.draw();
2892
2899
 
2893
2900
  // Cluster heartbeat — lets other nodes see this node is alive
2894
2901
  if (CLUSTER_ENABLED) {
package/lib/ui.js CHANGED
@@ -1,131 +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
- 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}`;
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) + '…';
57
90
  }
58
91
 
59
- function _render(extraLines = []) {
60
- 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));
61
97
  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}`
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}`
71
125
  );
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);
80
- const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
81
-
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)}%`
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`
85
176
  );
86
177
  }
87
178
 
88
- for (const el of extraLines) lines.push(el);
89
-
90
- lines.push(`${c.bold}${'─'.repeat(W)}${c.reset}`);
179
+ console.log(` ${hr}`);
91
180
 
92
181
  // Totals
93
- let totalCoins = 0, totalCmds = 0;
94
- for (const w of workers) {
182
+ let totalCoins = 0, totalCmds = 0, totalOk = 0;
183
+ for (const w of _workers) {
95
184
  totalCoins += w.stats.coins || 0;
96
185
  totalCmds += w.stats.commands || 0;
186
+ totalOk += w.stats.successes || 0;
97
187
  }
188
+ const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
98
189
  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`
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}`
103
203
  );
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;
204
+ console.log('');
117
205
  }
118
206
 
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
- }
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;
129
217
  }
130
218
 
131
219
  // ── Public API ────────────────────────────────────────────────
@@ -134,53 +222,26 @@ function init({ workers, isShuttingDown }) {
134
222
  _startTime = Date.now();
135
223
  _workers = workers;
136
224
  _isShuttingDown = isShuttingDown || (() => false);
225
+ _eventCount = 0;
137
226
  }
138
227
 
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
- }
228
+ function drawBanner(version) {}
229
+
230
+ let _interval = null;
147
231
 
148
- // Show dashboard and start the 10s refresh interval
149
232
  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);
233
+ // No periodic refresh — dashboard re-draws on each event
162
234
  }
163
235
 
164
- // Immediately re-render the dashboard (use after worker status changes)
165
- function render() {
236
+ function draw() {
166
237
  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();
238
+ const info = drawHeader(require('../package.json').version);
239
+ drawRows(info);
179
240
  }
180
241
 
181
242
  function stop() {
182
243
  if (_interval) { clearInterval(_interval); _interval = null; }
183
- _clear();
244
+ process.stdout.write(c.reset + '\n');
184
245
  }
185
246
 
186
- module.exports = { init, drawBanner, start, render, printLogin, clearLoginLines, 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.36.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"