dankgrinder 8.37.0 → 8.39.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 -30
  2. package/lib/ui.js +225 -173
  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,8 +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.logEvent(`${c.red}E${c.reset} DEATH — ${w.username} has 0 lifesavers! Crime/search disabled`);
2759
- ui.draw();
2789
+ ui.log(workers.indexOf(w), `${c.red}E${c.reset} DEATH — 0 lifesavers! Crime/search disabled`);
2760
2790
  } else {
2761
2791
  w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
2762
2792
  if (prev !== event.lifesaversLeft) {
@@ -2766,8 +2796,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2766
2796
  if (event.lifesaversLeft <= 2) {
2767
2797
  w._alert = { type: 'lowls' };
2768
2798
  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();
2799
+ ui.log(workers.indexOf(w), `⚠ ${event.lifesaversLeft} lifesavers left!`);
2771
2800
  }
2772
2801
  }
2773
2802
  }
@@ -2776,7 +2805,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2776
2805
  if (event.type === 'levelup') {
2777
2806
  if (event.to > 0) {
2778
2807
  w._level = event.to;
2779
- ui.logEvent(`${c.blue}↑${c.reset} ${w.username} leveled up to ${event.to}`);
2808
+ ui.log(workers.indexOf(w), `↑ level ${event.to}`);
2780
2809
  }
2781
2810
  }
2782
2811
  }
@@ -2790,8 +2819,7 @@ async function start(apiKey, apiUrl, opts = {}) {
2790
2819
  const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
2791
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));
2792
2821
 
2793
- ui.draw(); // draw initial table
2794
- ui.logEvent(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
2822
+ ui.log(-1, `Logging in ${accounts.length} accounts...`);
2795
2823
  const BATCH_SIZE = 10;
2796
2824
  for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
2797
2825
  if (shutdownCalled) break;
@@ -2806,21 +2834,20 @@ async function start(apiKey, apiUrl, opts = {}) {
2806
2834
  await worker.start();
2807
2835
  if (worker._tokenInvalid) {
2808
2836
  worker.lastStatus = 'invalid token';
2809
- ui.logEvent(`${c.red}E${c.reset} [${i + idx + 1}] ${acc.label || acc.id} — invalid token`);
2837
+ ui.log(worker.idx, `✗ invalid token`);
2810
2838
  } else if (worker.channel) {
2811
2839
  worker.lastStatus = 'ready';
2812
- ui.logEvent(`${c.green}·${c.reset} [${i + idx + 1}] ${worker.username} connected`);
2813
- ui.draw();
2840
+ ui.log(worker.idx, `✓ connected`);
2814
2841
  } else {
2815
2842
  worker.lastStatus = 'timeout';
2816
- ui.logEvent(`${c.yellow}·${c.reset} [${i + idx + 1}] ${acc.label || acc.id} — timeout`);
2843
+ ui.log(worker.idx, `✗ timeout`);
2817
2844
  }
2818
2845
  } catch (e) {
2819
- ui.logEvent(`${c.red}E${c.reset} [${i + idx + 1}] ERROR: ${e.message}`);
2846
+ ui.log(i + idx, `ERROR: ${e.message}`);
2820
2847
  }
2821
2848
  }));
2822
2849
  } catch (e) {
2823
- ui.logEvent(`${c.red}!${c.reset} BATCH ERROR: ${e.message}`);
2850
+ ui.log(-1, `BATCH ERROR: ${e.message}`);
2824
2851
  }
2825
2852
  if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
2826
2853
  hintGC();
