dankgrinder 8.38.0 → 8.40.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 +57 -26
  2. package/lib/ui.js +198 -98
  3. package/package.json +1 -1
package/lib/grinder.js CHANGED
@@ -260,14 +260,32 @@ function colorBanner() {
260
260
  return gradientLine(title, [77, 142, 255], [255, 92, 147]);
261
261
  }
262
262
 
263
- // ── Simple Logging ─────────────────────────────────────────────
263
+ // ── Simple Logging (routed through UI) ────────────────────────
264
+ const _logQueue = []; // buffers logs before UI is ready
265
+
264
266
  function log(type, msg, label) {
265
267
  const icons = {
266
268
  info: '.', success: '[OK]', error: '[X]', warn: '[!]',
267
269
  cmd: '>', coin: '$', buy: '#', bal: '*', debug: '.',
268
270
  };
269
- const tagCol = label ? `${label} ` : '';
270
- console.log(` ${icons[type] || icons.info} ${tagCol}${msg}`);
271
+ const icon = icons[type] || icons.info;
272
+ const text = label ? `${icon} ${label} ${msg}` : `${icon} ${msg}`;
273
+
274
+ // Drain queue: UI is ready (called after ui.init)
275
+ while (_logQueue.length > 0) {
276
+ const item = _logQueue.shift();
277
+ if (typeof ui !== 'undefined') {
278
+ try { ui.log(-1, item); } catch { process.stdout.write(item + '\n'); }
279
+ } else {
280
+ process.stdout.write(item + '\n');
281
+ }
282
+ }
283
+
284
+ if (typeof ui !== 'undefined') {
285
+ try { ui.log(-1, text); } catch { process.stdout.write(text + '\n'); }
286
+ } else {
287
+ _logQueue.push(text);
288
+ }
271
289
  }
272
290
 
273
291
  async function fetchConfig(retries = 3, delayMs = 1500, opts = {}) {
@@ -694,7 +712,15 @@ class AccountWorker {
694
712
  log(type, msg) {
695
713
  const stripped = msg.replace(/\x1b\[[0-9;]*m/g, '');
696
714
  if (type !== 'debug') this.lastStatus = stripped.substring(0, 28);
697
- log(type, msg, this.tag);
715
+ // Route through UI
716
+ const icons = { info: '.', success: '[OK]', error: '[X]', warn: '[!]', cmd: '>', coin: '$', buy: '#', bal: '*', debug: '.' };
717
+ const icon = icons[type] || icons.info;
718
+ const text = `${icon} ${this.username} ${msg}`;
719
+ if (typeof ui !== 'undefined') {
720
+ try { ui.log(this.idx, text); } catch { process.stdout.write(text + '\n'); }
721
+ } else {
722
+ process.stdout.write(text + '\n');
723
+ }
698
724
  }
699
725
 
700
726
  setStatus(text) {
@@ -2448,7 +2474,12 @@ class AccountWorker {
2448
2474
  }
2449
2475
 
2450
2476
  async start() {
2451
- process.stdout.write(`[WORKER START] ${this.account.label || this.account.id}\n`);
2477
+ // Worker start route through UI
2478
+ if (typeof ui !== 'undefined') {
2479
+ try { ui.log(this.idx, `starting...`); } catch { process.stdout.write(`[WORKER START] ${this.account.label || this.account.id}\n`); }
2480
+ } else {
2481
+ process.stdout.write(`[WORKER START] ${this.account.label || this.account.id}\n`);
2482
+ }
2452
2483
  if (!this.account.discord_token) { this.log('error', 'No token'); return; }
2453
2484
  if (!this.account.channel_id) { this.log('error', 'No channel'); return; }
2454
2485
 
@@ -2755,7 +2786,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2755
2786
  w.setCooldown?.('crime', 86400);
2756
2787
  w.setCooldown?.('search', 86400);
2757
2788
  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`);
2789
+ ui.log(workers.indexOf(w), `${c.red}E${c.reset} DEATH — 0 lifesavers! Crime/search disabled`);
2759
2790
  } else {
2760
2791
  w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
2761
2792
  if (prev !== event.lifesaversLeft) {
@@ -2765,7 +2796,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2765
2796
  if (event.lifesaversLeft <= 2) {
2766
2797
  w._alert = { type: 'lowls' };
2767
2798
  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!`);
2799
+ ui.log(workers.indexOf(w), `⚠ ${event.lifesaversLeft} lifesavers left!`);
2769
2800
  }
2770
2801
  }
2771
2802
  }
@@ -2774,7 +2805,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2774
2805
  if (event.type === 'levelup') {
2775
2806
  if (event.to > 0) {
2776
2807
  w._level = event.to;
2777
- ui.log(`${c.blue}↑${c.reset} ${w.username} leveled up to ${event.to}`);
2808
+ ui.log(workers.indexOf(w), `↑ level ${event.to}`);
2778
2809
  }
2779
2810
  }
2780
2811
  }
