dankgrinder 8.37.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 +21 -25
  2. package/lib/ui.js +95 -141
  3. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -2755,8 +2755,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2755
2755
  w.setCooldown?.('crime', 86400);
2756
2756
  w.setCooldown?.('search', 86400);
2757
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();
2758
+ ui.log(`${c.red}E${c.reset} DEATH — ${w.username} has 0 lifesavers! Crime/search disabled`);
2760
2759
  } else {
2761
2760
  w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
2762
2761
  if (prev !== event.lifesaversLeft) {
@@ -2766,8 +2765,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2766
2765
  if (event.lifesaversLeft <= 2) {
2767
2766
  w._alert = { type: 'lowls' };
2768
2767
  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();
2768
+ ui.log(`${c.yellow}!${c.reset} ${w.username} — only ${event.lifesaversLeft} lifesavers!`);
2771
2769
  }
2772
2770
  }
2773
2771
  }
@@ -2776,7 +2774,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2776
2774
  if (event.type === 'levelup') {
2777
2775
  if (event.to > 0) {
2778
2776
  w._level = event.to;
2779
- ui.logEvent(`${c.blue}↑${c.reset} ${w.username} leveled up to ${event.to}`);
2777
+ ui.log(`${c.blue}↑${c.reset} ${w.username} leveled up to ${event.to}`);
2780
2778
  }
2781
2779
  }
2782
2780
  }
@@ -2790,8 +2788,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2790
2788
  const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
2791
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));
2792
2790
 
2793
- ui.draw(); // draw initial table
2794
- ui.logEvent(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
2791
+ ui.log(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
2795
2792
  const BATCH_SIZE = 10;
2796
2793
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
2797
2794
  if (shutdownCalled) break;
@@ -2806,21 +2803,20 @@ async function start(apiKey, apiUrl, opts = {}) {
2806
2803
  await worker.start();
2807
2804
  if (worker._tokenInvalid) {
2808
2805
  worker.lastStatus = 'invalid token';
2809
- ui.logEvent(`${c.red}E${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`);
2810
2807
  } else if (worker.channel) {
2811
2808
  worker.lastStatus = 'ready';
2812
- ui.logEvent(`${c.green}·${c.reset} [${i + idx + 1}] ${worker.username} connected`);
2813
- ui.draw();
2809
+ ui.log(`${c.green}·${c.reset} [${i + idx + 1}] ${worker.username} connected`);
2814
2810
  } else {
2815
2811
  worker.lastStatus = 'timeout';
2816
- ui.logEvent(`${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`);
2817
2813
  }
2818
2814
  } catch (e) {
2819
- ui.logEvent(`${c.red}E${c.reset} [${i + idx + 1}] ERROR: ${e.message}`);
2815
+ ui.log(`${c.red}E${c.reset} [${i + idx + 1}] ERROR: ${e.message}`);
2820
2816
  }
2821
2817
  }));
2822
2818
  } catch (e) {
2823
- ui.logEvent(`${c.red}!${c.reset} BATCH ERROR: ${e.message}`);
2819
+ ui.log(`${c.red}!${c.reset} BATCH ERROR: ${e.message}`);
2824
2820
  }
2825
2821
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
2826
2822
  hintGC();
