dankgrinder 7.78.0 → 7.79.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.
package/lib/grinder.js CHANGED
@@ -339,6 +339,17 @@ function colorBanner() {
339
339
 
340
340
  // ── Simple Logging ─────────────────────────────────────────────
341
341
  function log(type, msg, label) {
342
+ // Route grinding logs through terminal flash events when active
343
+ if (terminal._active) {
344
+ const clean = stripAnsi(String(msg || '')).substring(0, 120);
345
+ const tagClean = stripAnsi(String(label || ''));
346
+ terminal.flashEvent(
347
+ type === 'error' ? 'death' : type === 'warn' ? 'warn' : 'info',
348
+ `${tagClean ? tagClean + ' ' : ''}${clean}`
349
+ );
350
+ return;
351
+ }
352
+
342
353
  const colorIcons = {
343
354
  info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
344
355
  error: `${rgb(239, 68, 68)}✗${c.reset}`, warn: `${rgb(251, 191, 36)}!${c.reset}`,
package/lib/terminal.js CHANGED
@@ -1,29 +1,13 @@
1
1
  /**
2
2
  * terminal.js — Modern animated terminal renderer for DankGrinder
3
3
  *
4
- * Design goals:
5
- * - Single-line-per-account rows (scales to 10k+ accounts)
6
- * - Virtual window: only renders visible slice of accounts
4
+ * Key design:
5
+ * - stdout capture during startup buffer prevents bleed-through
6
+ * - When setActive() called: clear screen + replay buffer, then normal
7
+ * - After activation: all w.log() routed through flashEvent()
8
+ * - Virtual window: single-line per account (scales to 10k+)
7
9
  * - 4 FPS render loop with dirty-row tracking (no flicker)
8
- * - Graceful degradation: falls back to console.log if not a TTY
9
- * - Graceful mode: Ctrl+C always restores cursor + shows summary
10
- *
11
- * Layout (fixed row heights, live-updated):
12
- *
13
- * ┌─ DANKGRINDER v7.77.0 ─────────────────────────────────────────────────────┐
14
- * │ ⏱ 00:32 ⬡ 5 accounts ⏣ 47,230 ⚡ 12 cmd/m 📊 89% success │
15
- * ├─ ACCOUNTS ──────────────────────────────────────────────────────────────────┤
16
- * │ 💎 alice_99 ⏣ 12,450 L:24 ♥3 34cmds 🟢 grinding ⚔ adventure │
17
- * │ 💎 bob_trades ⏣ 8,920 L:18 ♥5 28cmds 🟢 grinding 🐟 fishing │
18
- * │ 💎 crypto_king ⏣ 5,100 L:31 ♥0 19cmds 🟡 depositing 💰 beg │
19
- * │ 💎 diamond_h.. ⏣ 3,780 L:12 ♥8 14cmds 🟢 grinding 🌾 farm │
20
- * │ 💎 moon_wallet ⏣ 1,200 L:9 ♥2 8cmds 🔴 paused ⚠ captcha │
21
- * ├─ EVENTS ────────────────────────────────────────────────────────────────────┤
22
- * │ 00:32 ⚔ alice_99 adventure: +⏣ 850 │
23
- * │ 00:31 💀 crypto_king DEATH — 0 lifesavers! crime/search disabled │
24
- * │ 00:30 ⬆️ bob_trades leveled up to Lv.19 │
25
- * │ 00:28 🌾 diamond_hands farm: +⏣ 2,100 │
26
- * └──────────────────────────────────────────────────────────────────────────────┘
10
+ * - Graceful degradation: falls back to plain console.log if not TTY
27
11
  */
28
12
 
29
13
  const READY = (() => {
@@ -40,51 +24,39 @@ const A = {
40
24
  dim: '\x1b[2m',
41
25
  italic: '\x1b[3m',
42
26
 
43
- black: '\x1b[30m',
44
- red: '\x1b[31m',
45
- green: '\x1b[32m',
46
- yellow: '\x1b[33m',
47
- blue: '\x1b[34m',
48
- magenta: '\x1b[35m',
49
- cyan: '\x1b[36m',
50
- white: '\x1b[37m',
51
-
52
- bgBlack: '\x1b[40m',
53
- bgRed: '\x1b[41m',
54
- bgGreen: '\x1b[42m',
55
- bgYellow: '\x1b[43m',
56
- bgBlue: '\x1b[44m',
57
- bgMagenta: '\x1b[45m',
58
- bgCyan: '\x1b[46m',
59
- bgWhite: '\x1b[47m',
60
-
61
- // 256-color RGB shortcuts
27
+ black: '\x1b[30m', red: '\x1b[31m', green: '\x1b[32m',
28
+ yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m',
29
+ cyan: '\x1b[36m', white: '\x1b[37m',
30
+
31
+ bgBlack: '\x1b[40m', bgRed: '\x1b[41m', bgGreen: '\x1b[42m',
32
+ bgYellow: '\x1b[43m', bgBlue: '\x1b[44m', bgMagenta: '\x1b[45m',
33
+ bgCyan: '\x1b[46m', bgWhite: '\x1b[47m',
34
+
35
+ // True-color RGB
62
36
  rgb: (r, g, b) => `\x1b[38;2;${r};${g};${b}m`,
63
37
 
64
38
  // Cursor
65
- save: '\x1b7',
66
- restore: '\x1b8',
67
- hide: '\x1b[?25l',
68
- show: '\x1b[?25h',
39
+ save: '\x1b7', restore: '\x1b8',
40
+ hide: '\x1b[?25l', show: '\x1b[?25h',
69
41
  up: (n = 1) => `\x1b[${n}A`,
70
42
  down: (n = 1) => `\x1b[${n}B`,
71
- right: (n = 1) => `\x1b[${n}C`,
72
- left: (n = 1) => `\x1b[${n}D`,
73
- col: (n) => `\x1b[${n}G`,
74
43
  clear: '\x1b[2J',
75
44
  clearLine: '\x1b[2K',
76
45
  home: '\x1b[H',
77
46
 
78
- // 256-color palette
79
- purple: '\x1b[38;5;141m', // #8b5cf6
80
- pink: '\x1b[38;5;205m', // #ff5c93
81
- orange: '\x1b[38;5;214m', // #ff9f43
82
- teal: '\x1b[38;5;44m', // #2dd4bf
83
- lime: '\x1b[38;5;82m', // #4cd137
84
- crimson: '\x1b[38;5;196m', // #ff4757
85
- slate: '\x1b[38;5;245m', // #a0a0b0
86
- gold: '\x1b[38;5;220m', // #ffc233
87
- emerald: '\x1b[38;5;48m', // #2ed573
47
+ // Erase in display (clears scrollback too)
48
+ eraseAll: '\x1b[3J\x1b[2J\x1b[H',
49
+
50
+ // 256-color shortcuts
51
+ purple: '\x1b[38;5;141m',
52
+ pink: '\x1b[38;5;205m',
53
+ orange: '\x1b[38;5;214m',
54
+ teal: '\x1b[38;5;44m',
55
+ lime: '\x1b[38;5;82m',
56
+ crimson: '\x1b[38;5;196m',
57
+ slate: '\x1b[38;5;245m',
58
+ gold: '\x1b[38;5;220m',
59
+ emerald: '\x1b[38;5;48m',
88
60
  };
89
61
 
90
62
  // ── Color scheme ────────────────────────────────────────────────────────────
@@ -92,60 +64,63 @@ const A = {
92
64
  const C = {
93
65
  header: A.purple,
94
66
  headerDim: A.rgb(100, 70, 180),
95
- border: A.rgb(60, 50, 90),
96
- borderDim: A.rgb(40, 35, 60),
67
+ border: A.rgb(55, 45, 85),
68
+ borderDim: A.rgb(35, 30, 55),
97
69
 
98
- rowDefault: A.white,
99
- rowAlt: A.rgb(30, 28, 45),
100
- rowHighlight: A.rgb(25, 23, 40),
101
-
102
- name: A.rgb(255, 255, 255),
70
+ name: A.rgb(220, 215, 255),
103
71
  nameDim: A.slate,
104
-
105
72
  coins: A.gold,
106
73
  level: A.cyan,
107
74
  lifesavers: A.pink,
108
75
  lifesaversLow: A.crimson,
109
76
  lifesaversMid: A.orange,
110
77
 
111
- statusActive: A.emerald, // 🟢 grinding
112
- statusPaused: A.crimson, // 🔴 paused
113
- statusWarning: A.orange, // 🟡 warning
114
- statusOffline: A.slate, // ⚫ offline
115
- statusConnecting: A.yellow, // 🟡 connecting
78
+ statusActive: A.emerald,
79
+ statusPaused: A.crimson,
80
+ statusWarning: A.orange,
81
+ statusOffline: A.slate,
82
+ statusConnecting: A.yellow,
116
83
 
117
84
  cmdSuccess: A.emerald,
118
85
  cmdError: A.crimson,
119
86
  cmdEvent: A.purple,
120
87
  cmdWarn: A.orange,
88
+ cmdInfo: A.slate,
121
89
 
122
90
  statLabel: A.slate,
123
91
  statValue: A.white,
124
92
 
125
- headerBg: A.rgb(20, 15, 35),
126
- rowBg1: '',
127
- rowBg2: A.rgb(25, 22, 38),
128
- rowBgPaused: A.rgb(40, 15, 20),
129
- rowBgWarning: A.rgb(40, 35, 15),
130
- rowBgDeath: A.rgb(50, 15, 15),
93
+ // Box styles
94
+ topLeft: '', topRight: '╮',
95
+ botLeft: '╰', botRight: '╯',
96
+ h: '─', v: '│',
131
97
  };
132
98
 
133
99
  // ── Spinner frames ──────────────────────────────────────────────────────────
134
100
 
135
- const SPINNERS = {
136
- dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠿'],
137
- moon: ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'],
138
- arrows: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
139
- pulse: ['▓', '▒', '░', '▒'],
140
- };
101
+ const SPIN = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠿'];
102
+
103
+ // ── stdout capture during startup ─────────────────────────────────────────
104
+
105
+ let _origWrite = null;
106
+ let _captureActive = false;
107
+ let _captureBuf = [];
108
+
109
+ function _captureWrite(chunk) {
110
+ if (_captureActive) {
111
+ _captureBuf.push(String(chunk));
112
+ return;
113
+ }
114
+ return _origWrite.call(process.stdout, chunk);
115
+ }
141
116
 
142
117
  // ── Terminal Renderer ────────────────────────────────────────────────────────
143
118
 
144
119
  class Terminal {
145
120
  constructor() {
146
121
  this.workers = [];
147
- this.events = []; // recent event feed (last N events)
148
- this.MAX_EVENTS = 5;
122
+ this.events = [];
123
+ this.MAX_EVENTS = 4;
149
124
 
150
125
  this.phaseName = '';
151
126
  this.phaseFrame = 0;
@@ -157,44 +132,54 @@ class Terminal {
157
132
  this._renderTimer = null;
158
133
  this._startTime = 0;
159
134
 
160
- // Virtual window state
161
135
  this.windowStart = 0;
162
- this.windowSize = 10;
163
- this.scrollLocked = false; // true = auto-follow most active worker
136
+ this.windowSize = 8;
137
+ this._followIdx = -1;
164
138
 
165
139
  this._lineCount = 0;
166
140
  this._active = false;
167
141
  this._shutdown = false;
168
142
 
169
- // Row offsets (set during render)
170
- this._headerRow = 0;
171
- this._statsRow = 0;
172
- this._accountsRow = 0;
173
- this._eventsRow = 0;
174
- this._footerRow = 0;
175
-
176
- // ── Scroll position state ──
177
- this._followWorkerIdx = -1; // -1 = follow newest active worker
178
-
179
143
  this._w = 80;
180
144
  this._h = 24;
181
145
  this._resizeTimer = null;
182
- this._col = (n) => `\x1b[${n}G`;
146
+
147
+ // ── Startup capture ──
148
+ this._capturing = false;
149
+ this._origLog = null;
150
+ this._phaseProgressDone = 0;
151
+ this._phaseProgressTotal = 0;
183
152
  }
184
153
 
185
154
  // ── Public API ──────────────────────────────────────────────────────────
186
155
 
187
156
  init(opts = {}) {
188
- if (opts.startTime) this._startTime = opts.startTime;
157
+ this._startTime = opts.startTime || Date.now();
189
158
  this.workers = opts.workers || [];
190
159
  this._updateSize();
191
160
 
192
161
  if (READY) {
193
- process.stdout.write(A.hide);
162
+ // Override stdout.write to capture everything during startup
163
+ _origWrite = process.stdout.write.bind(process.stdout);
164
+ process.stdout.write = _captureWrite;
165
+ this._capturing = true;
166
+ _captureActive = true;
167
+ _captureBuf = [];
168
+
169
+ // Also override console.log temporarily
170
+ this._origLog = console.log;
171
+ console.log = (...args) => {
172
+ if (this._active) {
173
+ // After activation, route to flashEvent
174
+ const msg = args.join(' ').replace(/\x1b\[[0-9;]*m/g, '').substring(0, 120);
175
+ if (msg.trim()) this.flashEvent('info', msg);
176
+ } else {
177
+ _captureWrite(args.join(' ') + '\n');
178
+ }
179
+ };
180
+
194
181
  process.stdout.on('resize', this._onResize.bind(this));
195
182
  this._drawStartupScreen();
196
- } else {
197
- console.log(`${C.header}🚀 DankGrinder starting...${A.reset}`);
198
183
  }
199
184
  }
200
185
 
@@ -203,46 +188,56 @@ class Terminal {
203
188
  startPhase(name) {
204
189
  this.phaseName = name;
205
190
  this.phaseFrame = 0;
191
+ this._phaseProgressDone = 0;
192
+ this._phaseProgressTotal = 0;
193
+
206
194
  if (!READY) {
207
- console.log(` ${A.dim}⟳${A.reset} ${name}...`);
195
+ console.log(` ${name}...`);
208
196
  return;
209
197
  }
210
198
  if (this.phaseTimer) clearInterval(this.phaseTimer);
211
199
  this.phaseTimer = setInterval(() => {
212
- this.phaseFrame = (this.phaseFrame + 1) % SPINNERS.dots.length;
200
+ this.phaseFrame = (this.phaseFrame + 1) % SPIN.length;
213
201
  this._redrawPhaseSpinner();
214
202
  }, 120);
215
203
  this._redrawPhaseSpinner();
216
204
  }
217
205
 
218
206
  updateProgress(done, total) {
207
+ this._phaseProgressDone = done;
208
+ this._phaseProgressTotal = total;
219
209
  if (!READY) return;
220
- this._writePhaseProgress(done, total);
210
+ this._redrawProgressBar();
221
211
  }
222
212
 
223
213
  endPhase(name, ok = true) {
224
214
  if (this.phaseTimer) { clearInterval(this.phaseTimer); this.phaseTimer = null; }
215
+ this.phaseName = '';
216
+ this._phaseProgressDone = 0;
217
+ this._phaseProgressTotal = 0;
218
+
225
219
  if (!READY) {
226
- const icon = ok ? `${C.header}✓${A.reset}` : `${A.crimson}✗${A.reset}`;
227
- console.log(` ${icon} ${name}`);
220
+ const icon = ok ? `✓ ${name}` : `✗ ${name}`;
221
+ console.log(` ${icon}`);
228
222
  return;
229
223
  }
230
224
  const icon = ok ? `${C.header}✓${A.reset}` : `${A.crimson}✗${A.reset}`;
231
- const frame = SPINNERS.dots[0];
232
- const cols = this._w;
233
- const label = ` ${frame} ${name}`;
234
- const pad = cols - this._ansiLen(label) - 4;
235
- process.stdout.write(
225
+ const line = ` ${icon} ${name}`;
226
+ const w = this._w;
227
+
228
+ // Clear spinner + progress rows, show result
229
+ this._write(
236
230
  A.save +
237
- this._cursor(2, 1) +
238
- A.clearLine +
239
- `${A.save}${A.home}` +
240
- `${label}${pad > 0 ? ' '.repeat(pad) : ''}` +
241
- `${this._rpad('', cols - this._ansiLen(` ${icon} ${name}`) - 2)}${icon} ${name}` +
231
+ this._cursor(6, 1) + A.clearLine +
232
+ this._cursor(7, 1) + A.clearLine +
233
+ this._cursor(8, 1) + A.clearLine +
234
+ this._cursor(9, 1) + A.clearLine +
235
+ this._cursor(10, 1) + A.clearLine +
236
+ this._cursor(11, 1) + A.clearLine +
237
+ this._cursor(12, 1) + A.clearLine +
238
+ `${this._rpad(line, w)}` +
242
239
  A.restore
243
240
  );
244
- // Clear spinner line
245
- process.stdout.write(this._cursor(3, 1) + A.clearLine);
246
241
  }
247
242
 
248
243
  flashEvent(type, msg) {
@@ -266,50 +261,75 @@ class Terminal {
266
261
  setActive() {
267
262
  if (this._active) return;
268
263
  this._active = true;
269
- if (!READY) return;
270
264
 
271
- // Clear startup, draw live view
272
- process.stdout.write(A.clear + A.home);
273
- this._drawLiveView();
274
- this._startRenderLoop();
265
+ if (READY) {
266
+ // ── Stop capturing, fully clear screen, draw live view ──
267
+ _captureActive = false;
268
+ this._capturing = false;
269
+
270
+ // Restore stdout.write first
271
+ if (_origWrite) {
272
+ process.stdout.write = _origWrite;
273
+ _origWrite = null;
274
+ }
275
+ if (this._origLog) {
276
+ console.log = this._origLog;
277
+ this._origLog = null;
278
+ }
279
+
280
+ // Discard all buffered startup output — we only want the live view
281
+ _captureBuf = [];
282
+
283
+ // Full screen clear + scrollback clear
284
+ this._write(A.eraseAll);
285
+
286
+ // Draw live view
287
+ this._drawLiveView();
288
+
289
+ // Reset dirty flags so we don't redraw header every frame
290
+ this.dirtyWorkers.clear();
291
+ this.dirtyEvents = false;
292
+ this.dirtyStats = false;
293
+
294
+ this._startRenderLoop();
295
+ } else {
296
+ // Non-TTY: restore console.log
297
+ if (this._origLog) {
298
+ console.log = this._origLog;
299
+ this._origLog = null;
300
+ }
301
+ }
275
302
  }
276
303
 
277
304
  scrollBy(delta) {
278
305
  if (!READY || this._shutdown) return;
279
306
  const max = Math.max(0, this.workers.length - this.windowSize);
280
307
  this.windowStart = Math.max(0, Math.min(max, this.windowStart + delta));
281
- this._followWorkerIdx = -1; // manual scroll cancels auto-follow
308
+ this._followIdx = -1;
282
309
  this.dirtyWorkers = new Set();
283
310
  for (let i = this.windowStart; i < this.windowStart + this.windowSize; i++) {
284
311
  if (i < this.workers.length) this.dirtyWorkers.add(i);
285
312
  }
286
313
  }
287
314
 
288
- followWorker(idx) {
289
- this._followWorkerIdx = idx;
290
- this.scrollLocked = true;
291
- // Ensure the worker is visible
292
- if (idx < this.windowStart) {
293
- this.windowStart = idx;
294
- } else if (idx >= this.windowStart + this.windowSize) {
295
- this.windowStart = idx - this.windowSize + 1;
296
- }
297
- }
298
-
299
315
  shutdown(summary = {}) {
300
316
  this._shutdown = true;
301
317
  if (this._renderTimer) { clearInterval(this._renderTimer); this._renderTimer = null; }
302
318
  if (this._phaseTimer) { clearInterval(this._phaseTimer); this._phaseTimer = null; }
303
319
 
304
- process.stdout.write(A.show);
305
-
306
- if (!READY) {
307
- this._printSummaryPlain(summary);
308
- return;
320
+ // Restore stdout state
321
+ if (READY && _origWrite) {
322
+ process.stdout.write = _origWrite;
323
+ _origWrite = null;
324
+ }
325
+ if (this._origLog) {
326
+ console.log = this._origLog;
327
+ this._origLog = null;
309
328
  }
310
329
 
311
- // Move to clean area below any existing output
312
- const cols = this._w;
330
+ this._write(A.show);
331
+
332
+ const w = this._w;
313
333
  const { totalCoins = 0, totalCmds = 0, totalSuccess = 0,
314
334
  workers = [], uptime = 0, memMB = 0 } = summary;
315
335
 
@@ -319,45 +339,68 @@ class Terminal {
319
339
  const dim = A.dim;
320
340
  const r = A.reset;
321
341
 
322
- const sep = (c) => `${b}${c.repeat(cols - 2)}${r}`;
323
- const bar = `${b}${'─'.repeat(cols - 2)}${r}`;
324
- const icon = (e) => `${h}${e}${r}`;
325
-
326
342
  let out = '';
327
- out += `${A.clear}${A.home}`;
328
- out += `${A.save}`;
329
-
330
- // Box title
331
- out += `${this._at(1, 1)}${bar}`;
332
- out += `${this._at(2, 1)}${b} ${h}${A.bold}⬡ DANKGRINDER — Session Summary${r} ${b}${'─'.repeat(Math.max(0, cols - 37))}${r}`;
333
- out += `${this._at(3, 1)}${bar}`;
334
-
335
- // Per-account summary
336
- let row = 4;
337
- for (const wk of workers) {
338
- const coins = `+⏣ ${(wk.stats?.coins || 0).toLocaleString()}`;
339
- const cmds = `${wk.stats?.commands || 0}cmds`;
343
+ out += A.eraseAll + A.home + A.save;
344
+
345
+ // Box top
346
+ out += `${this._at(1, 1)}${b}${C.topLeft}${'─'.repeat(w - 2)}${C.topRight}${r}`;
347
+ out += `${this._at(2, 1)}${b}${C.v} ${h}${A.bold} ⬡ DANKGRINDER — Session Summary ${r}${' '.repeat(Math.max(0, w - 40))}${C.v}${r}`;
348
+ out += `${this._at(3, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`;
349
+
350
+ // Column headers
351
+ const hdr = [
352
+ `${h}#${r}`, `${h}ACCOUNT${r}`, `${h}COINS${r}`,
353
+ `${h}LV${r}`, `${h}♥${r}`, `${h}CMDS${r}`,
354
+ `${h}OK%${r}`, `${h}STATUS${r}`,
355
+ ].join(` `);
356
+ out += `${this._at(4, 1)}${b} ${this._rpad(hdr, w - 4)} ${C.v}${r}`;
357
+ out += `${this._at(5, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`;
358
+
359
+ // Per-account rows
360
+ let row = 6;
361
+ for (let i = 0; i < workers.length && row < this._h - 3; i++) {
362
+ const wk = workers[i];
340
363
  const rate = wk.stats?.commands > 0
341
364
  ? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
342
365
  : '0%';
343
366
  const ls = wk._lifesavers ?? '?';
344
- const lv = wk._level ?? '?';
345
- const line = ` ${icon('💎')} ${g}${A.bold}${(wk.username || '?').padEnd(18)}${r} ${C.coins}${coins}${r} ${dim}${cmds}${r} ${g}${rate} OK${r} ${C.level}Lv.${lv}${r} ${C.lifesavers}♥${ls}${r}`;
346
- out += `${this._at(row++, 1)}${b}${this._rpad(line, cols - 2)}${r}`;
367
+ const lsColor = ls === 0 ? C.lifesaversLow : ls <= 2 ? C.lifesaversMid : C.lifesavers;
368
+ const statusIcon = (!wk.running || wk._tokenInvalid) ? '⚫ offline'
369
+ : wk.paused || wk.dashboardPaused ? '🔴 paused'
370
+ : '🟢 active';
371
+
372
+ const line = [
373
+ `${g}${String(i + 1).padEnd(2)}${r}`,
374
+ `${C.name}${A.bold}${(wk.username || '?').substring(0, 18).padEnd(19)}${r}`,
375
+ `${C.coins}⏣${(wk.stats?.coins || 0).toLocaleString().padStart(8)}${r}`,
376
+ `${C.level}Lv.${wk._level ?? '?'}${r}`,
377
+ `${lsColor}♥${String(ls).padStart(2)}${r}`,
378
+ `${g}${String(wk.stats?.commands || 0).padStart(4)}cmds${r}`,
379
+ `${g}${rate.padStart(4)}${r}`,
380
+ `${g}${statusIcon}${r}`,
381
+ ].join(' ');
382
+ out += `${this._at(row++, 1)}${b} ${this._rpad(line, w - 4)} ${C.v}${r}`;
347
383
  }
348
384
 
349
- row++; // blank row
350
- out += `${this._at(row++, 1)}${bar}`;
351
-
352
- // Totals
353
- const totalLine = ` ${icon('💰')} ${h}${A.bold}Total:${r} ${C.coins}${A.bold}⏣ ${totalCoins.toLocaleString()}${r} ${dim}${totalCmds} cmds${r} ${g}${totalSuccess}% OK${r} ${dim}${this._fmtUptime(uptime)}${r} ${dim}${memMB}MB${r}`;
354
- out += `${this._at(row++, 1)}${b}${this._rpad(totalLine, cols - 2)}${r}`;
355
- out += `${this._at(row++, 1)}${sep('─')}`;
356
- out += `${A.restore}`;
357
- out += `${A.show}`;
385
+ row++; // blank
386
+ out += `${this._at(row++, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`;
387
+
388
+ // Totals row
389
+ const totalRate = totalCmds > 0 ? `${((totalSuccess / totalCmds) * 100).toFixed(0)}%` : '0%';
390
+ const uptimeStr = this._fmtUptime(uptime);
391
+ const totalLine = [
392
+ `${h}💰 TOTAL:${r}`,
393
+ `${C.coins}${A.bold}⏣ ${totalCoins.toLocaleString()}${r}`,
394
+ `${dim}${totalCmds} cmds${r}`,
395
+ `${dim}${totalRate} OK${r}`,
396
+ `${dim}${uptimeStr}${r}`,
397
+ `${dim}${memMB}MB RAM${r}`,
398
+ ].join(' ');
399
+ out += `${this._at(row++, 1)}${b} ${this._rpad(totalLine, w - 4)} ${C.v}${r}`;
400
+ out += `${this._at(row, 1)}${b}${C.botLeft}${'─'.repeat(w - 2)}${C.botRight}${r}`;
401
+ out += A.restore + A.show;
358
402
 
359
- process.stdout.write(out);
360
- setTimeout(() => {}, 100);
403
+ this._write(out);
361
404
  }
362
405
 
363
406
  // ── Internal ────────────────────────────────────────────────────────────
@@ -366,10 +409,9 @@ class Terminal {
366
409
  try {
367
410
  this._w = process.stdout.columns || 80;
368
411
  this._h = process.stdout.rows || 24;
369
- // Window size: leave room for header (3 rows) + stats (2) + events footer (3) + border (2)
370
- this.windowSize = Math.max(3, this._h - 12);
412
+ this.windowSize = Math.max(3, this._h - 11);
371
413
  } catch (_) {
372
- this._w = 80; this._h = 24; this.windowSize = 10;
414
+ this._w = 80; this._h = 24; this.windowSize = 8;
373
415
  }
374
416
  }
375
417
 
@@ -378,52 +420,32 @@ class Terminal {
378
420
  this._resizeTimer = setTimeout(() => {
379
421
  this._updateSize();
380
422
  if (this._active) {
381
- this._clearScreen();
423
+ this._write(A.eraseAll);
382
424
  this._drawLiveView();
383
425
  }
384
426
  }, 100);
385
427
  }
386
428
 
387
429
  _ansiLen(s) {
388
- // Fast ANSI strip just count escape sequences by looking for \x1b[
389
- let len = 0;
390
- let i = 0;
430
+ let len = 0, i = 0;
391
431
  const str = String(s);
392
432
  while (i < str.length) {
393
433
  if (str.charCodeAt(i) === 0x1b && str[i + 1] === '[') {
394
434
  let j = i + 2;
395
435
  while (j < str.length && str[j] !== 'm') j++;
396
436
  i = j + 1;
397
- } else {
398
- len++;
399
- i++;
400
- }
437
+ } else { len++; i++; }
401
438
  }
402
439
  return len;
403
440
  }
404
441
 
405
442
  _rpad(s, width) {
406
- const len = this._ansiLen(s);
407
- const pad = width > len ? width - len : 0;
408
- return s + (pad > 0 ? ' '.repeat(pad) : '');
443
+ return s + ' '.repeat(Math.max(0, width - this._ansiLen(s)));
409
444
  }
410
445
 
411
- _cursor(row, col) {
412
- // Position cursor at absolute row/col (1-indexed)
413
- return `\x1b[${row};${col}H`;
414
- }
415
-
416
- _at(row, col) {
417
- return `\x1b[${row};${col}H`;
418
- }
419
-
420
- _write(str) {
421
- process.stdout.write(str);
422
- }
423
-
424
- _ansi(s, code) { return `${code}${s}${A.reset}`; }
425
- _bold(s) { return `${A.bold}${s}${A.reset}`; }
426
- _dim(s) { return `${A.dim}${s}${A.reset}`; }
446
+ _cursor(row) { return `\x1b[${row};1H`; }
447
+ _at(row, col) { return `\x1b[${row};${col}H`; }
448
+ _write(str) { if (str) process.stdout.write(str); }
427
449
 
428
450
  _fmtUptime(ms) {
429
451
  if (!ms) return '0s';
@@ -433,8 +455,7 @@ class Terminal {
433
455
  if (m < 60) return `${m}m ${s % 60}s`;
434
456
  const h = Math.floor(m / 60);
435
457
  if (h < 24) return `${h}h ${m % 60}m`;
436
- const d = Math.floor(h / 24);
437
- return `${d}d ${h % 24}h`;
458
+ return `${Math.floor(h / 24)}d ${h % 24}h`;
438
459
  }
439
460
 
440
461
  _fmtCoins(n) {
@@ -445,53 +466,35 @@ class Terminal {
445
466
 
446
467
  _buildAccountRow(wk, idx) {
447
468
  const w = this._w;
448
- // Row layout (all in one line):
449
- // #N 💎 username ⏣coins Lv.N ♥N Ncmds ● status ⚔ current_cmd
450
- // We need to fit within `w` columns, truncate username if needed
451
-
452
- const num = `${idx + 1}.`.padEnd(3);
453
- const username = (wk.username || '?').substring(0, 16).padEnd(17);
454
- const coins = `⏣${this._fmtCoins(wk.stats?.coins || 0)}`.padEnd(8);
455
- const level = `Lv.${wk._level ?? '?'}`.padEnd(5);
456
469
  const ls = wk._lifesavers ?? '?';
457
- const lifesaversColor = ls === 0 ? C.lifesaversLow
458
- : ls <= 2 ? C.lifesaversMid
459
- : C.lifesavers;
460
- const lifesavers = `${lifesaversColor}♥${ls}`.padEnd(4);
461
- const cmds = `${wk.stats?.commands || 0}cmds`.padEnd(7);
470
+ const lsColor = ls === 0 ? C.lifesaversLow : ls <= 2 ? C.lifesaversMid : C.lifesavers;
471
+ const rate = wk.stats?.commands > 0
472
+ ? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
473
+ : '0%';
462
474
 
463
- // Status dot + text
464
475
  let statusDot, statusText, rowBg;
465
476
  if (!wk.running || wk._tokenInvalid) {
466
- statusDot = '⚫'; statusText = 'offline'; rowBg = C.rowBgPaused;
467
- } else if (wk.paused) {
468
- statusDot = '🔴'; statusText = 'paused'; rowBg = C.rowBgPaused;
469
- } else if (wk.dashboardPaused) {
470
- statusDot = '🟠'; statusText = 'dashboard'; rowBg = C.rowBgWarning;
477
+ statusDot = '⚫'; statusText = 'offline';
478
+ } else if (wk.paused || wk.dashboardPaused) {
479
+ statusDot = '🔴'; statusText = 'paused';
480
+ } else if (wk.lastStatus?.includes('claim') || wk.lastStatus?.includes('daily')) {
481
+ statusDot = '🟡'; statusText = 'claiming';
471
482
  } else {
472
- statusDot = '🟢'; statusText = 'grinding'; rowBg = '';
483
+ statusDot = '🟢'; statusText = 'grinding';
473
484
  }
474
485
 
475
- // Current command
476
- const cmd = (wk.lastStatus || '').substring(0, 20).padEnd(21);
477
- const successRate = wk.stats?.commands > 0
478
- ? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
479
- : '0%';
480
-
481
- const parts = [
482
- `${C.header}${num}${A.reset}`,
483
- `${C.name}${username}${A.reset}`,
484
- `${C.coins}${coins}${A.reset}`,
485
- `${C.level}${level}${A.reset}`,
486
- `${lifesaversColor}${lifesavers}${A.reset}`,
487
- `${C.statValue}${cmds}${A.reset}`,
488
- `${C.statValue}${successRate}`.padEnd(5) + A.reset,
486
+ const line = [
487
+ `${C.header}${idx + 1}.${A.reset}`,
488
+ `${C.name}${A.bold}${(wk.username || '?').substring(0, 18).padEnd(19)}${A.reset}`,
489
+ `${C.coins}⏣${this._fmtCoins(wk.stats?.coins || 0).padStart(7)}${A.reset}`,
490
+ `${C.level}Lv.${String(wk._level ?? '?').padStart(3)}${A.reset}`,
491
+ `${lsColor}♥${String(ls).padStart(2)}${A.reset}`,
492
+ `${C.statValue}${String(wk.stats?.commands || 0).padStart(4)}cmds${A.reset}`,
493
+ `${C.statValue}${rate.padStart(5)}${A.reset}`,
489
494
  `${statusDot} ${statusText}`.padEnd(14),
490
- `${this._dim(cmd)}`,
491
- ];
495
+ `${A.dim}${(wk.lastStatus || '').substring(0, 22).padEnd(22)}${A.reset}`,
496
+ ].join(' ');
492
497
 
493
- // Join and pad to screen width
494
- const line = parts.join(' ');
495
498
  return this._rpad(line, w);
496
499
  }
497
500
 
@@ -499,94 +502,68 @@ class Terminal {
499
502
  const w = this._w;
500
503
  const b = C.border;
501
504
  const h = C.header;
502
- const g = C.statValue;
503
505
  const dim = A.dim;
504
506
  const r = A.reset;
505
507
 
506
- const sep = (c) => `${b}${c.repeat(w - 2)}${r}`;
507
-
508
- // Figure out how many rows we need for the spinner
509
- const spinnerRow = 3;
510
- const progressRow = 5;
511
- const readyRow = 7;
512
-
513
508
  let out = '';
514
- out += `${A.clear}${A.home}`;
509
+ out += A.eraseAll + A.home;
510
+ out += `${this._at(1, 1)}${b}${C.topLeft}${'─'.repeat(w - 2)}${C.topRight}${r}`;
511
+ out += `${this._at(2, 1)}${b} ${h}${A.bold} ⬡ DANKGRINDER v${this._version || '?'} ${r}${'─'.repeat(Math.max(0, w - 28 - (this._version || '').length))}${r}`;
512
+ out += `${this._at(3, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`;
513
+ // Status bar placeholder
514
+ out += `${this._at(4, 1)}${b}${' '.repeat(w - 2)}${r}`;
515
+ out += `${this._at(5, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`;
516
+ // Spinner area
517
+ out += `${this._at(7, 1)}${b}${' '.repeat(w - 2)}${r}`;
518
+ out += `${this._at(8, 1)}${b}${' '.repeat(w - 2)}${r}`;
519
+ out += `${this._at(9, 1)}${b}${' '.repeat(w - 2)}${r}`;
520
+ out += `${this._at(10, 1)}${b}${' '.repeat(w - 2)}${r}`;
521
+ out += `${this._at(11, 1)}${b}${' '.repeat(w - 2)}${r}`;
522
+ out += `${this._at(12, 1)}${b}${' '.repeat(w - 2)}${r}`;
523
+ // Footer
524
+ out += `${this._at(14, 1)}${b}${C.botLeft}${'─'.repeat(w - 2)}${C.botRight}${r}`;
525
+ out += `${this._at(15, 1)}${b} ${dim}Starting up...${r}${' '.repeat(Math.max(0, w - 18))}${r}`;
515
526
 
516
- // Title box
517
- const titleText = ` ⬡ DANKGRINDER v${this._version || '?'} `;
518
- const titlePad = w - 2 - this._ansiLen(titleText);
519
- out += `${this._at(1, 1)}${sep('─')}`;
520
- out += `${this._at(2, 1)}${b} ${h}${A.bold}${titleText}${r}${b}${'─'.repeat(Math.max(0, titlePad))}${r}`;
521
- out += `${this._at(3, 1)}${sep('─')}`;
522
-
523
- // Spinner + phase text
524
- out += `${this._at(5, 1)}${b}${' '.repeat(w - 2)}${r}`;
525
- out += `${this._at(6, 1)}${b}${' '.repeat(w - 2)}${r}`;
526
-
527
- // Footer hint
528
- out += `${this._at(8, 1)}${sep('─')}`;
529
- const hint = `${dim}Starting up...${r}`;
530
- out += `${this._at(9, 1)}${b} ${hint}${' '.repeat(Math.max(0, w - 2 - this._ansiLen(hint)))}${r}`;
531
- out += `${this._at(10, 1)}${sep('─')}`;
532
-
533
- this._lineCount = 10;
534
527
  this._write(out);
535
528
  }
536
529
 
537
530
  _redrawPhaseSpinner() {
538
- if (!READY) return;
539
- const frame = SPINNERS.dots[this.phaseFrame];
540
- const label = this.phaseName;
531
+ if (!READY || !this._phaseName) return;
532
+ const frame = SPIN[this.phaseFrame];
533
+ const line = ` ${frame} ${this.phaseName}...`;
541
534
  const w = this._w;
542
-
543
- const line = ` ${frame} ${label}...`;
544
- const pad = w - this._ansiLen(line) - 2;
545
- const padded = line + (pad > 0 ? ' '.repeat(pad) : '');
546
-
547
535
  const b = C.border;
548
536
  const r = A.reset;
549
537
  this._write(
550
- `${A.save}` +
551
- `${this._cursor(6, 1)}` +
552
- `${b}${padded}${r}` +
553
- `${A.restore}`
538
+ A.save +
539
+ this._cursor(8, 1) + A.clearLine +
540
+ `${b} ${line}${' '.repeat(Math.max(0, w - this._ansiLen(line) - 3))}${r}` +
541
+ A.restore
554
542
  );
555
543
  }
556
544
 
557
- _writePhaseProgress(done, total) {
558
- if (!READY) {
559
- console.log(` ${A.dim} → ${done}/${total}${A.reset}`);
560
- return;
561
- }
545
+ _redrawProgressBar() {
546
+ if (!READY || !this._phaseName) return;
547
+ const { done, total } = { done: this._phaseProgressDone, total: this._phaseProgressTotal };
562
548
  const w = this._w;
563
- const b = C.border;
564
549
  const h = C.header;
565
550
  const dim = A.dim;
566
551
  const r = A.reset;
567
- const barLen = Math.max(10, w - 30);
568
- const filled = Math.round((done / total) * barLen);
569
- const empty = barLen - filled;
570
552
 
571
- const bar = `${h}${'█'.repeat(filled)}${dim}${'░'.repeat(empty)}${r}`;
553
+ const barW = Math.max(10, w - 35);
554
+ const filled = total > 0 ? Math.round((done / total) * barW) : 0;
555
+ const bar = `${h}${'█'.repeat(filled)}${dim}${'░'.repeat(barW - filled)}${r}`;
572
556
  const label = ` ${done}/${total} `;
573
- const line = `${bar} ${label}`;
574
- const pad = w - this._ansiLen(line) - 2;
575
-
557
+ const line = bar + label;
576
558
  this._write(
577
- `${A.save}` +
578
- `${this._cursor(6, 1)}` +
579
- `${b}${line}${' '.repeat(Math.max(0, pad))}${r}` +
580
- `${A.restore}`
559
+ A.save +
560
+ this._cursor(9, 1) + A.clearLine +
561
+ `${C.border} ${line}${' '.repeat(Math.max(0, w - this._ansiLen(line) - 3))}${r}` +
562
+ A.restore
581
563
  );
582
564
  }
583
565
 
584
- _clearScreen() {
585
- this._write(`${A.clear}${A.home}`);
586
- }
587
-
588
566
  _drawLiveView() {
589
- if (!READY) return;
590
567
  this._drawHeader();
591
568
  this._drawAccounts();
592
569
  this._drawEvents();
@@ -601,94 +578,79 @@ class Terminal {
601
578
  const dim = A.dim;
602
579
  const r = A.reset;
603
580
 
604
- const sep = `${b}${'─'.repeat(w - 2)}${r}`;
605
- const title = ` ⬡ DANKGRINDER v${this._version || '?'} `;
606
- const titlePad = w - 2 - this._ansiLen(title);
607
-
608
- this._headerRow = 1;
609
- this._write(`${this._at(1, 1)}${sep}`);
610
- this._write(`${this._at(2, 1)}${b} ${h}${A.bold}${title}${r}${b}${'─'.repeat(Math.max(0, titlePad))}${r}`);
611
- this._write(`${this._at(3, 1)}${sep}`);
612
- this._statsRow = 4;
581
+ // Title bar
582
+ const titleText = ` ⬡ DANKGRINDER v${this._version || '?'} `;
583
+ const titlePad = Math.max(0, w - 2 - this._ansiLen(titleText));
584
+ this._write(`${this._at(1, 1)}${b}${C.topLeft}${'─'.repeat(w - 2)}${C.topRight}${r}`);
585
+ this._write(`${this._at(2, 1)}${b} ${h}${A.bold}${titleText}${r}${' '.repeat(titlePad)} ${C.v}${r}`);
586
+ this._write(`${this._at(3, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`);
613
587
 
614
588
  // Stats bar
615
589
  const stats = this._buildStatsLine();
616
- this._write(`${this._at(4, 1)}${b}${this._rpad(' ' + stats, w - 2)}${r}`);
617
- this._accountsRow = 5;
618
- this._write(`${this._at(5, 1)}${sep}`);
590
+ const statsPad = Math.max(0, w - 2 - this._ansiLen(stats) - 2);
591
+ this._write(`${this._at(4, 1)}${b} ${stats}${' '.repeat(statsPad)} ${C.v}${r}`);
592
+ this._write(`${this._at(5, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`);
593
+
594
+ this._accountsRow = 6;
619
595
  }
620
596
 
621
597
  _buildStatsLine() {
622
- const g = C.statValue;
623
- const dim = A.dim;
624
- const h = C.header;
625
- const coins = C.coins;
626
-
627
598
  let totalCoins = 0, totalCmds = 0, totalSuccess = 0, totalLs = 0;
628
- let pausedCount = 0, activeCount = 0;
599
+ let paused = 0, active = 0;
629
600
 
630
601
  for (const wk of this.workers) {
631
602
  totalCoins += wk.stats?.coins || 0;
632
603
  totalCmds += wk.stats?.commands || 0;
633
604
  totalSuccess += wk.stats?.successes || 0;
634
605
  if (wk._lifesavers != null) totalLs += wk._lifesavers;
635
- if (!wk.running || wk._tokenInvalid) {}
636
- else if (wk.paused || wk.dashboardPaused) pausedCount++;
637
- else activeCount++;
606
+ if (wk.running && !wk._tokenInvalid) {
607
+ if (wk.paused || wk.dashboardPaused) paused++;
608
+ else active++;
609
+ }
638
610
  }
639
611
 
640
- const uptime = this._fmtUptime(Date.now() - (this._startTime || Date.now()));
641
- const rate = totalCmds > 0 ? ((totalSuccess / totalCmds) * 100).toFixed(0) : 0;
612
+ const uptime = this._fmtUptime(Date.now() - this._startTime);
613
+ const rate = totalCmds > 0 ? ((totalSuccess / totalCmds) * 100).toFixed(0) : '0';
642
614
 
643
615
  return [
644
- `${dim}⏱${A.reset} ${g}${uptime}${A.reset}`,
645
- `${dim}⬡${A.reset} ${g}${this.workers.length}${A.reset} ${dim}accounts${A.reset}`,
646
- `${coins}⏣${A.reset} ${g}${totalCoins.toLocaleString()}${A.reset}`,
647
- `${dim}⚡${A.reset} ${g}${totalCmds}${A.reset} ${dim}cmds${A.reset}`,
648
- `${dim}📊${A.reset} ${g}${rate}%${A.reset}`,
649
- `${C.lifesavers}♥${A.reset} ${g}${totalLs}${A.reset}`,
650
- `${h}🟢${A.reset} ${g}${activeCount}${A.reset} ${h}🔴${A.reset} ${g}${pausedCount}${A.reset}`,
651
- ].join(' ');
616
+ `${A.dim}⏱${A.reset} ${C.statValue}${uptime}${A.reset}`,
617
+ `${A.dim}⬡${A.reset} ${C.statValue}${this.workers.length}${A.reset} ${A.dim}accounts${A.reset}`,
618
+ `${C.coins}⏣${A.reset} ${C.statValue}${totalCoins.toLocaleString()}${A.reset}`,
619
+ `${A.dim}⚡${A.reset} ${C.statValue}${totalCmds}${A.reset} ${A.dim}cmds${A.reset}`,
620
+ `${A.dim}📊${A.reset} ${C.statValue}${rate}%${A.reset}`,
621
+ `${C.lifesavers}♥${A.reset} ${C.statValue}${totalLs}${A.reset}`,
622
+ `${C.statusActive}🟢${A.reset} ${C.statValue}${active}${A.reset}`,
623
+ `${C.statusPaused}🔴${A.reset} ${C.statValue}${paused}${A.reset}`,
624
+ ].join(` │ `);
652
625
  }
653
626
 
654
627
  _drawAccounts() {
655
628
  const w = this._w;
656
629
  const b = C.border;
657
- const dim = A.dim;
630
+ const h = C.header;
658
631
  const r = A.reset;
659
- const sep = `${b}${'─'.repeat(w - 2)}${r}`;
660
-
661
- const visible = this.workers.slice(this.windowStart, this.windowStart + this.windowSize);
662
632
 
663
- // Column header
633
+ // Column headers
664
634
  const cols = [
665
- `${C.header}#${A.reset}`,
666
- `${C.header}ACCOUNT${A.reset}`,
667
- `${C.header}COINS${A.reset}`,
668
- `${C.header}LV${A.reset}`,
669
- `${C.header}♥${A.reset}`,
670
- `${C.header}CMDS${A.reset}`,
671
- `${C.header}OK%${A.reset}`,
672
- `${C.header}STATUS${A.reset}`,
673
- `${C.header}CURRENT${A.reset}`,
674
- ].join(' ');
675
- this._write(`${this._at(this._accountsRow, 1)}${b}${this._rpad(' ' + cols, w - 2)}${r}`);
676
- this._write(`${this._at(this._accountsRow + 1, 1)}${sep}`);
635
+ `${h}#${r}`, `${h}ACCOUNT${r}`, `${h}COINS${r}`,
636
+ `${h}LV${r}`, `${h}♥${r}`, `${h}CMDS${r}`,
637
+ `${h}OK%${r}`, `${h}STATUS${r}`,
638
+ ].join(` `);
639
+ this._write(`${this._at(this._accountsRow, 1)}${b} ${this._rpad(cols, w - 4)} ${C.v}${r}`);
640
+ this._write(`${this._at(this._accountsRow + 1, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`);
677
641
 
678
- // Worker rows
642
+ const visible = this.workers.slice(this.windowStart, this.windowStart + this.windowSize);
679
643
  for (let i = 0; i < this.windowSize; i++) {
680
644
  const row = this._accountsRow + 2 + i;
681
645
  if (row > this._h - 4) break;
682
-
683
646
  if (i < visible.length) {
684
- const wk = visible[i];
685
- const line = this._buildAccountRow(wk, this.windowStart + i);
686
- this._write(`${this._at(row, 1)}${b}${line}${r}`);
647
+ const line = this._buildAccountRow(visible[i], this.windowStart + i);
648
+ this._write(`${this._at(row, 1)}${b} ${line} ${C.v}${r}`);
687
649
  } else {
688
650
  this._write(`${this._at(row, 1)}${b}${' '.repeat(w - 2)}${r}`);
689
651
  }
690
652
  }
691
- this._eventsRow = this._accountsRow + 2 + this.windowSize + 1;
653
+ this._eventsRow = this._accountsRow + 2 + Math.min(this.windowSize, this.workers.length);
692
654
  }
693
655
 
694
656
  _drawEvents() {
@@ -696,25 +658,24 @@ class Terminal {
696
658
  const b = C.border;
697
659
  const dim = A.dim;
698
660
  const r = A.reset;
699
- const sep = `${b}${'─'.repeat(w - 2)}${r}`;
700
661
 
701
662
  if (this._eventsRow > this._h - 4) return;
702
- this._write(`${this._at(this._eventsRow, 1)}${sep}`);
663
+ this._write(`${this._at(this._eventsRow, 1)}${b}${C.h}${'─'.repeat(w - 2)}${C.h}${r}`);
703
664
 
704
- const visibleEvents = this.events.slice(0, Math.min(this.MAX_EVENTS, this._h - this._eventsRow - 3));
705
- for (let i = 0; i < visibleEvents.length; i++) {
665
+ const visible = this.events.slice(0, Math.min(this.MAX_EVENTS, this._h - this._eventsRow - 3));
666
+ for (let i = 0; i < visible.length; i++) {
706
667
  const row = this._eventsRow + 1 + i;
707
668
  if (row > this._h - 2) break;
708
- const evt = visibleEvents[i];
709
- const typeColor = evt.type === 'error' || evt.type === 'death' ? C.cmdError
710
- : evt.type === 'warn' || evt.type === 'lowls' ? C.cmdWarn
711
- : evt.type === 'success' || evt.type === 'levelup' ? C.cmdSuccess
712
- : C.cmdEvent;
713
-
714
- const line = ` ${dim}${evt.ts}${A.reset} ${typeColor}${evt.msg}${A.reset}`;
715
- this._write(`${this._at(row, 1)}${b}${this._rpad(line, w - 2)}${r}`);
669
+ const e = visible[i];
670
+ const typeColor = e.type === 'death' ? C.cmdError
671
+ : e.type === 'lowls' ? C.cmdWarn
672
+ : e.type === 'levelup' ? C.cmdSuccess
673
+ : e.type === 'success' ? C.cmdSuccess
674
+ : C.cmdInfo;
675
+ const line = ` ${dim}${e.ts}${A.reset} ${typeColor}${e.msg}${A.reset}`;
676
+ this._write(`${this._at(row, 1)}${b} ${this._rpad(line, w - 4)} ${C.v}${r}`);
716
677
  }
717
- this._footerRow = this._eventsRow + 1 + visibleEvents.length;
678
+ this._footerRow = this._eventsRow + 1 + visible.length;
718
679
  }
719
680
 
720
681
  _drawFooter() {
@@ -722,56 +683,36 @@ class Terminal {
722
683
  const b = C.border;
723
684
  const dim = A.dim;
724
685
  const r = A.reset;
725
- const sep = `${b}${'─'.repeat(w - 2)}${r}`;
726
-
727
686
  if (this._footerRow > this._h - 1) this._footerRow = this._h - 2;
728
- this._write(`${this._at(this._footerRow, 1)}${sep}`);
729
-
730
- const hint = `${dim}↑↓ scroll · j/k navigate · Ctrl+C quit${r}`;
687
+ this._write(`${this._at(this._footerRow, 1)}${b}${C.botLeft}${'─'.repeat(w - 2)}${C.botRight}${r}`);
688
+ const hint = `${dim}↑↓ scroll${r}`;
731
689
  this._write(`${this._at(this._footerRow + 1, 1)}${b} ${hint}${' '.repeat(Math.max(0, w - 2 - this._ansiLen(hint)))}${r}`);
732
690
  }
733
691
 
734
692
  _render() {
735
693
  if (!READY || this._shutdown || !this._active) return;
736
694
 
737
- // Auto-follow newest active worker
738
- if (this._followWorkerIdx >= 0 && this._followWorkerIdx < this.workers.length) {
739
- const target = this._followWorkerIdx;
740
- if (target < this.windowStart) {
741
- this.windowStart = target;
742
- this.dirtyWorkers = new Set();
743
- for (let i = this.windowStart; i < this.windowStart + this.windowSize; i++) {
744
- if (i < this.workers.length) this.dirtyWorkers.add(i);
745
- }
746
- } else if (target >= this.windowStart + this.windowSize) {
747
- this.windowStart = target - this.windowSize + 1;
748
- this.dirtyWorkers = new Set();
749
- for (let i = this.windowStart; i < this.windowStart + this.windowSize; i++) {
750
- if (i < this.workers.length) this.dirtyWorkers.add(i);
751
- }
752
- }
753
- }
754
-
755
- // Redraw everything on resize changes
695
+ // Only update stats line (row 4) when dirty — don't redraw whole header
756
696
  if (this.dirtyStats) {
757
- this._drawHeader();
758
- this.dirtyWorkers = new Set(this.workers.map((_, i) => i));
697
+ const w = this._w;
698
+ const b = C.border;
699
+ const stats = this._buildStatsLine();
700
+ const statsPad = Math.max(0, w - 2 - this._ansiLen(stats) - 2);
701
+ this._write(`${this._at(4, 1)}${b} ${stats}${' '.repeat(statsPad)} ${C.v}${A.reset}`);
702
+ this.dirtyStats = false;
759
703
  }
760
704
 
761
705
  if (this.dirtyWorkers.size > 0) {
762
706
  const w = this._w;
763
707
  const b = C.border;
764
708
  const r = A.reset;
765
- const sep = `${b}${'─'.repeat(w - 2)}${r}`;
766
-
767
709
  for (const idx of this.dirtyWorkers) {
768
710
  const localIdx = idx - this.windowStart;
769
711
  const row = this._accountsRow + 2 + localIdx;
770
712
  if (row < this._accountsRow + 2 || row > this._h - 4) continue;
771
-
772
713
  if (idx < this.workers.length) {
773
714
  const line = this._buildAccountRow(this.workers[idx], idx);
774
- this._write(`${this._at(row, 1)}${b}${line}${r}`);
715
+ this._write(`${this._at(row, 1)}${b} ${line} ${C.v}${r}`);
775
716
  } else {
776
717
  this._write(`${this._at(row, 1)}${b}${' '.repeat(w - 2)}${r}`);
777
718
  }
@@ -780,40 +721,18 @@ class Terminal {
780
721
 
781
722
  if (this.dirtyEvents) {
782
723
  this._drawEvents();
783
- this._drawFooter();
784
724
  }
785
725
 
786
726
  this.dirtyWorkers.clear();
787
727
  this.dirtyEvents = false;
788
- this.dirtyStats = false;
789
728
  }
790
729
 
791
730
  _startRenderLoop() {
792
731
  if (this._renderTimer) clearInterval(this._renderTimer);
793
- this._renderTimer = setInterval(() => this._render(), 250); // 4 FPS
794
- }
795
-
796
- _printSummaryPlain(summary) {
797
- const { totalCoins = 0, totalCmds = 0, totalSuccess = 0,
798
- workers = [], uptime = 0, memMB = 0 } = summary;
799
- const b = '═'.repeat(60);
800
- console.log('');
801
- console.log(` ${'═'.repeat(60)}`);
802
- console.log(` ⬡ DANKGRINDER — Session Summary`);
803
- console.log(` ${'─'.repeat(60)}`);
804
- for (const wk of workers) {
805
- const rate = wk.stats?.commands > 0
806
- ? `${((wk.stats.successes / wk.stats.commands) * 100).toFixed(0)}%`
807
- : '0%';
808
- console.log(
809
- ` 💎 ${(wk.username || '?').padEnd(18)} +⏣ ${(wk.stats?.coins || 0).toLocaleString().padStart(8)} ` +
810
- `${(wk.stats?.commands || 0)}cmds ${rate} ♥${wk._lifesavers ?? '?'} Lv.${wk._level ?? '?'}`
811
- );
812
- }
813
- console.log(` ${'─'.repeat(60)}`);
814
- console.log(` 💰 Total: ⏣ ${totalCoins.toLocaleString()} ${totalCmds}cmds ${totalSuccess}%OK ${this._fmtUptime(uptime)} ${memMB}MB`);
815
- console.log(` ${'═'.repeat(60)}`);
816
- console.log('');
732
+ this._renderTimer = setInterval(() => {
733
+ this.dirtyStats = true; // always refresh uptime/stats
734
+ this._render();
735
+ }, 250);
817
736
  }
818
737
  }
819
738
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "7.78.0",
3
+ "version": "7.79.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"