@@ -2788,7 +2819,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2788
2819
  const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
2789
2820
  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));
2790
2821
 
2791
- ui.log(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
2822
+ ui.log(-1, `Logging in ${accounts.length} accounts...`);
2792
2823
  const BATCH_SIZE = 10;
2793
2824
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
2794
2825
  if (shutdownCalled) break;
@@ -2803,20 +2834,20 @@ async function start(apiKey, apiUrl, opts = {}) {
2803
2834
  await worker.start();
2804
2835
  if (worker._tokenInvalid) {
2805
2836
  worker.lastStatus = 'invalid token';
2806
- ui.log(`${c.red}E${c.reset} [${i + idx + 1}] ${acc.label || acc.id} — invalid token`);
2837
+ ui.log(worker.idx, `✗ invalid token`);
2807
2838
  } else if (worker.channel) {
2808
2839
  worker.lastStatus = 'ready';
2809
- ui.log(`${c.green}·${c.reset} [${i + idx + 1}] ${worker.username} connected`);
2840
+ ui.log(worker.idx, `✓ connected`);
2810
2841
  } else {
2811
2842
  worker.lastStatus = 'timeout';
2812
- ui.log(`${c.yellow}·${c.reset} [${i + idx + 1}] ${acc.label || acc.id} — timeout`);
2843
+ ui.log(worker.idx, `✗ timeout`);
2813
2844
  }
2814
2845
  } catch (e) {
2815
- ui.log(`${c.red}E${c.reset} [${i + idx + 1}] ERROR: ${e.message}`);
2846
+ ui.log(i + idx, `ERROR: ${e.message}`);
2816
2847
  }
2817
2848
  }));
2818
2849
  } catch (e) {
2819
- ui.log(`${c.red}!${c.reset} BATCH ERROR: ${e.message}`);
2850
+ ui.log(-1, `BATCH ERROR: ${e.message}`);
2820
2851
  }
2821
2852
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
2822
2853
  hintGC();
