dankgrinder 8.72.0 → 8.74.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 (2) hide show
  1. package/lib/ui.js +241 -190
  2. package/package.json +1 -1
package/lib/ui.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
- * CLI Live Dashboard — cursor-positioned in-place row updates.
3
- * Box drawn once. Rows updated in-place. Events below box.
2
+ * CLI Live Dashboard — cursor-positioned, fixed-width columns.
3
+ * Design: Option 1 box + animated dot status + current logs + events box below.
4
4
  */
5
5
 
6
6
  let _startTime = Date.now();
@@ -10,97 +10,79 @@ let _live = false;
10
10
  let _phase = 'init';
11
11
 
12
12
  // Terminal dimensions
13
- let _W = 100;
14
- let _inner = 98;
13
+ let _W = 110;
14
+ let _inner = 108;
15
15
  let _maxAccounts = 4;
16
16
 
17
- // Row map: which terminal row each account starts at (1-indexed)
18
- let _accountRows = []; // _accountRows[accountIdx] = row
19
- let _totalsRow = 0;
17
+ // Row map
18
+ let _accountRows = []; // _accountRows[accountIdx] = starting row
20
19
  let _bottomRow = 0;
21
20
 
22
- // ── Spinner frames ────────────────────────────────────────────
21
+ // ── Fixed column widths ─────────────────────────────────────────
22
+ // # | ACCOUNT | BAL | LS | LV | LOGS
23
+ // 3 19 12 3 4 rest
24
+ const NUM_W = 3; // " # "
25
+ const ACC_W = 19; // dot + name (padded to 18, dot takes 1)
26
+ const BAL_W = 12; // "⏣95,230" padded
27
+ const LS_W = 3; // lifesavers
28
+ const LV_W = 4; // level
29
+ // ROW = '│ ' + # + ' ' + dot + name + ' ' + bal + ' ' + ls + ' ' + lv + ' ' + logs + ' │'
30
+ // = 2 + 2 + 1 + 1 + 18 + 1 + 12 + 1 + 3 + 1 + 4 + 1 + N + 2 = 50 + N
31
+ // LOGS_W computed dynamically
32
+
33
+ // ── Spinner frames ──────────────────────────────────────────────
23
34
  const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
24
35
  function spinnerFrame() { return SPINNER[Math.floor(Date.now() / 150) % SPINNER.length]; }
25
36
 
26
- // ── ASCII banner ──────────────────────────────────────────────
27
- const BANNER_LINES = [
28
- ' ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗ DANKGRINDER',
29
- ' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║ vPLACEHOLDER',
30
- ' ██║ ██║██║ ██║██╔██╗ ██║██║ ███╗█████╗ ██║ ██║██╔██╗ ██║',
31
- ' ██║ ██║██║ ██║██║╚██╗██║██║ ██║██╔══╝ ██║ ██║██║╚██╗██║',
32
- ' ██████╔╝╚██████╔╝██║ ╚████║╚██████╔╝███████╗╚██████╔╝██║ ╚████║',
33
- ' ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝',
34
- ];
35
-
36
- // ── Palette ───────────────────────────────────────────────────
37
- const PALETTE = [
38
- '\x1b[38;2;77;212;238m', '\x1b[38;2;255;194;77m', '\x1b[38;2;130;210;100m',
39
- '\x1b[38;2;255;120;200m', '\x1b[38;2;120;180;255m', '\x1b[38;2;255;180;80m',
40
- '\x1b[38;2;180;130;255m', '\x1b[38;2;100;255;180m', '\x1b[38;2;255;150;100m',
41
- '\x1b[38;2;150;255;200m', '\x1b[38;2;255;255;120m', '\x1b[38;2;200;150;255m',
42
- ];
43
- function wc(idx) { return PALETTE[idx % PALETTE.length]; }
44
-
45
- // ── ANSI helpers ──────────────────────────────────────────────
37
+ // ── Status dot + color ───────────────────────────────────────────
38
+ const STATUS_DOT = {
39
+ online: { dot: '●', color: '\x1b[38;2;80;255;120m' }, // green
40
+ busy: { dot: '◐', color: '\x1b[38;2;255;220;80m' }, // yellow
41
+ paused: { dot: '○', color: '\x1b[38;2;180;180;180m' }, // dim gray
42
+ dead: { dot: '', color: '\x1b[38;2;255;80;100m' }, // red
43
+ connect: { dot: '◯', color: '\x1b[38;2;255;180;80m' }, // orange
44
+ };
45
+ function getDot(w) {
46
+ if (!w.channel) return STATUS_DOT.connect;
47
+ if (w.paused || w.dashboardPaused) return STATUS_DOT.paused;
48
+ if (w._alert?.type === 'death') return STATUS_DOT.dead;
49
+ if (w.busy || w._invRunning || w._sellRunning) return STATUS_DOT.busy;
50
+ if (w.globalCooldownUntil && Date.now() < w.globalCooldownUntil) return STATUS_DOT.busy;
51
+ return STATUS_DOT.online;
52
+ }
53
+
54
+ // ── ANSI helpers ─────────────────────────────────────────────────
46
55
  const c = {
47
56
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
48
57
  green: '\x1b[38;2;80;255;120m',
49
58
  red: '\x1b[38;2;255;80;100m',
50
59
  yellow: '\x1b[38;2;255;220;80m',
51
60
  cyan: '\x1b[38;2;80;220;255m',
52
- white: '\x1b[37m',
53
61
  };
