dankgrinder 8.73.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 -196
  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,130 +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: 18, bal: 10, ls: 3, lv: 3, earned: 10 };
131
- // Account row visual parts: '│ ' + '# ' + status + ' ' + name + ' ' + doing + ' ' + bal + ' ' + ls + ' ' + lv + ' ' + earned + ' │'
132
- const rowPadding = 1 + 2 + 1 + col.st + 1 + col.name + 1 + col.cmd + 1 + col.bal + 1 + col.ls + 1 + col.lv + 1 + col.earned + 2;
133
- const nameExtra = Math.max(0, _inner - rowPadding);
134
- col.name += nameExtra;
135
- 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);
136
117
  }
137
118
 
138
- // ── Build account row string ─────────────────────────────────
139
- function accountRow(w, wi, col) {
140
- const col2 = wc(wi);
141
- const stCol = statusColor(w);
142
- const stTxt = statusText(w);
143
- const earned = fmtCoins(w.stats.coins);
144
- 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;
145
125
  const ls = fmtLifesavers(w);
146
126
  const lv = fmtLevel(w);
147
- const name = padR(trunc(w.username || '?', col.name), col.name);
148
- const doing = padR(w.lastStatus || 'idle', col.cmd);
149
- const earnedStr = stripAnsi(earned);
150
- const balStr = stripAnsi(bal);
151
127
 
152
- // Pad earned and bal to column widths
153
- const earnedPadded = earned.padEnd(col.earned + (earned.length - earnedStr.length));
154
- 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"
155
139
 
156
140
  return (
157
- `${B}│\x1b[0m ` +
158
- `${DIM}${padL(wi + 1, 2)}${c.reset} ` +
159
- `${stCol}${padR(stTxt, col.st)}${c.reset} ` +
160
- `${col2}${name}${c.reset} ` +
161
- `${DIM}${doing}${c.reset} ` +
162
- `${balPadded} ` +
163
- `${padL(ls, col.ls)} ` +
164
- `${padL(lv, col.lv)} ` +
165
- `${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} ` +
166
148
  `${B}│\x1b[0m`
167
149
  );
168
150
  }
169
151
 