@@ -2825,32 +2856,32 @@ async function start(apiKey, apiUrl, opts = {}) {
2825
2856
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
2826
2857
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
2827
2858
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
2828
- ui.log(`${c.green}·${c.reset} Login: ${loginDone}/${accounts.length} connected`);
2859
+ ui.log(-1, `Login: ${loginDone}/${accounts.length} connected`);
2829
2860
  if (invalidWorkers.length > 0) {
2830
- for (const w of invalidWorkers) ui.log(`${c.red}E${c.reset} invalid token: ${w.account.label || w.account.id}`);
2861
+ for (const w of invalidWorkers) ui.log(workers.indexOf(w), `✗ invalid token`);
2831
2862
  }
2832
2863
  if (timedOutWorkers.length > 0) {
2833
- ui.log(`${c.yellow}~${c.reset} ${timedOutWorkers.length} timed out (will retry in background)`);
2864
+ ui.log(-1, `${timedOutWorkers.length} timed out (retrying in background)`);
2834
2865
  }
2835
2866
  ui.draw();
2836
2867
 
2837
2868
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
2838
2869
 
2839
2870
  // ── Phase 2: Inventory check ────────────────────────────────────
2840
- ui.log(`${c.dim}Checking inventory...${c.reset}`);
2871
+ ui.log(-1, `Checking inventory...`);
2841
2872
  let invFailed = 0;
2842
2873
  await Promise.all(activeWorkers.map(async (w) => {
2843
2874
  try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
2844
2875
  catch { invFailed++; }
2845
2876
  }));
2846
2877
  if (invFailed > 0) {
2847
- ui.log(`${c.red}!${c.reset} Inventory failed for ${invFailed} accounts`);
2878
+ ui.log(-1, `Inventory failed for ${invFailed} accounts`);
2848
2879
  } else {
2849
- ui.log(`${c.green}·${c.reset} Inventory OK`);
2880
+ ui.log(-1, `Inventory OK`);
2850
2881
  }
2851
2882
 
2852
2883
  // ── Phase 2.5: Balance check ───────────────────────────────────
2853
- ui.log(`${c.dim}Checking balances...${c.reset}`);
2884
+ ui.log(-1, `Checking balances...`);
2854
2885
  for (const w of activeWorkers) {
2855
2886
  try { await w.checkBalance(true); } catch {}
2856
2887
  }
@@ -2859,10 +2890,10 @@ async function start(apiKey, apiUrl, opts = {}) {
2859
2890
  totalCoins += w.stats?.balance || 0;
2860
2891
  totalCoins += w.stats?.bankBalance || 0;
2861
2892
  }
2862
- ui.log(`${c.blue}$${c.reset} Balances: ${c.green}+⏣${totalCoins.toLocaleString()}${c.reset} across ${activeWorkers.length} accounts`);
2893
+ ui.log(-1, `Balance: +⏣${totalCoins.toLocaleString()} across ${activeWorkers.length} accounts`);
2863
2894
 
2864
2895
  // ── Phase 2.75: DM history check ────────────────────────────────
2865
- ui.log(`${c.dim}Checking DM history...${c.reset}`);
2896
+ ui.log(-1, `Checking DM history...`);
2866
2897
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
2867
2898
  for (const w of activeWorkers) {
2868
2899
  try {
@@ -2880,14 +2911,14 @@ async function start(apiKey, apiUrl, opts = {}) {
2880
2911
  }
2881
2912
  } catch {}
2882
2913
  }
2883
- if (dmNoLs.length > 0) ui.log(`${c.red}E${c.reset} No lifesavers: ${dmNoLs.join(', ')}`);
2914
+ if (dmNoLs.length > 0) ui.log(-1, `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
2884
2915
  const parts = [];
2885
2916
  if (dmDeaths > 0) parts.push(`${dmDeaths} deaths`);
2886
2917
  if (dmLevelUps > 0) parts.push(`${dmLevelUps} level-ups`);
2887
- ui.log(`${c.green}·${c.reset} DM: ${parts.length > 0 ? parts.join(', ') : c.green + 'clean' + c.reset}`);
2918
+ ui.log(-1, `DM: ${parts.length > 0 ? parts.join(', ') : 'clean'}`);
2888
2919
 
2889
2920
  // ── Phase 3: Start grind loops ───────────────────────────────────
2890
- ui.log(`${c.green}·${c.reset} Starting ${activeWorkers.length} grind loops...`);
2921
+ ui.log(-1, `Starting ${activeWorkers.length} grind loops...`);
2891
2922
  for (const w of activeWorkers) {
2892
2923
  if (!shutdownCalled) w.grindLoop();
2893
2924
  }
package/lib/ui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * CLI Live Dashboard — one table, append-only events below.
3
- * Draws once after login. Events stream below without touching the table.
2
+ * CLI Live Dashboard — box design, loading animations, per-account status.
3
+ * Everything inside a single box. Events stream below. No duplicate output.
4
4
  */
5
5
 
6
6
  let _startTime = Date.now();
@@ -8,8 +8,12 @@ let _workers = [];
8
8
  let _isShuttingDown = () => false;
9
9
  let _version = '0.0.0';
10
10
 
11
+ // ── Spinner frames ────────────────────────────────────────────
12
+ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
13
+ function spinnerFrame() { return SPINNER[Math.floor(Date.now() / 150) % SPINNER.length]; }
14
+
11
15
  // ── Big ASCII art banner ──────────────────────────────────────
12
- const BANNER = [
16
+ const BANNER_LINES = [
13
17
  ' ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗',
14
18
  ' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║',
15
19
  ' ██║ ██║██║ ██║██╔██╗ ██║██║ ███╗█████╗ ██║ ██║██╔██╗ ██║',
@@ -24,33 +28,21 @@ const BANNER = [
24
28
  ' ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝',
25
29
  ];
26
30
 
27
- // Gradient each banner line cyan→pink
28
31
  function gradientLine(text, r1, g1, b1, r2, g2, b2) {
29
32
  let out = '';
30
33
  for (let i = 0; i < text.length; i++) {
31
34
  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]}`;
35
+ out += `\x1b[38;2;${Math.round(r1 + (r2 - r1) * t)};${Math.round(g1 + (g2 - g1) * t)};${Math.round(b1 + (b2 - b1) * t)}m${text[i]}`;
36
36
  }
37
37
  return out + '\x1b[0m';
38
38
  }