@@ -2829,32 +2825,32 @@ async function start(apiKey, apiUrl, opts = {}) {
2829
2825
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
2830
2826
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
2831
2827
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
2832
- ui.logEvent(`${c.green}·${c.reset} Login: ${loginDone}/${accounts.length} connected`);
2828
+ ui.log(`${c.green}·${c.reset} Login: ${loginDone}/${accounts.length} connected`);
2833
2829
  if (invalidWorkers.length > 0) {
2834
- for (const w of invalidWorkers) ui.logEvent(`${c.red}E${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}`);
2835
2831
  }
2836
2832
  if (timedOutWorkers.length > 0) {
2837
- ui.logEvent(`${c.yellow}~${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)`);
2838
2834
  }
2839
2835
  ui.draw();
2840
2836
 
2841
2837
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
2842
2838
 
2843
2839
  // ── Phase 2: Inventory check ────────────────────────────────────
2844
- ui.logEvent(`${c.dim}Checking inventory...${c.reset}`);
2840
+ ui.log(`${c.dim}Checking inventory...${c.reset}`);
2845
2841
  let invFailed = 0;
2846
2842
  await Promise.all(activeWorkers.map(async (w) => {
2847
2843
  try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
2848
2844
  catch { invFailed++; }
2849
2845
  }));
2850
2846
  if (invFailed > 0) {
2851
- ui.logEvent(`${c.red}!${c.reset} Inventory failed for ${invFailed} accounts`);
2847
+ ui.log(`${c.red}!${c.reset} Inventory failed for ${invFailed} accounts`);
2852
2848
  } else {
2853
- ui.logEvent(`${c.green}·${c.reset} Inventory OK`);
2849
+ ui.log(`${c.green}·${c.reset} Inventory OK`);
2854
2850
  }
2855
2851
 
2856
2852
  // ── Phase 2.5: Balance check ───────────────────────────────────
2857
- ui.logEvent(`${c.dim}Checking balances...${c.reset}`);
2853
+ ui.log(`${c.dim}Checking balances...${c.reset}`);
2858
2854
  for (const w of activeWorkers) {
2859
2855
  try { await w.checkBalance(true); } catch {}
2860
2856
  }
@@ -2863,10 +2859,10 @@ async function start(apiKey, apiUrl, opts = {}) {
2863
2859
  totalCoins += w.stats?.balance || 0;
2864
2860
  totalCoins += w.stats?.bankBalance || 0;
2865
2861
  }
2866
- ui.logEvent(`${c.blue}$${c.reset} Balances: ${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`);
2867
2863
 
2868
2864
  // ── Phase 2.75: DM history check ────────────────────────────────
2869
- ui.logEvent(`${c.dim}Checking DM history...${c.reset}`);
2865
+ ui.log(`${c.dim}Checking DM history...${c.reset}`);
2870
2866
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
2871
2867
  for (const w of activeWorkers) {
2872
2868
  try {
@@ -2884,14 +2880,14 @@ async function start(apiKey, apiUrl, opts = {}) {
2884
2880
  }
2885
2881
  } catch {}
2886
2882
  }
2887
- if (dmNoLs.length > 0) ui.logEvent(`${c.red}E${c.reset} No lifesavers: ${dmNoLs.join(', ')}`);
2883
+ if (dmNoLs.length > 0) ui.log(`${c.red}E${c.reset} No lifesavers: ${dmNoLs.join(', ')}`);
2888
2884
  const parts = [];
2889
2885
  if (dmDeaths > 0) parts.push(`${dmDeaths} deaths`);
2890
2886
  if (dmLevelUps > 0) parts.push(`${dmLevelUps} level-ups`);
2891
- ui.logEvent(`${c.green}·${c.reset} DM: ${parts.length > 0 ? parts.join(', ') : c.green + 'clean' + c.reset}`);
2887
+ ui.log(`${c.green}·${c.reset} DM: ${parts.length > 0 ? parts.join(', ') : c.green + 'clean' + c.reset}`);
2892
2888
 
2893
2889
  // ── Phase 3: Start grind loops ───────────────────────────────────
2894
- ui.logEvent(`${c.green}·${c.reset} Starting ${activeWorkers.length} grind loops...`);
2890
+ ui.log(`${c.green}·${c.reset} Starting ${activeWorkers.length} grind loops...`);
2895
2891
  for (const w of activeWorkers) {
2896
2892
  if (!shutdownCalled) w.grindLoop();
2897
2893
  }
package/lib/ui.js CHANGED
@@ -1,14 +1,43 @@
1
1
  /**
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.
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
+ ];
10
26
 
11
- // ── Dark-theme palette (light/bright colors on dark bg) ───────
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 ────────────────────────────────────────
12
41
  const PALETTE = [
13
42
  '\x1b[38;2;77;212;238m', // cyan
14
43
  '\x1b[38;2;255;194;77m', // amber
@@ -18,13 +47,12 @@ const PALETTE = [
18
47
  '\x1b[38;2;255;180;80m', // orange
19
48
  '\x1b[38;2;180;130;255m', // lavender
20
49
  '\x1b[38;2;100;255;180m', // teal
21
- '\x1b[38;2;255;150;100m', // peach
50
+ '\x1b[38;2;255;150;100m', // peach
22
51
  '\x1b[38;2;150;255;200m', // mint
23
52
  '\x1b[38;2;255;255;120m', // yellow
24
53
  '\x1b[38;2;200;150;255m', // violet
25
54
  ];
26
-
27
- function workerColor(idx) { return PALETTE[idx % PALETTE.length]; }
55
+ function wc(idx) { return PALETTE[idx % PALETTE.length]; }
28
56
 
29
57
  // ── ANSI helpers ──────────────────────────────────────────────
30
58
  const c = {
@@ -34,12 +62,13 @@ const c = {
34
62
  yellow: '\x1b[38;2;255;220;80m',
35
63
  cyan: '\x1b[38;2;80;220;255m',
36
64
  blue: '\x1b[38;2;100;160;255m',
37
- gray: '\x1b[38;2;120;130;150m',
38
- white: '\x1b[38;2;220;220;240m',
39
65
  };
40
66
  const DIM = c.dim;
41
67
 
42
- // ── Uptime ───────────────────────────────────────────────────
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); }
71
+
43
72
  function fmtUptime() {
44
73
  const s = Math.floor((Date.now() - _startTime) / 1000);
45
74
  if (s < 60) return `${s}s`;
@@ -51,26 +80,6 @@ function fmtUptime() {
51
80
  return `${m}m`;
52
81
  }
53
82
 
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
- }
72
-
73
- // ── Status icon ─────────────────────────────────────────────
74
83
  function icon(w) {
75
84
  if (!w.running || !w.channel) return `${c.red}E${c.reset}`;
76
85
  if (w.paused || w.dashboardPaused) return `${c.yellow}P${c.reset}`;
@@ -82,103 +91,73 @@ function icon(w) {
82
91
  return `${c.green}·${c.reset}`;
83
92
  }
84
93
 
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) + '…';
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
+ });
90
102
  }
91
103
 
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));
104
+ // ── Draw full table (call once after login) ──────────────────
105
+ function draw() {
97
106
  const W = Math.min(process.stdout.columns || 120, 120);
98
-
99
- const cols = colWidths(W);
100
107
  const hr = '─'.repeat(W);
101
-
102
- const running = _workers.filter(w => w.running && w.channel).length;
108
+ const running = _workers.filter(w => w.running && w.channel && !w.paused && !w.dashboardPaused).length;
103
109
  const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
104
- const active = running + paused;
105
110
 
106
- // Top bar
111
+ console.log('\x1b[2J\x1b[H');
107
112
  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
113
 
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}`);
114
+ // Big banner with gradient
115
+ for (const line of BANNER) {
116
+ console.log(gradientLine(line, 77, 212, 238, 255, 92, 147));
113
117
  }
114
- console.log('');
115
118
 
116
- // Column headers
119
+ console.log('');
117
120
  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}`
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}`
127
+ );
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}`
125
136
  );
126
137
  console.log(` ${hr}`);
127
138
 
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
-
139
+ const sorted = sortedWorkers();
155
140
  for (let si = 0; si < sorted.length; si++) {
156
141
  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;
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;
163
145
  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;
146
+ const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
167
147
 
168
148
  process.stdout.write(
169
149
  ` ${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`
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`
176
156
  );
177
157
  }
178
158
 
179
159
  console.log(` ${hr}`);
180
160
 
181
- // Totals
182
161
  let totalCoins = 0, totalCmds = 0, totalOk = 0;
183
162
  for (const w of _workers) {
184
163
  totalCoins += w.stats.coins || 0;
@@ -187,61 +166,36 @@ function drawRows({ visible, W, cols }) {
187
166
  }
188
167
  const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
189
168
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
190
- const extra = Math.max(0, W - 74);
191
- const namePad = 16 + Math.floor(extra * 0.5);
192
169
 
193
170
  console.log(
194
171
  ` ${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)}% ` +
172
+ `${DIM}${padL(_workers.length, 2)} acc${c.reset} ` +
173
+ `${' '.repeat(18)}` +
174
+ `${DIM}${padL(totalCmds, 5)} cmds ${rate}% ` +
200
175
  `${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}`
176
+ `${' '.repeat(Math.max(0, 8 - String(totalCoins).length - 1))}${DIM}${fmtUptime()} | ${memMB}MB${c.reset}`
203
177
  );
178
+ console.log(` ${hr}`);
204
179
  console.log('');
205
180
  }
206
181
 
207
- // ── Event log ────────────────────────────────────────────────
208
- let _eventCount = 0;
209
- const MAX_EVENTS = 6;
210
-
211
- function logEvent(msg) {
182
+ // ── Append event log line ────────────────────────────────────
183
+ function log(msg) {
212
184
  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;
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`);
217
187
  }
218
188
 
219
189
  // ── Public API ────────────────────────────────────────────────
220
-
221
190
  function init({ workers, isShuttingDown }) {
222
191
  _startTime = Date.now();
223
192
  _workers = workers;
224
193
  _isShuttingDown = isShuttingDown || (() => false);
225
- _eventCount = 0;
194
+ _version = '0.0.0';
226
195
  }
227
196
 
228
- function drawBanner(version) {}
229
-
230
- let _interval = null;
231
-
232
- function start() {
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);
240
- }
241
-
242
- function stop() {
243
- if (_interval) { clearInterval(_interval); _interval = null; }
244
- process.stdout.write(c.reset + '\n');
245
- }
197
+ function drawBanner(version) { _version = version; }
198
+ function start() {}
199
+ function stop() { process.stdout.write(c.reset + '\n'); }
246
200
 
247
- module.exports = { init, drawBanner, start, draw, logEvent, workerColor, 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.37.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"