@@ -2829,32 +2856,32 @@ async function start(apiKey, apiUrl, opts = {}) {
2829
2856
  const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
2830
2857
  const invalidWorkers = workers.filter(w => w._tokenInvalid);
2831
2858
  const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
2832
- ui.logEvent(`${c.green}·${c.reset} Login: ${loginDone}/${accounts.length} connected`);
2859
+ ui.log(-1, `Login: ${loginDone}/${accounts.length} connected`);
2833
2860
  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}`);
2861
+ for (const w of invalidWorkers) ui.log(workers.indexOf(w), `✗ invalid token`);
2835
2862
  }
2836
2863
  if (timedOutWorkers.length > 0) {
2837
- ui.logEvent(`${c.yellow}~${c.reset} ${timedOutWorkers.length} timed out (will retry in background)`);
2864
+ ui.log(-1, `${timedOutWorkers.length} timed out (retrying in background)`);
2838
2865
  }
2839
2866
  ui.draw();
2840
2867
 
2841
2868
  const activeWorkers = workers.filter(w => !w._tokenInvalid);
2842
2869
 
2843
2870
  // ── Phase 2: Inventory check ────────────────────────────────────
2844
- ui.logEvent(`${c.dim}Checking inventory...${c.reset}`);
2871
+ ui.log(-1, `Checking inventory...`);
2845
2872
  let invFailed = 0;
2846
2873
  await Promise.all(activeWorkers.map(async (w) => {
2847
2874
  try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
2848
2875
  catch { invFailed++; }
2849
2876
  }));
2850
2877
  if (invFailed > 0) {
2851
- ui.logEvent(`${c.red}!${c.reset} Inventory failed for ${invFailed} accounts`);
2878
+ ui.log(-1, `Inventory failed for ${invFailed} accounts`);
2852
2879
  } else {
2853
- ui.logEvent(`${c.green}·${c.reset} Inventory OK`);
2880
+ ui.log(-1, `Inventory OK`);
2854
2881
  }
2855
2882
 
2856
2883
  // ── Phase 2.5: Balance check ───────────────────────────────────
2857
- ui.logEvent(`${c.dim}Checking balances...${c.reset}`);
2884
+ ui.log(-1, `Checking balances...`);
2858
2885
  for (const w of activeWorkers) {
2859
2886
  try { await w.checkBalance(true); } catch {}
2860
2887
  }
@@ -2863,10 +2890,10 @@ async function start(apiKey, apiUrl, opts = {}) {
2863
2890
  totalCoins += w.stats?.balance || 0;
2864
2891
  totalCoins += w.stats?.bankBalance || 0;
2865
2892
  }
2866
- ui.logEvent(`${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`);
2867
2894
 
2868
2895
  // ── Phase 2.75: DM history check ────────────────────────────────
2869
- ui.logEvent(`${c.dim}Checking DM history...${c.reset}`);
2896
+ ui.log(-1, `Checking DM history...`);
2870
2897
  let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
2871
2898
  for (const w of activeWorkers) {
2872
2899
  try {
@@ -2884,14 +2911,14 @@ async function start(apiKey, apiUrl, opts = {}) {
2884
2911
  }
2885
2912
  } catch {}
2886
2913
  }
2887
- if (dmNoLs.length > 0) ui.logEvent(`${c.red}E${c.reset} No lifesavers: ${dmNoLs.join(', ')}`);
2914
+ if (dmNoLs.length > 0) ui.log(-1, `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
2888
2915
  const parts = [];
2889
2916
  if (dmDeaths > 0) parts.push(`${dmDeaths} deaths`);
2890
2917
  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}`);
2918
+ ui.log(-1, `DM: ${parts.length > 0 ? parts.join(', ') : 'clean'}`);
2892
2919
 
2893
2920
  // ── Phase 3: Start grind loops ───────────────────────────────────
2894
- ui.logEvent(`${c.green}·${c.reset} Starting ${activeWorkers.length} grind loops...`);
2921
+ ui.log(-1, `Starting ${activeWorkers.length} grind loops...`);
2895
2922
  for (const w of activeWorkers) {
2896
2923
  if (!shutdownCalled) w.grindLoop();
2897
2924
  }
package/lib/ui.js CHANGED
@@ -1,30 +1,50 @@
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 — box design, loading animations, per-account status.
3
+ * Everything inside a single box. Events stream below. No duplicate output.
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
+ // ── Spinner frames ────────────────────────────────────────────
12
+ const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
13
+ function spinnerFrame() { return SPINNER[Math.floor(Date.now() / 150) % SPINNER.length]; }
14
+
15
+ // ── Big ASCII art banner ──────────────────────────────────────
16
+ const BANNER_LINES = [
17
+ ' ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗',
18
+ ' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║',
19
+ ' ██║ ██║██║ ██║██╔██╗ ██║██║ ███╗█████╗ ██║ ██║██╔██╗ ██║',
20
+ ' ██║ ██║██║ ██║██║╚██╗██║██║ ██║██╔══╝ ██║ ██║██║╚██╗██║',
21
+ ' ██████╔╝╚██████╔╝██║ ╚████║╚██████╔╝███████╗╚██████╔╝██║ ╚████║',
22
+ ' ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝',
23
+ ' ██╗██╗ ██╗███╗ ██╗ ██████╗ ██╗ ███████╗██████╗ ',
24
+ ' ██║██║ ██║████╗ ██║██╔═══██╗██║ ██╔════╝██╔══██╗',
25
+ ' ███████╗███████╗ ██║██║ ██║██╔██╗ ██║██║ ██║██║ █████╗ ██████╔╝',
26
+ ' ╚════██║╚════██║ ██║██║ ██║██║╚██╗██║██║ ██║██║ ██╔══╝ ██╔══██╗',
27
+ ' ██║ ██║ ██║╚██████╔╝██║ ╚████║╚██████╔╝███████╗███████╗██║ ██║',
28
+ ' ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝',
29
+ ];
10
30
 
11
- // ── Dark-theme palette (light/bright colors on dark bg) ───────
31
+ function gradientLine(text, r1, g1, b1, r2, g2, b2) {
32
+ let out = '';
33
+ for (let i = 0; i < text.length; i++) {
34
+ const t = text.length <= 1 ? 0 : i / (text.length - 1);
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
+ }
37
+ return out + '\x1b[0m';
38
+ }
39
+
40
+ // ── Palette ───────────────────────────────────────────────────
12
41
  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
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',
25
46
  ];
26
-
27
- function workerColor(idx) { return PALETTE[idx % PALETTE.length]; }
47
+ function wc(idx) { return PALETTE[idx % PALETTE.length]; }
28
48
 
29
49
  // ── ANSI helpers ──────────────────────────────────────────────
30
50
  const c = {
@@ -34,12 +54,18 @@ const c = {
34
54
  yellow: '\x1b[38;2;255;220;80m',
35
55
  cyan: '\x1b[38;2;80;220;255m',
36
56
  blue: '\x1b[38;2;100;160;255m',
37
- gray: '\x1b[38;2;120;130;150m',
38
- white: '\x1b[38;2;220;220;240m',
39
57
  };
40
58
  const DIM = c.dim;
41
59
 
42
- // ── Uptime ───────────────────────────────────────────────────
60
+ function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
61
+ function padR(s, n) { return trunc(s, n).padEnd(n); }
62
+ function padL(s, n) { return String(s).padStart(n); }
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
+ }
68
+
43
69
  function fmtUptime() {
44
70
  const s = Math.floor((Date.now() - _startTime) / 1000);
45
71
  if (s < 60) return `${s}s`;
@@ -51,134 +77,134 @@ function fmtUptime() {
51
77
  return `${m}m`;
52
78
  }
53
79
 
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
- };
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;
71
87
  }
72
88
 
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}`;
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';
80
99
  }