39
39
 
40
- // ── Dark-theme palette ────────────────────────────────────────
40
+ // ── Palette ───────────────────────────────────────────────────
41
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
42
+ '\x1b[38;2;77;212;238m', '\x1b[38;2;255;194;77m', '\x1b[38;2;130;210;100m',
43
+ '\x1b[38;2;255;120;200m', '\x1b[38;2;120;180;255m', '\x1b[38;2;255;180;80m',
44
+ '\x1b[38;2;180;130;255m', '\x1b[38;2;100;255;180m', '\x1b[38;2;255;150;100m',
45
+ '\x1b[38;2;150;255;200m', '\x1b[38;2;255;255;120m', '\x1b[38;2;200;150;255m',
54
46
  ];
55
47
  function wc(idx) { return PALETTE[idx % PALETTE.length]; }
56
48
 
@@ -67,7 +59,12 @@ const DIM = c.dim;
67
59
 
68
60
  function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
69
61
  function padR(s, n) { return trunc(s, n).padEnd(n); }
70
- function padL(s, n) { return String(s).padStart(n); }
62
+ function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
63
+ function padC(s, n) {
64
+ s = String(s || '');
65
+ const pad = Math.max(0, n - s.length);
66
+ return ' '.repeat(Math.floor(pad / 2)) + s + ' '.repeat(Math.ceil(pad / 2));
67
+ }
71
68
 
