dankgrinder 8.49.0 → 8.53.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 +2 -0
- package/lib/ui.js +116 -73
- package/package.json +1 -1
package/lib/grinder.js
CHANGED
|
@@ -2932,6 +2932,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2932
2932
|
ui.log(-1, `Starting ${activeWorkers.length} grind loops...`);
|
|
2933
2933
|
ui.setLive(true);
|
|
2934
2934
|
ui.draw();
|
|
2935
|
+
ui.startRefresh();
|
|
2935
2936
|
for (const w of activeWorkers) {
|
|
2936
2937
|
if (!shutdownCalled) w.grindLoop();
|
|
2937
2938
|
}
|
|
@@ -3002,6 +3003,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3002
3003
|
shutdownInProgress = true;
|
|
3003
3004
|
shutdownCalled = true;
|
|
3004
3005
|
setDashboardActive(false);
|
|
3006
|
+
ui.stopRefresh();
|
|
3005
3007
|
ui.stop();
|
|
3006
3008
|
process.stdout.write(c.show + '\n');
|
|
3007
3009
|
|
package/lib/ui.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI Live Dashboard —
|
|
3
|
-
* Box
|
|
2
|
+
* CLI Live Dashboard — cursor-positioned in-place row updates.
|
|
3
|
+
* Box drawn once at startup. Account rows updated in-place. Events below box.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
let _startTime = Date.now();
|
|
7
7
|
let _workers = [];
|
|
8
|
-
let _isShuttingDown = () => false;
|
|
9
8
|
let _version = '0.0.0';
|
|
10
9
|
let _live = false;
|
|
11
|
-
let _phase = 'init';
|
|
10
|
+
let _phase = 'init';
|
|
11
|
+
|
|
12
|
+
// Terminal dimensions at last draw
|
|
13
|
+
let _W = 100;
|
|
14
|
+
let _inner = 98;
|
|
15
|
+
let _maxAccounts = 4;
|
|
16
|
+
|
|
17
|
+
// Row map: which terminal row each account row starts at (1-indexed)
|
|
18
|
+
let _accountRows = []; // _accountRows[accountIdx] = terminalRow
|
|
12
19
|
|
|
13
20
|
// ── Spinner frames ────────────────────────────────────────────
|
|
14
21
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
15
22
|
function spinnerFrame() { return SPINNER[Math.floor(Date.now() / 150) % SPINNER.length]; }
|
|
16
23
|
|
|
17
|
-
// ──
|
|
24
|
+
// ── ASCII banner ──────────────────────────────────────────────
|
|
18
25
|
const BANNER_LINES = [
|
|
19
26
|
' ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗ DANKGRINDER',
|
|
20
27
|
' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║ vPLACEHOLDER',
|
|
@@ -40,7 +47,6 @@ const c = {
|
|
|
40
47
|
red: '\x1b[38;2;255;80;100m',
|
|
41
48
|
yellow: '\x1b[38;2;255;220;80m',
|
|
42
49
|
cyan: '\x1b[38;2;80;220;255m',
|
|
43
|
-
blue: '\x1b[38;2;100;160;255m',
|
|
44
50
|
white: '\x1b[37m',
|
|
45
51
|
};
|
|
46
52
|
const DIM = c.dim;
|
|
@@ -49,6 +55,7 @@ function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0
|
|
|
49
55
|
function padR(s, n) { return trunc(s, n).padEnd(n); }
|
|
50
56
|
function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
|
|
51
57
|
function stripAnsi(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, ''); }
|
|
58
|
+
function clearLine() { process.stdout.write('\x1b[2K'); } // clear current line
|
|
52
59
|
|
|
53
60
|
function fmtUptime() {
|
|
54
61
|
const s = Math.floor((Date.now() - _startTime) / 1000);
|
|
@@ -66,7 +73,7 @@ function statusColor(w) {
|
|
|
66
73
|
if (!w.channel) return c.red;
|
|
67
74
|
if (w.paused || w.dashboardPaused) return c.yellow;
|
|
68
75
|
if (w.busy || w._invRunning || w._sellRunning) return c.yellow;
|
|
69
|
-
if (Date.now() < w.globalCooldownUntil) return c.
|
|
76
|
+
if (Date.now() < w.globalCooldownUntil) return c.cyan;
|
|
70
77
|
return c.green;
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -79,7 +86,7 @@ function statusText(w) {
|
|
|
79
86
|
if (w.busy || w._invRunning || w._sellRunning) return spinnerFrame() + ' WORK';
|
|
80
87
|
if (Date.now() < w.globalCooldownUntil) {
|
|
81
88
|
const wait = Math.ceil((w.globalCooldownUntil - Date.now()) / 1000);
|
|
82
|
-
return '⏳' + wait + 's';
|
|
89
|
+
return '⏳' + padL(wait, 3) + 's';
|
|
83
90
|
}
|
|
84
91
|
return '● READY';
|
|
85
92
|
}
|
|
@@ -107,39 +114,56 @@ function fmtLevel(w) {
|
|
|
107
114
|
return c.cyan + lv + c.reset;
|
|
108
115
|
}
|
|
109
116
|
|
|
110
|
-
// ──
|
|
111
|
-
function
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
117
|
+
// ── Build a full account row string ──────────────────────────
|
|
118
|
+
function buildAccountRow(w, wi, col) {
|
|
119
|
+
const col2 = wc(wi);
|
|
120
|
+
const stCol = statusColor(w);
|
|
121
|
+
const stTxt = statusText(w);
|
|
122
|
+
const earned = fmtCoins(w.stats.coins);
|
|
123
|
+
const bal = w.stats.balance !== undefined ? fmtCoins(w.stats.balance) : DIM + '?' + c.reset;
|
|
124
|
+
const ls = fmtLifesavers(w);
|
|
125
|
+
const lv = fmtLevel(w);
|
|
126
|
+
const name = padR(trunc(w.username || '?', col.name), col.name);
|
|
127
|
+
const doing = padR(w.lastStatus || 'idle', col.cmd);
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
'\x1b[2K' + // clear the line first
|
|
131
|
+
`\x1b[38;2;77;212;238m│\x1b[0m ` +
|
|
132
|
+
`${DIM}${padL(wi + 1, 2)}${c.reset} ` +
|
|
133
|
+
`${stCol}${padR(stTxt, col.st)}${c.reset} ` +
|
|
134
|
+
`${col2}${name}${c.reset} ` +
|
|
135
|
+
`${DIM}${doing}${c.reset} ` +
|
|
136
|
+
`${padL(bal, col.bal)} ` +
|
|
137
|
+
`${padL(ls, col.ls)} ` +
|
|
138
|
+
`${padL(lv, col.lv)} ` +
|
|
139
|
+
`${padL(earned, col.earned)} ` +
|
|
140
|
+
`\x1b[38;2;77;212;238m│\x1b[0m\n`
|
|
141
|
+
);
|
|
116
142
|
}
|
|
117
143
|
|
|
118
|
-
// ── Draw the
|
|
144
|
+
// ── Draw the FULL box (called once at startup) ───────────────
|
|
119
145
|
function draw() {
|
|
120
|
-
const
|
|
121
|
-
|
|
146
|
+
const rows = process.stdout.rows || 40;
|
|
147
|
+
_W = Math.min(process.stdout.columns || 100, 120);
|
|
148
|
+
_inner = _W - 2;
|
|
122
149
|
|
|
123
|
-
//
|
|
150
|
+
// Clear entire screen
|
|
124
151
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
125
152
|
|
|
126
153
|
// ── Top border ──
|
|
127
|
-
process.stdout.write(`\x1b[38;2;77;212;238m┌${'─'.repeat(
|
|
154
|
+
process.stdout.write(`\x1b[38;2;77;212;238m┌${'─'.repeat(_inner)}┐\x1b[0m\n`);
|
|
128
155
|
|
|
129
156
|
// ── ASCII banner ──
|
|
130
157
|
const bannerWithVer = BANNER_LINES[1].replace('PLACEHOLDER', _version);
|
|
131
158
|
const bannerLines = [BANNER_LINES[0], bannerWithVer, ...BANNER_LINES.slice(2)];
|
|
132
|
-
|
|
133
159
|
for (let i = 0; i < bannerLines.length; i++) {
|
|
134
160
|
const line = bannerLines[i];
|
|
161
|
+
const inner = _inner - stripAnsi(line).length;
|
|
135
162
|
if (i < 2) {
|
|
136
163
|
const gradLine = gradientLine(line, 77, 212, 238, 255, 92, 147);
|
|
137
|
-
|
|
138
|
-
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m${gradLine}${pad}\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
164
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m${gradLine}${' '.repeat(_inner - stripAnsi(gradLine).length)}\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
139
165
|
} else {
|
|
140
|
-
|
|
141
|
-
const pad = ' '.repeat(Math.max(0, inner - stripAnsi(dimLine).length));
|
|
142
|
-
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m${dimLine}${pad}\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
166
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m${DIM}${line}${c.reset}${' '.repeat(_inner - stripAnsi(line).length)}\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
143
167
|
}
|
|
144
168
|
}
|
|
145
169
|
|
|
@@ -147,7 +171,7 @@ function draw() {
|
|
|
147
171
|
const running = _workers.filter(w => w.channel && !w.paused && !w.dashboardPaused).length;
|
|
148
172
|
const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
|
|
149
173
|
const errors = _workers.filter(w => !w.channel).length;
|
|
150
|
-
const phaseLabel = _phase === 'grinding' ? '' : ` ${
|
|
174
|
+
const phaseLabel = _phase === 'grinding' ? '' : ` ${DIM}[${_phase}]${c.reset}`;
|
|
151
175
|
const statusParts = [
|
|
152
176
|
`${c.green}●${c.reset} ${running} online`,
|
|
153
177
|
paused > 0 ? `${c.yellow}~${c.reset} ${paused} paused` : null,
|
|
@@ -155,16 +179,15 @@ function draw() {
|
|
|
155
179
|
`${DIM}Ctrl+C${c.reset}`,
|
|
156
180
|
].filter(Boolean);
|
|
157
181
|
const statusStr = statusParts.join(' ') + phaseLabel;
|
|
158
|
-
|
|
159
|
-
process.stdout.write(`\x1b[38;2;77;212;238m
|
|
160
|
-
process.stdout.write(`\x1b[38;2;77;212;238m├${'─'.repeat(inner)}┤\x1b[0m\n`);
|
|
182
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${statusStr}${' '.repeat(_inner - stripAnsi(statusStr))}\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
183
|
+
process.stdout.write(`\x1b[38;2;77;212;238m├${'─'.repeat(_inner)}┤\x1b[0m\n`);
|
|
161
184
|
|
|
162
185
|
// ── Table header ──
|
|
163
186
|
const col = { st: 9, name: 18, cmd: 16, bal: 8, ls: 3, lv: 3, earned: 8 };
|
|
164
|
-
const nameExtra = Math.max(0,
|
|
187
|
+
const nameExtra = Math.max(0, _inner - col.st - col.name - col.cmd - col.bal - col.ls - col.lv - col.earned - 14);
|
|
165
188
|
col.name += nameExtra;
|
|
166
189
|
|
|
167
|
-
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
|
|
190
|
+
process.stdout.write(`\x1b[2K\x1b[38;2;77;212;238m│\x1b[0m `);
|
|
168
191
|
process.stdout.write(`${c.bold}#${c.reset} `);
|
|
169
192
|
process.stdout.write(`${c.bold}${padR('STATUS', col.st)}${c.reset} `);
|
|
170
193
|
process.stdout.write(`${c.bold}${padR('ACCOUNT', col.name)}${c.reset} `);
|
|
@@ -174,7 +197,16 @@ function draw() {
|
|
|
174
197
|
process.stdout.write(`${c.bold}${padL('LV', col.lv)}${c.reset} `);
|
|
175
198
|
process.stdout.write(`${c.bold}${padL('EARNED', col.earned)}${c.reset} `);
|
|
176
199
|
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
177
|
-
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(
|
|
200
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(_inner)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
201
|
+
|
|
202
|
+
// Track row positions
|
|
203
|
+
const BOX_TOP = 1;
|
|
204
|
+
const bannerH = bannerLines.length; // 6
|
|
205
|
+
const statusBarRow = BOX_TOP + bannerH + 1; // row 8
|
|
206
|
+
const headerRow = statusBarRow + 1; // row 9
|
|
207
|
+
const hrRow = headerRow + 1; // row 10
|
|
208
|
+
_maxAccounts = Math.min(_workers.length, Math.max(3, rows - 14));
|
|
209
|
+
_accountRows = [];
|
|
178
210
|
|
|
179
211
|
// ── Account rows ──
|
|
180
212
|
const sorted = [..._workers].sort((a, b) => {
|
|
@@ -184,49 +216,27 @@ function draw() {
|
|
|
184
216
|
if (aA !== bA) return bA ? 1 : -1;
|
|
185
217
|
return (b.stats.commands || 0) - (a.stats.commands || 0);
|
|
186
218
|
});
|
|
187
|
-
const shown = sorted.slice(0,
|
|
219
|
+
const shown = sorted.slice(0, _maxAccounts);
|
|
188
220
|
|
|
189
221
|
for (let si = 0; si < shown.length; si++) {
|
|
190
222
|
const w = shown[si];
|
|
191
223
|
const wi = _workers.indexOf(w);
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const earned = fmtCoins(w.stats.coins);
|
|
196
|
-
const bal = w.stats.balance !== undefined ? fmtCoins(w.stats.balance) : DIM + '?' + c.reset;
|
|
197
|
-
const ls = fmtLifesavers(w);
|
|
198
|
-
const lv = fmtLevel(w);
|
|
199
|
-
|
|
200
|
-
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
|
|
201
|
-
process.stdout.write(`${DIM}${padL(wi + 1, 2)}${c.reset} `);
|
|
202
|
-
process.stdout.write(`${stCol}${padR(stTxt, col.st)}${c.reset} `);
|
|
203
|
-
process.stdout.write(`${col2}${padR(trunc(w.username || '?', col.name), col.name)}${c.reset} `);
|
|
204
|
-
process.stdout.write(`${DIM}${padR(w.lastStatus || 'idle', col.cmd)}${c.reset} `);
|
|
205
|
-
process.stdout.write(`${padL(bal, col.bal)} `);
|
|
206
|
-
process.stdout.write(`${padL(ls, col.ls)} `);
|
|
207
|
-
process.stdout.write(`${padL(lv, col.lv)} `);
|
|
208
|
-
process.stdout.write(`${padL(earned, col.earned)} `);
|
|
209
|
-
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
if (sorted.length > maxAccounts) {
|
|
213
|
-
const extra = sorted.length - maxAccounts;
|
|
214
|
-
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${DIM}+ ${extra} more accounts${' '.repeat(Math.max(0, inner - 20 - String(extra).length))}${c.reset} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
224
|
+
const rowNum = hrRow + 1 + si;
|
|
225
|
+
_accountRows[wi] = rowNum;
|
|
226
|
+
process.stdout.write(buildAccountRow(w, wi, col));
|
|
215
227
|
}
|
|
216
228
|
|
|
217
229
|
// ── Totals ──
|
|
218
|
-
let totalCoins = 0, totalBal = 0
|
|
230
|
+
let totalCoins = 0, totalBal = 0;
|
|
219
231
|
for (const w of _workers) {
|
|
220
232
|
totalCoins += w.stats.coins || 0;
|
|
221
233
|
totalBal += w.stats.balance || 0;
|
|
222
|
-
totalCmds += w.stats.commands || 0;
|
|
223
|
-
totalOk += w.stats.successes || 0;
|
|
224
234
|
}
|
|
225
|
-
const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
|
|
226
235
|
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
227
236
|
|
|
228
|
-
|
|
229
|
-
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
|
|
237
|
+
const totalsRow = hrRow + 1 + shown.length;
|
|
238
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(_inner)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
239
|
+
process.stdout.write(`\x1b[2K\x1b[38;2;77;212;238m│\x1b[0m `);
|
|
230
240
|
process.stdout.write(`${c.bold}Σ${c.reset} `);
|
|
231
241
|
process.stdout.write(`${DIM}${padL(_workers.length, 2)} acc${c.reset} `);
|
|
232
242
|
process.stdout.write(`${' '.repeat(col.name)} `);
|
|
@@ -235,11 +245,28 @@ function draw() {
|
|
|
235
245
|
process.stdout.write(`${' '.repeat(col.ls)} `);
|
|
236
246
|
process.stdout.write(`${' '.repeat(col.lv)} `);
|
|
237
247
|
process.stdout.write(`${fmtCoins(totalCoins)} `);
|
|
238
|
-
process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(
|
|
248
|
+
process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(_inner));
|
|
239
249
|
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
240
250
|
|
|
241
251
|
// ── Bottom ──
|
|
242
|
-
process.stdout.write(`\x1b[38;2;77;212;238m└${'─'.repeat(
|
|
252
|
+
process.stdout.write(`\x1b[38;2;77;212;238m└${'─'.repeat(_inner)}┘\x1b[0m\n`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Update ONE account row in place ─────────────────────────
|
|
256
|
+
function updateAccountRow(accountIdx) {
|
|
257
|
+
const row = _accountRows[accountIdx];
|
|
258
|
+
if (!row) return; // not visible
|
|
259
|
+
|
|
260
|
+
const w = _workers[accountIdx];
|
|
261
|
+
if (!w) return;
|
|
262
|
+
|
|
263
|
+
const col = { st: 9, name: 18, cmd: 16, bal: 8, ls: 3, lv: 3, earned: 8 };
|
|
264
|
+
const nameExtra = Math.max(0, _inner - col.st - col.name - col.cmd - col.bal - col.ls - col.lv - col.earned - 14);
|
|
265
|
+
col.name += nameExtra;
|
|
266
|
+
|
|
267
|
+
// Move cursor to this row, column 1
|
|
268
|
+
process.stdout.write(`\x1b[${row};1H`);
|
|
269
|
+
process.stdout.write(buildAccountRow(w, accountIdx, col));
|
|
243
270
|
}
|
|
244
271
|
|
|
245
272
|
// ── Gradient line ─────────────────────────────────────────────
|
|
@@ -254,34 +281,33 @@ function gradientLine(text, r1, g1, b1, r2, g2, b2) {
|
|
|
254
281
|
|
|
255
282
|
// ── Event tracking ────────────────────────────────────────────
|
|
256
283
|
let _eventLines = []; // [accountIdx] = [{text, ts}]
|
|
257
|
-
const MAX_EVENTS =
|
|
284
|
+
const MAX_EVENTS = 15;
|
|
258
285
|
|
|
259
286
|
// ── Public API ────────────────────────────────────────────────
|
|
260
287
|
|
|
261
288
|
function init({ workers, isShuttingDown }) {
|
|
262
289
|
_startTime = Date.now();
|
|
263
290
|
_workers = workers;
|
|
264
|
-
_isShuttingDown = isShuttingDown || (() => false);
|
|
265
291
|
_version = '0.0.0';
|
|
266
292
|
_eventLines = [];
|
|
267
293
|
_live = false;
|
|
268
294
|
_phase = 'init';
|
|
295
|
+
_accountRows = [];
|
|
269
296
|
}
|
|
270
297
|
|
|
271
298
|
function drawBanner(version) { _version = version || '0.0.0'; }
|
|
272
|
-
|
|
273
299
|
function start() {}
|
|
300
|
+
|
|
274
301
|
function stop() {
|
|
275
302
|
_live = false;
|
|
276
303
|
_phase = 'init';
|
|
277
|
-
// Clear screen on shutdown so box disappears
|
|
278
304
|
process.stdout.write('\x1b[2J\x1b[H' + c.reset + '\n');
|
|
279
305
|
}
|
|
280
306
|
|
|
281
307
|
function setLive(val) { _live = val; }
|
|
282
308
|
function setPhase(phase) { _phase = phase; }
|
|
283
309
|
|
|
284
|
-
// log:
|
|
310
|
+
// log: update account row in-place + append event below box
|
|
285
311
|
function log(accountIdx, msg) {
|
|
286
312
|
const now = new Date();
|
|
287
313
|
const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
|
|
@@ -290,12 +316,13 @@ function log(accountIdx, msg) {
|
|
|
290
316
|
if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
|
|
291
317
|
_eventLines[accountIdx].push({ text: msg, ts });
|
|
292
318
|
if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
|
|
319
|
+
// Update the row in place
|
|
320
|
+
if (_live) updateAccountRow(accountIdx);
|
|
293
321
|
}
|
|
294
322
|
|
|
295
323
|
if (!_live) return;
|
|
296
324
|
|
|
297
|
-
//
|
|
298
|
-
draw();
|
|
325
|
+
// Append event below the box (after the bottom border)
|
|
299
326
|
const col2 = accountIdx >= 0 ? wc(accountIdx) : c.cyan;
|
|
300
327
|
const name = accountIdx >= 0 ? trunc(_workers[accountIdx]?.username || '?', 14) : 'GLOBAL';
|
|
301
328
|
process.stdout.write(`${col2}${name}${c.reset} ${DIM}[${ts}]${c.reset} ${msg}\n`);
|
|
@@ -303,6 +330,22 @@ function log(accountIdx, msg) {
|
|
|
303
330
|
|
|
304
331
|
function logGlobal(msg) { log(-1, msg); }
|
|
305
332
|
|
|
306
|
-
//
|
|
333
|
+
// Force full redraw (call this when window resizes or box needs rebuild)
|
|
334
|
+
function fullRedraw() { if (_live) draw(); }
|
|
335
|
+
|
|
336
|
+
// ── Periodic refresh: animate spinners & countdowns every 3s ──
|
|
337
|
+
let _refreshTimer = null;
|
|
338
|
+
function startRefresh() {
|
|
339
|
+
if (_refreshTimer) return;
|
|
340
|
+
_refreshTimer = setInterval(() => {
|
|
341
|
+
if (!_live) return;
|
|
342
|
+
for (let i = 0; i < _workers.length; i++) {
|
|
343
|
+
updateAccountRow(i);
|
|
344
|
+
}
|
|
345
|
+
}, 1500);
|
|
346
|
+
}
|
|
347
|
+
function stopRefresh() {
|
|
348
|
+
if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
|
|
349
|
+
}
|
|
307
350
|
|
|
308
|
-
module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase };
|
|
351
|
+
module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase, updateAccountRow, fullRedraw, startRefresh, stopRefresh };
|