81
- if (w._rateLimitHits > (w._prevRateLimits || 0)) return `${c.cyan}~${c.reset}`;
82
- return `${c.green}·${c.reset}`;
100
+ return '● READY';
83
101
  }
84
102
 
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) + '…';
90
- }
91
-
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));
103
+ // ── Layout ────────────────────────────────────────────────────
104
+ function layout() {
97
105
  const W = Math.min(process.stdout.columns || 120, 120);
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}`
125
- );
126
- console.log(` ${hr}`);
127
-
128
- return { visible, W, cols };
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 };
129
115
  }
130
116
 
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
- }
117
+ // ── Draw the full box ────────────────────────────────────────
118
+ function draw() {
119
+ const { W, bannerH, statusH, headerH, totalsH, footerH, maxAccounts, eventH } = layout();
120
+ const T = '─'.repeat(W - 2); // inner width
149
121
 
150
- // ── Draw all rows ───────────────────────────────────────────
151
- function drawRows({ visible, W, cols }) {
152
- const hr = '─'.repeat(W);
153
- const sorted = sortedWorkers(visible);
122
+ // ── Top of box ──
123
+ process.stdout.write('\x1b[2J\x1b[H');
124
+ process.stdout.write(`\x1b[38;2;77;212;238m┌─${T}─┐\x1b[0m\n`);
154
125
 
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);
126
+ // ── Banner inside box ──
127
+ for (const line of BANNER_LINES) {
128
+ 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`);
129
+ }
130
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
159
131
 