54
62
  const DIM = c.dim;
55
- const B = '\x1b[38;2;77;212;238m'; // box border color
63
+ const B = '\x1b[38;2;77;212;238m'; // box border blue
56
64
 
57
65
  function row(n) { process.stdout.write(`\x1b[${n};1H`); }
58
66
  function clrLine() { process.stdout.write('\x1b[2K'); }
67
+ function ln() { process.stdout.write('\n'); }
59
68
  function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
60
69
  function padR(s, n) { return trunc(s, n).padEnd(n); }
61
70
  function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
62
- function stripAnsi(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, ''); }
63
- function ln() { process.stdout.write('\n'); }
71
+ function stripAnsi(s) { return String(s || '').replace(/\x1b\[[0-9;]*m/g, ''); }
64
72
 
65
- function fmtUptime() {
66
- const s = Math.floor((Date.now() - _startTime) / 1000);
67
- if (s < 60) return `${s}s`;
68
- const m = Math.floor(s / 60);
69
- const h = Math.floor(m / 60);
70
- if (h > 0) return `${h}h${m % 60}m`;
71
- return `${m}m`;
72
- }
73
-
74
- // ── Status helpers ─────────────────────────────────────────────
75
- function statusColor(w) {
76
- if (!w.channel) return c.red;
77
- if (w.paused || w.dashboardPaused) return c.yellow;
78
- if (w.busy || w._invRunning || w._sellRunning) return c.yellow;
79
- if (Date.now() < w.globalCooldownUntil) return c.cyan;
80
- return c.green;
81
- }
82
-
83
- function statusText(w) {
84
- if (!w.channel) return spinnerFrame() + ' CONN';
85
- if (w.paused) return 'PAUSED';
86
- if (w.dashboardPaused) return 'HELD';
87
- if (w._alert?.type === 'death') return 'DEAD';
88
- if (w._alert?.type === 'lowls') return 'LOW LS';
89
- if (w.busy || w._invRunning || w._sellRunning) return spinnerFrame() + ' WORK';
90
- if (Date.now() < w.globalCooldownUntil) {
91
- const wait = Math.ceil((w.globalCooldownUntil - Date.now()) / 1000);
92
- return '⏳' + padL(wait, 3) + 's';
93
- }
94
- return '● READY';
73
+ // ── Format helpers ───────────────────────────────────────────────
74
+ function fmtCoins(n) {
75
+ if (!n && n !== 0) return '—';
76
+ if (n >= 1_000_000) return '+' + (n / 1_000_000).toFixed(1) + 'M';
77
+ if (n >= 1_000) return '+' + (n / 1_000).toFixed(1) + 'k';
78
+ if (n > 0) return '+' + n;
79
+ return '—';
95
80
  }
96
81
 
97
- // ── Format helpers ─────────────────────────────────────────────
98
- function fmtCoins(n) {
82
+ function fmtBal(n) {
99
83
  if (!n && n !== 0) return DIM + '—' + c.reset;
100
- if (n >= 1_000_000) return c.green + '+' + (n / 1_000_000).toFixed(1) + 'M' + c.reset;
101
- if (n >= 1_000) return c.green + '+' + (n / 1_000).toFixed(1) + 'k' + c.reset;
102
- if (n > 0) return c.green + '+' + n + c.reset;
103
- return DIM + '—' + c.reset;
84
+ const s = n >= 1_000 ? n.toLocaleString() : String(n);
85
+ return c.green + '' + s + c.reset;
104
86
  }
105
87
 
106
88
  function fmtLifesavers(w) {
@@ -114,124 +96,133 @@ function fmtLifesavers(w) {
114
96
  function fmtLevel(w) {
115
97
  const lv = w._level;
116
98
  if (!lv) return DIM + '—' + c.reset;
117
- return c.cyan + lv + c.reset;
99
+ return c.cyan + padL(lv, 4) + c.reset;
118
100
  }
119
101
 
120
- // ── Layout ────────────────────────────────────────────────────
102
+ // ── Layout ─────────────────────────────────────────────────────
121
103
  function layout() {
122
- _W = Math.min(process.stdout.columns || 100, 120);
104
+ _W = Math.min(process.stdout.columns || 110, 130);
123
105
  _inner = _W - 2;
124
106
  const rows = process.stdout.rows || 40;
107
+ // Main box: banner(6) + status(1) + divider(1) + header(1) + hr(1) + accounts + totals(2) + bottom(1) = 13 + accounts
125
108
  _maxAccounts = Math.min(_workers.length, Math.max(3, rows - 16));
126
109
  }
127
110
 
128
- // ── Column layout ─────────────────────────────────────────────
129
- function getCol() {
130
- const col = { st: 9, name: 18, cmd: 16, bal: 8, ls: 3, lv: 3, earned: 8 };
131
- const nameExtra = Math.max(0, _inner - col.st - col.name - col.cmd - col.bal - col.ls - col.lv - col.earned - 14);
132
- col.name += nameExtra;
133
- return col;
111
+ // ── Get LOGS column width ─────────────────────────────────────────
112
+ function getLogsW() {
113
+ // ROW = + # + ' ' + dot+name + ' ' + bal + ' ' + ls + ' ' + lv + ' ' + logs + ' │ + \n
114
+ // 1 2 1 19 1 12 1 3 1 4 1 N 1 1 = 48+N
115
+ const FIXED = NUM_W + 1 + ACC_W + 1 + BAL_W + 1 + LS_W + 1 + LV_W + 1 + 2;
116
+ return Math.max(20, _inner - FIXED);
134
117
  }
135
118
 
136
- // ── Build account row string ─────────────────────────────────
137
- function accountRow(w, wi, col) {
138
- const col2 = wc(wi);
139
- const stCol = statusColor(w);
140
- const stTxt = statusText(w);
141
- const earned = fmtCoins(w.stats.coins);
142
- const bal = w.stats.balance !== undefined ? fmtCoins(w.stats.balance) : DIM + '?' + c.reset;
119
+ // ── Build one account row ─────────────────────────────────────────
120
+ function accountRow(w, wi) {
121
+ const LOG_W = getLogsW();
122
+ const dot = getDot(w);
123
+ const name = padR(trunc(w.username || '?', ACC_W - 1), ACC_W - 1); // -1 for dot
124
+ const bal = w.stats.balance !== undefined ? w.stats.balance : null;
143
125
  const ls = fmtLifesavers(w);
144
126
  const lv = fmtLevel(w);
145
- const name = padR(trunc(w.username || '?', col.name), col.name);
146
- const doing = padR(w.lastStatus || 'idle', col.cmd);
147
- const earnedStr = stripAnsi(earned);
148
- const balStr = stripAnsi(bal);
149
127
 
150
- // Pad earned and bal to column widths
151
- const earnedPadded = earned.padEnd(col.earned + (earned.length - earnedStr.length));
152
- const balPadded = bal.padEnd(col.bal + (bal.length - balStr.length));
128
+ // Current log: lastStatus or cooldown
129
+ let logText = w.lastStatus || 'idle';
130
+ if (w.globalCooldownUntil && Date.now() < w.globalCooldownUntil) {
131
+ const s = Math.ceil((w.globalCooldownUntil - Date.now()) / 1000);
132
+ logText = s > 60 ? `cooldown ${Math.ceil(s/60)}m` : `cooldown ${s}s`;
133
+ }
134
+ if (w.paused || w.dashboardPaused) logText = 'paused';
135
+ if (w._alert?.type === 'death') logText = 'DEAD — lifesavers?';
136
+ const logPadded = padR(logText, LOG_W);
137
+
138
+ const numStr = padL(String(wi + 1), NUM_W - 1); // " 1" or "10"
153
139
 
154
140
  return (
155
- `${B}│\x1b[0m ` +
156
- `${DIM}${padL(wi + 1, 2)}${c.reset} ` +
157
- `${stCol}${padR(stTxt, col.st)}${c.reset} ` +
158
- `${col2}${name}${c.reset} ` +
159
- `${DIM}${doing}${c.reset} ` +
160
- `${balPadded} ` +
161
- `${padL(ls, col.ls)} ` +
162
- `${padL(lv, col.lv)} ` +
163
- `${earnedPadded} ` +
141
+ `${B}│\x1b[0m` +
142
+ `${DIM}${numStr}${c.reset} ` +
143
+ `${dot.dot}${dot.color}${name}${c.reset} ` +
144
+ `${fmtBal(bal)} ` +
145
+ `${ls} ` +
146
+ `${lv} ` +
147
+ `${DIM}${logPadded}${c.reset} ` +
164
148
  `${B}│\x1b[0m`
165
149
  );
166
150
  }
167
151
 
168
- // ── Draw the FULL box (called once) ─────────────────────────
152
+ // ── Top border helper ─────────────────────────────────────────────
153
+ function hRule(char) {
154
+ process.stdout.write(`${B}│\x1b[0m${char.repeat(_inner)}${B}│`);
155
+ }
156
+
157
+ // ── Header row ───────────────────────────────────────────────────
158
+ function headerRow(LOG_W) {
159
+ return (
160
+ `${B}│\x1b[0m` +
161
+ `${c.bold}${padL('#', NUM_W - 1)}${c.reset} ` +
162
+ `${c.bold}${padR('ACCOUNT', ACC_W)}${c.reset} ` +
163
+ `${c.bold}${padL('BAL', BAL_W)}${c.reset} ` +
164
+ `${c.bold}${padL('LS', LS_W)}${c.reset} ` +
165
+ `${c.bold}${padL('LV', LV_W)}${c.reset} ` +
166
+ `${c.bold}${padR('LOGS', LOG_W)}${c.reset} ` +
167
+ `${B}│\x1b[0m`
168
+ );
169
+ }
170
+
171
+ // ── Draw FULL box (called once on startup) ─────────────────────
169
172
  function draw() {
170
173
  layout();
171
- const col = getCol();
172
- const bannerH = BANNER_LINES.length;
174
+ const LOG_W = getLogsW();
173
175
 
174
- // Clear entire screen + home
176
+ // Clear screen + home
175
177
  process.stdout.write('\x1b[2J\x1b[H');
176
178
 
177
179
  let r = 1;
180
+ const top = _inner;
178
181
 
179
182
  // ── Top border ──
180
183
  row(r++); clrLine();
181
- process.stdout.write(`${B}┌${'─'.repeat(_inner)}┐`);
184
+ process.stdout.write(`${B}┌${'─'.repeat(top)}┐`);
182
185
  ln();
183
186
 
184
- // ── Banner ──
185
- for (let i = 0; i < bannerH; i++) {
186
- row(r++); clrLine();
187
- const line = i === 1 ? BANNER_LINES[1].replace('PLACEHOLDER', _version) : BANNER_LINES[i];
188
- if (i < 2) {
189
- const gradLine = gradientLine(line, 77, 212, 238, 255, 92, 147);
190
- process.stdout.write(`${B}│\x1b[0m ${gradLine}${' '.repeat(_inner - 2 - stripAnsi(gradLine) - 2)}${B}│`);
191
- } else {
192
- process.stdout.write(`${B}│\x1b[0m ${DIM}${line}${c.reset}${' '.repeat(_inner - 2 - stripAnsi(line) - 2)}${B}│`);
193
- }
194
- ln();
195
- }
187
+ // ── Title bar ──
188
+ const up = fmtUptime();
189
+ let totalCoins = 0, totalBal = 0;
190
+ for (const w of _workers) { totalCoins += w.stats.coins || 0; totalBal += w.stats.balance || 0; }
191
+ const totalBalStr = totalBal > 0 ? c.green + '⏣' + totalBal.toLocaleString() + c.reset : DIM + '—' + c.reset;
192
+ const online = _workers.filter(w => w.channel && !w.paused && !w.dashboardPaused).length;
196
193
 
197
- // ── Status bar ──
198
- const running = _workers.filter(w => w.channel && !w.paused && !w.dashboardPaused).length;
199
- const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
200
- const errors = _workers.filter(w => !w.channel).length;
201
- const phaseLabel = _phase === 'grinding' ? '' : ` ${DIM}[${_phase}]${c.reset}`;
202
- const statusParts = [
203
- `${c.green}●${c.reset} ${running} online`,
204
- paused > 0 ? `${c.yellow}~${c.reset} ${paused} paused` : null,
205
- errors > 0 ? `${c.red}E${c.reset} ${errors} error` : null,
206
- `${DIM}Ctrl+C${c.reset}`,
207
- ].filter(Boolean);
208
- const statusStr = statusParts.join(' ') + phaseLabel;
194
+ const titleLeft = `${c.bold}DANKGRINDER${c.reset} ${c.dim}v${_version}${c.reset}`;
195
+ const titleRight = `${c.green}●${c.reset} ${online} online ${totalBalStr} uptime ${up}`;
196
+ const totalW = stripAnsi(titleLeft).length + stripAnsi(titleRight).length;
197
+ const padding = Math.max(0, top - totalW);
198
+ const leftPad = Math.floor(padding / 2);
199
+ const rightPad = padding - leftPad;
209
200
 
210
201
  row(r++); clrLine();
211
- process.stdout.write(`${B}│\x1b[0m ${statusStr}${' '.repeat(_inner - 1 - stripAnsi(statusStr))}${B}│`);
202
+ process.stdout.write(`${B}│\x1b[0m${' '.repeat(leftPad)}${titleLeft}${' '.repeat(rightPad)}${titleRight}${B}│`);
212
203
  ln();
213
204
 
214
205
  // ── Divider ──
215
206
  row(r++); clrLine();
216
- process.stdout.write(`${B}├${'─'.repeat(_inner)}┤`);
207
+ process.stdout.write(`${B}├${'─'.repeat(top)}┤`);
217
208
  ln();
218
209
 
219
- // ── Header ──
210
+ // ── Column header ──
220
211
  row(r++); clrLine();
221
- process.stdout.write(`${B}│\x1b[0m ${c.bold}#${c.reset} ${c.bold}${padR('STATUS', col.st)}${c.reset} ${c.bold}${padR('ACCOUNT', col.name)}${c.reset} ${c.bold}${padR('DOING', col.cmd)}${c.reset} ${c.bold}${padL('BAL', col.bal)}${c.reset} ${c.bold}${padL('LS', col.ls)}${c.reset} ${c.bold}${padL('LV', col.lv)}${c.reset} ${c.bold}${padL('EARNED', col.earned)}${c.reset} ${B}│`);
212
+ process.stdout.write(headerRow(LOG_W));
222
213
  ln();
223
214
 
224
215
  // ── HR ──
225
216
  row(r++); clrLine();
226
- process.stdout.write(`${B}│\x1b[0m ${'─'.repeat(_inner)} ${B}│`);
217
+ process.stdout.write(`${B}│\x1b[0m${'─'.repeat(top)}${B}│`);
227
218
  ln();
228
219
 
229
220
  // ── Account rows ──
230
221
  const sorted = [..._workers].sort((a, b) => {
231
- if (!a.channel !== !b.channel) return !a.channel ? 1 : -1;
222
+ if (!a.channel !== !b.channel) return a.channel ? -1 : 1;
232
223
  const aA = a.channel && !a.paused && !a.dashboardPaused;
233
- const bA = b.channel && !b.paused && !b.dashboardPaused;
234
- if (aA !== bA) return bA ? 1 : -1;
224
+ const bB = b.channel && !b.paused && !b.dashboardPaused;
225
+ if (aA !== bB) return aA ? -1 : 1;
235
226
  return (b.stats.commands || 0) - (a.stats.commands || 0);
236
227
  });
237
228
  const shown = sorted.slice(0, _maxAccounts);
@@ -242,55 +233,105 @@ function draw() {
242
233
  const wi = _workers.indexOf(w);
243
234
  _accountRows[wi] = r;
244
235
  row(r++); clrLine();
245
- process.stdout.write(accountRow(w, wi, col));
236
+ process.stdout.write(accountRow(w, wi));
246
237
  ln();
247
238
  }
248
239
 
249
240
  // ── Totals divider ──
250
- _totalsRow = r;
251
241
  row(r++); clrLine();
252
- process.stdout.write(`${B}│\x1b[0m ${'─'.repeat(_inner)} ${B}│`);
242
+ process.stdout.write(`${B}│\x1b[0m${'─'.repeat(top)}${B}│`);
253
243
  ln();
254
244
 
255
- // ── Totals ──
256
- let totalCoins = 0, totalBal = 0;
257
- for (const w of _workers) {
258
- totalCoins += w.stats.coins || 0;
259
- totalBal += w.stats.balance || 0;
260
- }
261
- const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
262
- const balStr = totalBal > 0 ? '⏣' + totalBal.toLocaleString() : '—';
263
- const balPadded = balStr.padEnd(col.bal);
264
- const earnedStr = stripAnsi(fmtCoins(totalCoins));
245
+ // ── Totals row ──
246
+ const coinsStr = fmtCoins(totalCoins);
247
+ const coinsLabel = `${c.bold}Σ${c.reset} ${_workers.length} accounts`;
248
+ const coinsVal = `${c.green}${coinsStr}${c.reset} coins`;
249
+ const valLen = stripAnsi(coinsVal);
250
+ const labelLen = stripAnsi(coinsLabel);
251
+ const gap = Math.max(0, top - valLen - labelLen - 4);
265
252
 
266
253
  row(r++); clrLine();
267
- process.stdout.write(
268
- `${B}│\x1b[0m ${c.bold}Σ${c.reset} ${DIM}${padL(_workers.length, 2)} acc${c.reset} ${' '.repeat(col.name + col.cmd)} ${balPadded} ${' '.repeat(col.ls + col.lv)} ${fmtCoins(totalCoins)}${' '.repeat(_inner - 2 - stripAnsi(` ${c.bold}Σ${c.reset} ${DIM}${padL(_workers.length, 2)} acc${c.reset} ${' '.repeat(col.name + col.cmd)} ${balPadded} ${' '.repeat(col.ls + col.lv)} ${fmtCoins(totalCoins)}`))}${B}│`
269
- );
254
+ process.stdout.write(`${B}│\x1b[0m${' '.repeat(2)}${coinsLabel}${' '.repeat(gap)}${coinsVal}${' '.repeat(2)}${B}│`);
270
255
  ln();
271
256
 
272
- // ── Bottom ──
257
+ // ── Bottom border ──
273
258
  _bottomRow = r;
274
259
  row(r++); clrLine();
275
- process.stdout.write(`${B}└${'─'.repeat(_inner)}┘`);
260
+ process.stdout.write(`${B}└${'─'.repeat(top)}┘`);
261
+ ln();
262
+
263
+ // ── Events box header ──
264
+ r++; // blank row
265
+ row(r++); clrLine();
266
+ process.stdout.write(`${B}┌${'─'.repeat(top)}┐`);
267
+ ln();
268
+ row(r++); clrLine();
269
+ process.stdout.write(`${B}│\x1b[0m ${c.bold}EVENTS${c.reset}${' '.repeat(top - 10)}${B}│`);
270
+ ln();
271
+ row(r++); clrLine();
272
+ process.stdout.write(`${B}├${'─'.repeat(top)}┤`);
276
273
  ln();
277
274
  }
278
275
 
279
- // ── Update ONE account row in-place ─────────────────────────
276
+ // ── Update ONE account row in-place ───────────────────────────────
280
277
  function updateAccountRow(accountIdx) {
281
278
  if (!_live) return;
282
279
  const rowNum = _accountRows[accountIdx];
283
- if (!rowNum) return; // not visible
280
+ if (!rowNum) return;
284
281
  const w = _workers[accountIdx];
285
282
  if (!w) return;
286
- const col = getCol();
287
-
288
283
  row(rowNum); clrLine();
289
- process.stdout.write(accountRow(w, accountIdx, col));
284
+ process.stdout.write(accountRow(w, accountIdx));
285
+ ln();
286
+ }
287
+
288
+ // ── Event log tracking ─────────────────────────────────────────────
289
+ const MAX_EVENTS = 8;
290
+ let _events = [];
291
+
292
+ function addEvent(type, msg) {
293
+ const now = new Date();
294
+ const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
295
+ const icons = { info: 'ℹ', warn: '⚠', error: '✗', success: '✔', debug: '⌘' };
296
+ const colors = { info: c.cyan, warn: c.yellow, error: c.red, success: c.green, debug: DIM };
297
+ const icon = icons[type] || 'ℹ';
298
+ const col = colors[type] || c.cyan;
299
+
300
+ _events.unshift({ icon, col, msg, ts });
301
+ if (_events.length > MAX_EVENTS) _events.pop();
302
+ if (_live) drawEvents();
303
+ }
304
+
305
+ // ── Draw events box ───────────────────────────────────────────────
306
+ function drawEvents() {
307
+ if (!_live) return;
308
+ const LOG_W = getLogsW();
309
+ const inner = _inner;
310
+
311
+ // Draw from _bottomRow + 2 (after the events box header rows)
312
+ let r = _bottomRow + 2;
313
+
314
+ for (let i = 0; i < MAX_EVENTS; i++) {
315
+ row(r); clrLine();
316
+ if (i < _events.length) {
317
+ const ev = _events[i];
318
+ const msgPadded = padR(`${ev.icon} ${ev.msg}`, inner - 30);
319
+ const tsPadded = padL(`[${ev.ts}]`, 20);
320
+ process.stdout.write(`${B}│\x1b[0m ${ev.col}${msgPadded}${c.reset}${tsPadded} ${B}│`);
321
+ } else {
322
+ process.stdout.write(`${B}│\x1b[0m${' '.repeat(inner)}${B}│`);
323
+ }
324
+ ln();
325
+ r++;
326
+ }
327
+
328
+ // Bottom border of events box
329
+ row(r); clrLine();
330
+ process.stdout.write(`${B}└${'─'.repeat(inner)}┘`);
290
331
  ln();
291
332
  }
292
333
 
293
- // ── Gradient line ─────────────────────────────────────────────
334
+ // ── Gradient line (unused in new design but kept) ─────────────────
294
335
  function gradientLine(text, r1, g1, b1, r2, g2, b2) {
295
336
  let out = '';
296
337
  for (let i = 0; i < text.length; i++) {
@@ -300,17 +341,21 @@ function gradientLine(text, r1, g1, b1, r2, g2, b2) {
300
341
  return out + '\x1b[0m';
301
342
  }
302
343
 
303
- // ── Event tracking ────────────────────────────────────────────
304
- let _eventLines = [];
305
- const MAX_EVENTS = 15;
306
-
307
- // ── Public API ────────────────────────────────────────────────
344
+ function fmtUptime() {
345
+ const s = Math.floor((Date.now() - _startTime) / 1000);
346
+ if (s < 60) return `${s}s`;
347
+ const m = Math.floor(s / 60);
348
+ const h = Math.floor(m / 60);
349
+ if (h > 0) return `${h}h${m % 60}m`;
350
+ return `${m}m`;
351
+ }
308
352
 
309
- function init({ workers, isShuttingDown }) {
353
+ // ── Public API ─────────────────────────────────────────────────────
354
+ function init({ workers }) {
310
355
  _startTime = Date.now();
311
356
  _workers = workers;
312
357
  _version = '0.0.0';
313
- _eventLines = [];
358
+ _events = [];
314
359
  _live = false;
315
360
  _phase = 'init';
316
361
  _accountRows = [];
@@ -318,47 +363,53 @@ function init({ workers, isShuttingDown }) {
318
363
 
319
364
  function drawBanner(version) { _version = version || '0.0.0'; }
320
365
  function start() {}
321
-
322
366
  function stop() {
323
367
  _live = false;
324
- _phase = 'init';
325
368
  process.stdout.write('\x1b[2J\x1b[H' + c.reset + '\n');
326
369
  }
327
-
328
370
  function setLive(val) { _live = val; }
329
371
  function setPhase(phase) { _phase = phase; }
330
372
 
331
373
  function log(accountIdx, msg) {
332
- const now = new Date();
333
- const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
334
-
335
- if (accountIdx >= 0) {
336
- if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
337
- _eventLines[accountIdx].push({ text: msg, ts });
338
- if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
339
- if (_live) updateAccountRow(accountIdx);
340
- }
374
+ if (accountIdx >= 0 && _live) updateAccountRow(accountIdx);
375
+ }
341
376
 
377
+ function logGlobal(msg) {
342
378
  if (!_live) return;
343
-
344
- const col2 = accountIdx >= 0 ? wc(accountIdx) : c.cyan;
345
- const name = accountIdx >= 0 ? trunc(_workers[accountIdx]?.username || '?', 14) : 'GLOBAL';
346
- process.stdout.write(`${col2}${name}${c.reset} ${DIM}[${ts}]${c.reset} ${msg}\n`);
379
+ addEvent('info', msg);
347
380
  }
348
381
 
349
- function logGlobal(msg) { log(-1, msg); }
350
-
351
- // ── Refresh ──────────────────────────────────────────────────
382
+ // ── Refresh timer ─────────────────────────────────────────────────
352
383
  let _refreshTimer = null;
353
384
  function startRefresh() {
354
385
  if (_refreshTimer) return;
355
386
  _refreshTimer = setInterval(() => {
356
387
  if (!_live) return;
357
- for (let i = 0; i < _workers.length; i++) updateAccountRow(i);
358
- }, 1500);
388
+ // Update all visible account rows
389
+ for (const idx of Object.keys(_accountRows)) {
390
+ updateAccountRow(parseInt(idx));
391
+ }
392
+ // Refresh uptime bar
393
+ const online = _workers.filter(w => w.channel && !w.paused && !w.dashboardPaused).length;
394
+ const up = fmtUptime();
395
+ let totalBal = 0;
396
+ for (const w of _workers) totalBal += w.stats.balance || 0;
397
+ const totalBalStr = totalBal > 0 ? c.green + '⏣' + totalBal.toLocaleString() + c.reset : DIM + '—' + c.reset;
398
+ // Update title bar on row 2 (row 1 = top border)
399
+ const titleLeft = `${c.bold}DANKGRINDER${c.reset} ${c.dim}v${_version}${c.reset}`;
400
+ const titleRight = `${c.green}●${c.reset} ${online} online ${totalBalStr} uptime ${up}`;
401
+ const totalW = stripAnsi(titleLeft) + stripAnsi(titleRight);
402
+ const padding = Math.max(0, _inner - totalW);
403
+ const leftPad = Math.floor(padding / 2);
404
+ const rightPad = padding - leftPad;
405
+ row(2); clrLine();
406
+ process.stdout.write(`${B}│\x1b[0m${' '.repeat(leftPad)}${titleLeft}${' '.repeat(rightPad)}${titleRight}${B}│`);
407
+ ln();
408
+ }, 1000);
359
409
  }
410
+
360
411
  function stopRefresh() {
361
412
  if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
362
413
  }
363
414
 
364
- module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase, updateAccountRow, startRefresh, stopRefresh };
415
+ module.exports = { init, drawBanner, start, draw, log, logGlobal, stop, setLive, setPhase, updateAccountRow, startRefresh, stopRefresh, addEvent };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "8.72.0",
3
+ "version": "8.74.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"