170
- // ── 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) ─────────────────────
171
172
  function draw() {
172
173
  layout();
173
- const col = getCol();
174
- const bannerH = BANNER_LINES.length;
174
+ const LOG_W = getLogsW();
175
175
 
176
- // Clear entire screen + home
176
+ // Clear screen + home
177
177
  process.stdout.write('\x1b[2J\x1b[H');
178
178
 
179
179
  let r = 1;
180
+ const top = _inner;
180
181
 
181
182
  // ── Top border ──
182
183
  row(r++); clrLine();
183
- process.stdout.write(`${B}┌${'─'.repeat(_inner)}┐`);
184
+ process.stdout.write(`${B}┌${'─'.repeat(top)}┐`);
184
185
  ln();
185
186
 
186
- // ── Banner ──
187
- for (let i = 0; i < bannerH; i++) {
188
- row(r++); clrLine();
189
- const line = i === 1 ? BANNER_LINES[1].replace('PLACEHOLDER', _version) : BANNER_LINES[i];
190
- if (i < 2) {
191
- const gradLine = gradientLine(line, 77, 212, 238, 255, 92, 147);
192
- const ansiLen = stripAnsi(gradLine).length;
193
- const totalContent = 2 + ansiLen; // left spaces + content
194
- process.stdout.write(`${B}│\x1b[0m ${gradLine}${' '.repeat(Math.max(0, _inner - totalContent - 1))}${B}│`);
195
- } else {
196
- const ansiLen = stripAnsi(line).length;
197
- const totalContent = 2 + ansiLen;
198
- process.stdout.write(`${B}│\x1b[0m ${DIM}${line}${c.reset}${' '.repeat(Math.max(0, _inner - totalContent - 1))}${B}│`);
199
- }
200
- ln();
201
- }
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;
202
193
 
203
- // ── Status bar ──
204
- const running = _workers.filter(w => w.channel && !w.paused && !w.dashboardPaused).length;
205
- const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
206
- const errors = _workers.filter(w => !w.channel).length;
207
- const phaseLabel = _phase === 'grinding' ? '' : ` ${DIM}[${_phase}]${c.reset}`;
208
- const statusParts = [
209
- `${c.green}●${c.reset} ${running} online`,
210
- paused > 0 ? `${c.yellow}~${c.reset} ${paused} paused` : null,
211
- errors > 0 ? `${c.red}E${c.reset} ${errors} error` : null,
212
- `${DIM}Ctrl+C${c.reset}`,
213
- ].filter(Boolean);
214
- 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;
215
200
 
216
201
  row(r++); clrLine();
217
- 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}│`);
218
203
  ln();
219
204
 
220
205
  // ── Divider ──
221
206
  row(r++); clrLine();
222
- process.stdout.write(`${B}├${'─'.repeat(_inner)}┤`);
207
+ process.stdout.write(`${B}├${'─'.repeat(top)}┤`);
223
208
  ln();
224
209
 
225
- // ── Header ──
210
+ // ── Column header ──
226
211
  row(r++); clrLine();
227
- 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));
228
213
  ln();
229
214
 
230
215
  // ── HR ──
231
216
  row(r++); clrLine();
232
- process.stdout.write(`${B}│\x1b[0m ${'─'.repeat(_inner)} ${B}│`);
217
+ process.stdout.write(`${B}│\x1b[0m${'─'.repeat(top)}${B}│`);
233
218
  ln();
234
219
 
235
220
  // ── Account rows ──
236
221
  const sorted = [..._workers].sort((a, b) => {
237
- if (!a.channel !== !b.channel) return !a.channel ? 1 : -1;
222
+ if (!a.channel !== !b.channel) return a.channel ? -1 : 1;
238
223
  const aA = a.channel && !a.paused && !a.dashboardPaused;
239
- const bA = b.channel && !b.paused && !b.dashboardPaused;
240
- if (aA !== bA) return bA ? 1 : -1;
224
+ const bB = b.channel && !b.paused && !b.dashboardPaused;
225
+ if (aA !== bB) return aA ? -1 : 1;
241
226
  return (b.stats.commands || 0) - (a.stats.commands || 0);
242
227
  });
243
228
  const shown = sorted.slice(0, _maxAccounts);
@@ -248,55 +233,105 @@ function draw() {
248
233
  const wi = _workers.indexOf(w);
249
234
  _accountRows[wi] = r;
250
235
  row(r++); clrLine();
251
- process.stdout.write(accountRow(w, wi, col));
236
+ process.stdout.write(accountRow(w, wi));
252
237
  ln();
253
238
  }
254
239
 
255
240
  // ── Totals divider ──
256
- _totalsRow = r;
257
241
  row(r++); clrLine();
258
- process.stdout.write(`${B}│\x1b[0m ${'─'.repeat(_inner)} ${B}│`);
242
+ process.stdout.write(`${B}│\x1b[0m${'─'.repeat(top)}${B}│`);
259
243
  ln();
260
244
 
261
- // ── Totals ──
262
- let totalCoins = 0, totalBal = 0;
263
- for (const w of _workers) {
264
- totalCoins += w.stats.coins || 0;
265
- totalBal += w.stats.balance || 0;
266
- }
267
- const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
268
- const balStr = totalBal > 0 ? '⏣' + totalBal.toLocaleString() : '—';
269
- const balPadded = balStr.padEnd(col.bal);
270
- 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);
271
252
 
272
253
  row(r++); clrLine();
273
- process.stdout.write(
274
- `${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}│`
275
- );
254
+ process.stdout.write(`${B}│\x1b[0m${' '.repeat(2)}${coinsLabel}${' '.repeat(gap)}${coinsVal}${' '.repeat(2)}${B}│`);
276
255
  ln();
277
256
 
278
- // ── Bottom ──
257
+ // ── Bottom border ──
279
258
  _bottomRow = r;
280
259
  row(r++); clrLine();
281
- 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)}┤`);
282
273
  ln();
283
274
  }
284
275
 
285
- // ── Update ONE account row in-place ─────────────────────────
276
+ // ── Update ONE account row in-place ───────────────────────────────
286
277
  function updateAccountRow(accountIdx) {
287
278
  if (!_live) return;
288
279
  const rowNum = _accountRows[accountIdx];
289
- if (!rowNum) return; // not visible
280
+ if (!rowNum) return;
290
281
  const w = _workers[accountIdx];
291
282
  if (!w) return;
292
- const col = getCol();
293
-
294
283
  row(rowNum); clrLine();
295
- 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)}┘`);
296
331
  ln();
297
332
  }
298
333
 
299
- // ── Gradient line ─────────────────────────────────────────────
334
+ // ── Gradient line (unused in new design but kept) ─────────────────
300
335
  function gradientLine(text, r1, g1, b1, r2, g2, b2) {
301
336
  let out = '';
302
337
  for (let i = 0; i < text.length; i++) {
@@ -306,17 +341,21 @@ function gradientLine(text, r1, g1, b1, r2, g2, b2) {
306
341
  return out + '\x1b[0m';
307
342
  }
308
343
 
309
- // ── Event tracking ────────────────────────────────────────────
310
- let _eventLines = [];
311
- const MAX_EVENTS = 15;
312
-
313
- // ── 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
+ }
314
352
 
315
- function init({ workers, isShuttingDown }) {
353
+ // ── Public API ─────────────────────────────────────────────────────
354
+ function init({ workers }) {
316
355
  _startTime = Date.now();
317
356
  _workers = workers;
318
357
  _version = '0.0.0';
319
- _eventLines = [];
358
+ _events = [];
320
359
  _live = false;
321
360
  _phase = 'init';
322
361
  _accountRows = [];
@@ -324,47 +363,53 @@ function init({ workers, isShuttingDown }) {
324
363
 
325
364
  function drawBanner(version) { _version = version || '0.0.0'; }
326
365
  function start() {}
327
-
328
366
  function stop() {
329
367
  _live = false;
330
- _phase = 'init';
331
368
  process.stdout.write('\x1b[2J\x1b[H' + c.reset + '\n');
332
369
  }
333
-
334
370
  function setLive(val) { _live = val; }
335
371
  function setPhase(phase) { _phase = phase; }
336
372
 
337
373
  function log(accountIdx, msg) {
338
- const now = new Date();
339
- const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
340
-
341
- if (accountIdx >= 0) {
342
- if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
343
- _eventLines[accountIdx].push({ text: msg, ts });
344
- if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
345
- if (_live) updateAccountRow(accountIdx);
346
- }
374
+ if (accountIdx >= 0 && _live) updateAccountRow(accountIdx);
375
+ }
347
376
 
377
+ function logGlobal(msg) {
348
378
  if (!_live) return;
349
-
350
- const col2 = accountIdx >= 0 ? wc(accountIdx) : c.cyan;
351
- const name = accountIdx >= 0 ? trunc(_workers[accountIdx]?.username || '?', 14) : 'GLOBAL';
352
- process.stdout.write(`${col2}${name}${c.reset} ${DIM}[${ts}]${c.reset} ${msg}\n`);
379
+ addEvent('info', msg);
353
380
  }
354
381
 
355
- function logGlobal(msg) { log(-1, msg); }
356
-
357
- // ── Refresh ──────────────────────────────────────────────────
382
+ // ── Refresh timer ─────────────────────────────────────────────────
358
383
  let _refreshTimer = null;
359
384
  function startRefresh() {
360
385
  if (_refreshTimer) return;
361
386
  _refreshTimer = setInterval(() => {
362
387
  if (!_live) return;
363
- for (let i = 0; i < _workers.length; i++) updateAccountRow(i);
364
- }, 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);
365
409
  }
410
+
366
411
  function stopRefresh() {
367
412
  if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
368
413
  }
369
414
 
370
- 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.73.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"