160
- const earned = w.stats.coins > 0
161
- ? `${c.green}+${w.stats.coins.toLocaleString()}${c.reset}`
162
- : DIM + '—' + c.reset;
132
+ // ── Status bar inside box ──
133
+ const running = _workers.filter(w => w.running && w.channel && !w.paused && !w.dashboardPaused).length;
134
+ const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
135
+ const errors = _workers.filter(w => !w.running || !w.channel).length;
136
+ const statusLine = [
137
+ `${c.bold}v${_version}${c.reset}`,
138
+ `${c.green}●${c.reset} online`,
139
+ `${fmtUptime()}`,
140
+ `${c.green}·${c.reset}${running}`,
141
+ `${c.yellow}~${c.reset}${paused}`,
142
+ errors > 0 ? `${c.red}E${c.reset}${errors}` : null,
143
+ `${DIM}Ctrl+C${c.reset}`,
144
+ ].filter(Boolean).join(' ');
145
+ const statusPad = W - 4 - statusLine.length;
146
+ 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`);
147
+ process.stdout.write(`\x1b[38;2;77;212;238m├─${T}─┤\x1b[0m\n`);
148
+
149
+ // ── Table header ──
150
+ const col = {
151
+ st: 8, // Status
152
+ name: 18, // Account name
153
+ last: 20, // Last command
154
+ cmds: 6, // Commands
155
+ ok: 4, // OK%
156
+ earned: 10, // Earned
157
+ };
158
+ const nameExtra = Math.max(0, W - 4 - col.st - col.name - col.last - col.cmds - col.ok - col.earned - 14);
159
+ col.name += nameExtra;
160
+
161
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
162
+ process.stdout.write(`${c.bold}#${c.reset} `);
163
+ process.stdout.write(`${c.bold}${padR('STATUS', col.st)}${c.reset} `);
164
+ process.stdout.write(`${c.bold}${padR('ACCOUNT', col.name)}${c.reset} `);
165
+ process.stdout.write(`${c.bold}${padR('LAST COMMAND', col.last)}${c.reset} `);
166
+ process.stdout.write(`${c.bold}${padL('CMDS', col.cmds)}${c.reset} `);
167
+ process.stdout.write(`${c.bold}${padL('OK%', col.ok)}${c.reset} `);
168
+ process.stdout.write(`${c.bold}${padL('EARNED', col.earned)}${c.reset} `);
169
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
170
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
171
+
172
+ // ── Account rows ──
173
+ const sorted = [..._workers].sort((a, b) => {
174
+ if (!a.channel !== !b.channel) return !a.channel ? 1 : -1;
175
+ const aA = a.running && a.channel && !a.paused && !a.dashboardPaused;
176
+ const bA = b.running && b.channel && !b.paused && !b.dashboardPaused;
177
+ if (aA !== bA) return bA ? 1 : -1;
178
+ return (b.stats.commands || 0) - (a.stats.commands || 0);
179
+ });
180
+ const shown = sorted.slice(0, maxAccounts);
181
+
182
+ for (let si = 0; si < shown.length; si++) {
183
+ const w = shown[si];
184
+ const wi = _workers.indexOf(w);
185
+ const col2 = wc(wi);
186
+ const stCol = statusColor(w);
187
+ const stText = statusText(w);
188
+ const earned = w.stats.coins > 0 ? `${c.green}+${w.stats.coins.toLocaleString()}${c.reset}` : DIM + '—' + c.reset;
163
189
  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`
176
- );
190
+ const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
191
+
192
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
193
+ process.stdout.write(`${DIM}${padL(wi + 1, 2)}${c.reset} `);
194
+ process.stdout.write(`${stCol}${padR(stText, col.st)}${c.reset} `);
195
+ process.stdout.write(`${col2}${padR(w.username || '?', col.name)}${c.reset} `);
196
+ process.stdout.write(`${DIM}${padR(w.lastStatus || 'idle', col.last)}${c.reset} `);
197
+ process.stdout.write(`${padL(cmds, col.cmds)} `);
198
+ process.stdout.write(`${padL(rate, col.ok)}% `);
199
+ process.stdout.write(`${padL(earned, col.earned)} `);
200
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
177
201
  }
178
202
 
179
- console.log(` ${hr}`);
203
+ if (sorted.length > maxAccounts) {
204
+ 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`);
205
+ }
180
206
 
181
- // Totals
207
+ // ── Totals ──
182
208
  let totalCoins = 0, totalCmds = 0, totalOk = 0;
183
209
  for (const w of _workers) {
184
210
  totalCoins += w.stats.coins || 0;
@@ -187,61 +213,87 @@ function drawRows({ visible, W, cols }) {
187
213
  }
188
214
  const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
189
215
  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
-
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}`
203
- );
204
- console.log('');
205
- }
206
216
 
207
- // ── Event log ────────────────────────────────────────────────
208
- let _eventCount = 0;
209
- const MAX_EVENTS = 6;
217
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
218
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
219
+ process.stdout.write(`${c.bold}Σ${c.reset} `);
220
+ process.stdout.write(`${DIM}${padL(_workers.length, 2)} acc${c.reset} `);
221
+ process.stdout.write(`${' '.repeat(col.name)} `);
222
+ process.stdout.write(`${DIM}${padR(w.lastStatus || '', col.last)}${c.reset} `);
223
+ process.stdout.write(`${padL(totalCmds, col.cmds)} `);
224
+ process.stdout.write(`${padL(rate, col.ok)}% `);
225
+ process.stdout.write(`${totalCoins > 0 ? c.green + padL('+' + totalCoins.toLocaleString(), col.earned) + c.reset : DIM + padL('—', col.earned) + c.reset} `);
226
+ process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(W - 4));
227
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
228
+
229
+ // ── Events section ──
230
+ process.stdout.write(`\x1b[38;2;77;212;238m├─${T}─┤\x1b[0m\n`);
231
+ 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`);
232
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
233
+
234
+ // Show recent events per account
235
+ const evLines = [];
236
+ for (let i = 0; i < _workers.length; i++) {
237
+ if (_eventLines[i] && _eventLines[i].length > 0) {
238
+ const latest = _eventLines[i][_eventLines[i].length - 1];
239
+ const col2 = wc(i);
240
+ const name = trunc(_workers[i]?.username || '?', 14);
241
+ evLines.push({ i, text: latest.text, ts: latest.ts, col: col2, name });
242
+ }
243
+ }
210
244
 
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;
245
+ for (let i = 0; i < Math.min(eventH, evLines.length); i++) {
246
+ const ev = evLines[evLines.length - 1 - i];
247
+ const now = new Date();
248
+ const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
249
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
250
+ process.stdout.write(`${ev.col}${padR(ev.name, 14)}${c.reset} `);
251
+ process.stdout.write(`${DIM}[${ts}]${c.reset} ${ev.text}${' '.repeat(Math.max(0, W - 20 - ev.name.length - ev.text.length))}`);
252
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
253
+ }
254
+
255
+ for (let i = 0; i < Math.max(0, eventH - evLines.length); i++) {
256
+ process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${' '.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
257
+ }
258
+
259
+ // ── Bottom of box ──
260
+ process.stdout.write(`\x1b[38;2;77;212;238m└─${T}─┘\x1b[0m\n`);
217
261
  }
218
262
 
263
+ // ── Event tracking ────────────────────────────────────────────
264
+ let _eventLines = []; // [accountIdx] = [{text, ts}]
265
+ const MAX_EVENTS = 3;
266
+
219
267
  // ── Public API ────────────────────────────────────────────────
220
268
 
221
269
  function init({ workers, isShuttingDown }) {
222
270
  _startTime = Date.now();
223
271
  _workers = workers;
224
272
  _isShuttingDown = isShuttingDown || (() => false);
225
- _eventCount = 0;
273
+ _version = '0.0.0';
274
+ _eventLines = [];
226
275
  }
227
276
 
228
- function drawBanner(version) {}
277
+ function drawBanner(version) { _version = version; }
278
+ function start() {}
279
+ function stop() { process.stdout.write(c.reset + '\n'); }
229
280
 
230
- let _interval = null;
281
+ function log(accountIdx, msg) {
282
+ const now = new Date();
283
+ const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
231
284
 
232
- function start() {
233
- // No periodic refresh — dashboard re-draws on each event
234
- }
285
+ if (accountIdx >= 0) {
286
+ if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
287
+ _eventLines[accountIdx].push({ text: msg, ts });
288
+ if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
289
+ }
235
290
 
236
- function draw() {
237
- if (_isShuttingDown()) return;
238
- const info = drawHeader(require('../package.json').version);
239
- drawRows(info);
291
+ draw();
240
292
  }
241
293
 
242
- function stop() {
243
- if (_interval) { clearInterval(_interval); _interval = null; }
244
- process.stdout.write(c.reset + '\n');
294
+ function logGlobal(msg) {
295
+ // Global events go to account 0
296
+ log(-1, msg);
245
297
  }
246
298
 
247
- module.exports = { init, drawBanner, start, draw, logEvent, workerColor, stop };
299
+ 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.37.0",
3
+ "version": "8.39.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"