72
69
  function fmtUptime() {
73
70
  const s = Math.floor((Date.now() - _startTime) / 1000);
@@ -80,84 +77,136 @@ function fmtUptime() {
80
77
  return `${m}m`;
81
78
  }
82
79
 
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}`;
80
+ // ── Status helpers ─────────────────────────────────────────────
81
+ function statusColor(w) {
82
+ if (!w.running || !w.channel) return c.red;
83
+ if (w.paused || w.dashboardPaused) return c.yellow;
84
+ if (w.busy || w._invRunning || w._sellRunning) return c.yellow;
85
+ if (Date.now() < w.globalCooldownUntil) return c.blue;
86
+ return c.green;
87
+ }
88
+
89
+ function statusText(w) {
90
+ if (!w.running || !w.channel) return 'ERROR';
91
+ if (w.paused) return 'PAUSED';
92
+ if (w.dashboardPaused) return 'HELD';
93
+ if (w._alert?.type === 'death') return 'DEAD';
94
+ if (w._alert?.type === 'lowls') return 'LOW LS';
95
+ if (w.busy || w._invRunning || w._sellRunning) return spinnerFrame() + ' BUSY';
96
+ if (Date.now() < w.globalCooldownUntil) {
97
+ const wait = Math.ceil((w.globalCooldownUntil - Date.now()) / 1000);
98
+ return '⏳' + wait + 's';
89
99
  }
90
- if (w._rateLimitHits > (w._prevRateLimits || 0)) return `${c.cyan}~${c.reset}`;
91
- return `${c.green}·${c.reset}`;
100
+ return '● READY';
92
101
  }
93
102
 
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
- });
103
+ // ── Layout ────────────────────────────────────────────────────
104
+ function layout() {
105
+ const W = Math.min(process.stdout.columns || 120, 120);
106
+ const rows = process.stdout.rows || 40;
107
+ const bannerH = BANNER_LINES.length + 1; // +1 for blank line
108
+ const statusH = 1;
109
+ const headerH = 3; // blank + headers + hr
110
+ const totalsH = 2; // totals + hr
111
+ const footerH = 1; // blank
112
+ const maxAccounts = Math.min(_workers.length, Math.max(3, rows - bannerH - statusH - headerH - totalsH - footerH - 8));
113
+ const eventH = Math.max(3, rows - bannerH - statusH - headerH - totalsH - footerH - maxAccounts);
114
+ return { W, bannerH, statusH, headerH, totalsH, footerH, maxAccounts, eventH };
102
115
  }
103
116
 
104
- // ── Draw full table (call once after login) ──────────────────
117
+ // ── Draw the full box ────────────────────────────────────────
105
118
  function draw() {
106
- const W = Math.min(process.stdout.columns || 120, 120);
107
- const hr = '─'.repeat(W);
119
+ const { W, bannerH, statusH, headerH, totalsH, footerH, maxAccounts, eventH } = layout();
120
+ const T = '─'.repeat(W - 2); // inner width
121
+
122
+ // ── Top of box ──
123
+ // Move cursor to top-left of box area and overwrite in place (no full clear)
124
+ process.stdout.write(`\x1b[${1};1H`);
125
+ process.stdout.write(`\x1b[0J`); // clear from cursor to end of screen
126
+ process.stdout.write(`\x1b[38;2;77;212;238m┌─${T}─┐\x1b[0m\n`);
127
+
128
+ // ── Banner inside box ──
129
+ for (const line of BANNER_LINES) {
130
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${gradientLine(line, 77, 212, 238, 255, 92, 147)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
131
+ }
132
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
133
+
134
+ // ── Status bar inside box ──
108
135
  const running = _workers.filter(w => w.running && w.channel && !w.paused && !w.dashboardPaused).length;
109
136
  const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
137
+ const errors = _workers.filter(w => !w.running || !w.channel).length;
138
+ const statusLine = [
139
+ `${c.bold}v${_version}${c.reset}`,
140
+ `${c.green}●${c.reset} online`,
141
+ `${fmtUptime()}`,
142
+ `${c.green}·${c.reset}${running}`,
143
+ `${c.yellow}~${c.reset}${paused}`,
144
+ errors > 0 ? `${c.red}E${c.reset}${errors}` : null,
145
+ `${DIM}Ctrl+C${c.reset}`,
146
+ ].filter(Boolean).join(' ');
147
+ const statusPad = W - 4 - statusLine.length;
148
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${statusLine}${' '.repeat(Math.max(0, statusPad))} \x1b[38;2;77;212;238m│\x1b[0m\n`);
149
+ process.stdout.write(`\x1b[38;2;77;212;238m├─${T}─┤\x1b[0m\n`);
110
150
 
111
- console.log('\x1b[2J\x1b[H');
112
- console.log('');
151
+ // ── Table header ──
152
+ const col = {
153
+ st: 8, // Status
154
+ name: 18, // Account name
155
+ last: 20, // Last command
156
+ cmds: 6, // Commands
157
+ ok: 4, // OK%
158
+ earned: 10, // Earned
159
+ };
160
+ const nameExtra = Math.max(0, W - 4 - col.st - col.name - col.last - col.cmds - col.ok - col.earned - 14);
161
+ col.name += nameExtra;
113
162
 
114
- // Big banner with gradient
115
- for (const line of BANNER) {
116
- console.log(gradientLine(line, 77, 212, 238, 255, 92, 147));
117
- }
163
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
164
+ process.stdout.write(`${c.bold}#${c.reset} `);
165
+ process.stdout.write(`${c.bold}${padR('STATUS', col.st)}${c.reset} `);
166
+ process.stdout.write(`${c.bold}${padR('ACCOUNT', col.name)}${c.reset} `);
167
+ process.stdout.write(`${c.bold}${padR('LAST COMMAND', col.last)}${c.reset} `);
168
+ process.stdout.write(`${c.bold}${padL('CMDS', col.cmds)}${c.reset} `);
169
+ process.stdout.write(`${c.bold}${padL('OK%', col.ok)}${c.reset} `);
170
+ process.stdout.write(`${c.bold}${padL('EARNED', col.earned)}${c.reset} `);
171
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
172
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
173
+
174
+ // ── Account rows ──
175
+ const sorted = [..._workers].sort((a, b) => {
176
+ if (!a.channel !== !b.channel) return !a.channel ? 1 : -1;
177
+ const aA = a.running && a.channel && !a.paused && !a.dashboardPaused;
178
+ const bA = b.running && b.channel && !b.paused && !b.dashboardPaused;
179
+ if (aA !== bA) return bA ? 1 : -1;
180
+ return (b.stats.commands || 0) - (a.stats.commands || 0);
181
+ });
182
+ const shown = sorted.slice(0, maxAccounts);
118
183
 
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}`
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}`
136
- );
137
- console.log(` ${hr}`);
138
-
139
- const sorted = sortedWorkers();
140
- for (let si = 0; si < sorted.length; si++) {
141
- const w = sorted[si];
184
+ for (let si = 0; si < shown.length; si++) {
185
+ const w = shown[si];
142
186
  const wi = _workers.indexOf(w);
143
- const col = wc(wi);
187
+ const col2 = wc(wi);
188
+ const stCol = statusColor(w);
189
+ const stText = statusText(w);
144
190
  const earned = w.stats.coins > 0 ? `${c.green}+${w.stats.coins.toLocaleString()}${c.reset}` : DIM + '—' + c.reset;
145
191
  const cmds = w.stats.commands || 0;
146
192
  const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
147
193
 
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`
156
- );
194
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
195
+ process.stdout.write(`${DIM}${padL(wi + 1, 2)}${c.reset} `);
196
+ process.stdout.write(`${stCol}${padR(stText, col.st)}${c.reset} `);
197
+ process.stdout.write(`${col2}${padR(w.username || '?', col.name)}${c.reset} `);
198
+ process.stdout.write(`${DIM}${padR(w.lastStatus || 'idle', col.last)}${c.reset} `);
199
+ process.stdout.write(`${padL(cmds, col.cmds)} `);
200
+ process.stdout.write(`${padL(rate, col.ok)}% `);
201
+ process.stdout.write(`${padL(earned, col.earned)} `);
202
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
157
203
  }
158
204
 
159
- console.log(` ${hr}`);
205
+ if (sorted.length > maxAccounts) {
206
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${DIM}+${sorted.length - maxAccounts} more accounts${' '.repeat(Math.max(0, W - 24 - String(sorted.length - maxAccounts).length))}${c.reset} \x1b[38;2;77;212;238m│\x1b[0m\n`);
207
+ }
160
208
 
209
+ // ── Totals ──
161
210
  let totalCoins = 0, totalCmds = 0, totalOk = 0;
162
211
  for (const w of _workers) {
163
212
  totalCoins += w.stats.coins || 0;
@@ -167,35 +216,86 @@ function draw() {
167
216
  const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
168
217
  const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
169
218
 
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}`
177
- );
178
- console.log(` ${hr}`);
179
- console.log('');
180
- }
219
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
220
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
221
+ process.stdout.write(`${c.bold}Σ${c.reset} `);
222
+ process.stdout.write(`${DIM}${padL(_workers.length, 2)} acc${c.reset} `);
223
+ process.stdout.write(`${' '.repeat(col.name)} `);
224
+ process.stdout.write(`${DIM}${' '.repeat(col.last)}${c.reset} `);
225
+ process.stdout.write(`${padL(totalCmds, col.cmds)} `);
226
+ process.stdout.write(`${padL(rate, col.ok)}% `);
227
+ process.stdout.write(`${totalCoins > 0 ? c.green + padL('+' + totalCoins.toLocaleString(), col.earned) + c.reset : DIM + padL('—', col.earned) + c.reset} `);
228
+ process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(W - 4));
229
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
181
230
 
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`);
231
+ // ── Events section ──
232
+ process.stdout.write(`\x1b[38;2;77;212;238m├─${T}─┤\x1b[0m\n`);
233
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${c.bold}EVENTS${c.reset}${' '.repeat(Math.max(0, W - 10))}\x1b[38;2;77;212;238m│\x1b[0m\n`);
234
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${''.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
235
+
236
+ // Show recent events per account
237
+ const evLines = [];
238
+ for (let i = 0; i < _workers.length; i++) {
239
+ if (_eventLines[i] && _eventLines[i].length > 0) {
240
+ const latest = _eventLines[i][_eventLines[i].length - 1];
241
+ const col2 = wc(i);
242
+ const name = trunc(_workers[i]?.username || '?', 14);
243
+ evLines.push({ i, text: latest.text, ts: latest.ts, col: col2, name });
244
+ }
245
+ }
246
+
247
+ for (let i = 0; i < Math.min(eventH, evLines.length); i++) {
248
+ const ev = evLines[evLines.length - 1 - i];
249
+ const now = new Date();
250
+ const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
251
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
252
+ process.stdout.write(`${ev.col}${padR(ev.name, 14)}${c.reset} `);
253
+ process.stdout.write(`${DIM}[${ts}]${c.reset} ${ev.text}${' '.repeat(Math.max(0, W - 20 - ev.name.length - ev.text.length))}`);
254
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
255
+ }
256
+
257
+ for (let i = 0; i < Math.max(0, eventH - evLines.length); i++) {
258
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${' '.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
259
+ }
260
+
261
+ // ── Bottom of box ──
262
+ process.stdout.write(`\x1b[38;2;77;212;238m└─${T}─┘\x1b[0m\n`);
187
263
  }
188
264
 
265
+ // ── Event tracking ────────────────────────────────────────────
266
+ let _eventLines = []; // [accountIdx] = [{text, ts}]
267
+ const MAX_EVENTS = 3;
268
+
189
269
  // ── Public API ────────────────────────────────────────────────
270
+
190
271
  function init({ workers, isShuttingDown }) {
191
272
  _startTime = Date.now();
192
273
  _workers = workers;
193
274
  _isShuttingDown = isShuttingDown || (() => false);
194
275
  _version = '0.0.0';
276
+ _eventLines = [];
195
277
  }
196
278
 
197
279
  function drawBanner(version) { _version = version; }
198
280
  function start() {}
199
281
  function stop() { process.stdout.write(c.reset + '\n'); }
200
282
 
201
- module.exports = { init, drawBanner, start, draw, log, workerColor: wc, stop };
283
+ function log(accountIdx, msg) {
284
+ const now = new Date();
285
+ const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
286
+
287
+ if (accountIdx >= 0) {
288
+ if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
289
+ _eventLines[accountIdx].push({ text: msg, ts });
290
+ if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
291
+ }
292
+
293
+ draw();
294
+ }
295
+
296
+ function logGlobal(msg) {
297
+ // Global events go to account 0
298
+ log(-1, msg);
299
+ }
300
+
301
+ module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.38.0",
3
+ "version": "8.40.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"