dankgrinder 8.53.0 → 8.56.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 +62 -24
- package/lib/ui.js +108 -95
- package/package.json +1 -1
package/lib/grinder.js
CHANGED
|
@@ -2002,11 +2002,25 @@ class AccountWorker {
|
|
|
2002
2002
|
|
|
2003
2003
|
// Set up error/disconnect handlers for auto-recovery
|
|
2004
2004
|
this._attachRecoveryListeners();
|
|
2005
|
-
rawLogger.attachRawLogger(this.client, { channelId: this.account.channel_id });
|
|
2005
|
+
rawLogger.attachRawLogger(this.client, { channelId: this.channel?.id || this.account.channel_id || '' });
|
|
2006
2006
|
rawLogger.attachDmLogger(this.client);
|
|
2007
2007
|
|
|
2008
|
-
|
|
2009
|
-
|
|
2008
|
+
// DM mode: resolve Dank Memer DM after login
|
|
2009
|
+
if (isDMMode) {
|
|
2010
|
+
const recoveryLoginToken = this.account.access_token || this.account.discord_token;
|
|
2011
|
+
await this.client.login(recoveryLoginToken);
|
|
2012
|
+
try {
|
|
2013
|
+
const dankUser = await this.client.users.fetch(DANK_MEMER_ID);
|
|
2014
|
+
this.channel = await dankUser.createDM();
|
|
2015
|
+
this._dmChannelId = this.channel.id;
|
|
2016
|
+
this.log('info', `DM re-opened with Dank Memer`);
|
|
2017
|
+
} catch (err) {
|
|
2018
|
+
this.log('error', `Failed to re-open DM: ${err?.message || err}`);
|
|
2019
|
+
}
|
|
2020
|
+
} else {
|
|
2021
|
+
await this.client.login(this.account.discord_token);
|
|
2022
|
+
this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
|
|
2023
|
+
}
|
|
2010
2024
|
|
|
2011
2025
|
if (this.channel) {
|
|
2012
2026
|
this._recoveryAttempts = 0;
|
|
@@ -2486,8 +2500,13 @@ class AccountWorker {
|
|
|
2486
2500
|
} else {
|
|
2487
2501
|
process.stdout.write(`[WORKER START] ${this.account.label || this.account.id}\n`);
|
|
2488
2502
|
}
|
|
2489
|
-
if (!this.account.discord_token) { this.log('error', 'No token'); return; }
|
|
2490
|
-
if (!this.account.channel_id) { this.log('error', 'No channel'); return; }
|
|
2503
|
+
if (!this.account.discord_token && !this.account.access_token) { this.log('error', 'No token or access token'); return; }
|
|
2504
|
+
if (!this.account.channel_id && this.account.grind_mode !== 'dm') { this.log('error', 'No channel'); return; }
|
|
2505
|
+
|
|
2506
|
+
// Determine login token: OAuth access_token takes priority over discord_token
|
|
2507
|
+
const loginToken = this.account.access_token || this.account.discord_token;
|
|
2508
|
+
const grindMode = this.account.grind_mode || 'channel';
|
|
2509
|
+
const isDMMode = grindMode === 'dm';
|
|
2491
2510
|
|
|
2492
2511
|
const LOGIN_TIMEOUT_MS = 30_000; // 30s max per account login
|
|
2493
2512
|
|
|
@@ -2512,8 +2531,8 @@ class AccountWorker {
|
|
|
2512
2531
|
this.loggedIn = true; // so statusColor/statusText show correct state
|
|
2513
2532
|
|
|
2514
2533
|
// Attach raw gateway logger for CV2 component capture
|
|
2515
|
-
rawLogger.attachRawLogger(this.client, { channelId: this.
|
|
2516
|
-
|
|
2534
|
+
rawLogger.attachRawLogger(this.client, { channelId: this.channel?.id || '' });
|
|
2535
|
+
rawLogger.attachDmLogger(this.client);
|
|
2517
2536
|
|
|
2518
2537
|
// Report status non-blocking
|
|
2519
2538
|
fetch(`${API_URL}/api/grinder/status`, {
|
|
@@ -2522,10 +2541,23 @@ class AccountWorker {
|
|
|
2522
2541
|
body: JSON.stringify({ account_id: this.account.id, discord_username: this.username, avatar_url: this.avatarUrl, userId: this.account.userId }),
|
|
2523
2542
|
}).catch(() => {});
|
|
2524
2543
|
|
|
2525
|
-
|
|
2526
|
-
if (
|
|
2527
|
-
|
|
2528
|
-
|
|
2544
|
+
// Resolve grinding channel: DM mode opens DM with Dank Memer, otherwise use configured channel
|
|
2545
|
+
if (isDMMode) {
|
|
2546
|
+
try {
|
|
2547
|
+
const dankUser = await this.client.users.fetch(DANK_MEMER_ID);
|
|
2548
|
+
this.channel = await dankUser.createDM();
|
|
2549
|
+
this._dmChannelId = this.channel.id;
|
|
2550
|
+
this.log('info', `DM opened with Dank Memer`);
|
|
2551
|
+
} catch (err) {
|
|
2552
|
+
this.log('error', `Failed to open DM: ${err?.message || err}`);
|
|
2553
|
+
done(); return;
|
|
2554
|
+
}
|
|
2555
|
+
} else {
|
|
2556
|
+
this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
|
|
2557
|
+
if (!this.channel) {
|
|
2558
|
+
this.log('error', `Channel not found`);
|
|
2559
|
+
done(); return;
|
|
2560
|
+
}
|
|
2529
2561
|
}
|
|
2530
2562
|
|
|
2531
2563
|
const enabledCmds = [
|
|
@@ -2574,7 +2606,7 @@ class AccountWorker {
|
|
|
2574
2606
|
|
|
2575
2607
|
// Attach auto-recovery event listeners before login
|
|
2576
2608
|
this._attachRecoveryListeners();
|
|
2577
|
-
this.client.login(
|
|
2609
|
+
this.client.login(loginToken).catch((err) => {
|
|
2578
2610
|
clearTimeout(timeoutId);
|
|
2579
2611
|
const msg = (err?.message || '').toLowerCase();
|
|
2580
2612
|
if (msg.includes('token_invalid') || msg.includes('invalid token') || msg.includes('401') || msg.includes('incorrect login')) {
|
|
@@ -2638,11 +2670,17 @@ class AccountWorker {
|
|
|
2638
2670
|
this.client.destroy();
|
|
2639
2671
|
this.client = createLeanClient();
|
|
2640
2672
|
this._attachRecoveryListeners();
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
this.
|
|
2673
|
+
const retryToken = this.account.access_token || this.account.discord_token;
|
|
2674
|
+
await this.client.login(retryToken);
|
|
2675
|
+
// DM mode: re-open DM with Dank Memer
|
|
2676
|
+
if (this.account.grind_mode === 'dm') {
|
|
2677
|
+
const dankUser = await this.client.users.fetch(DANK_MEMER_ID);
|
|
2678
|
+
this.channel = await dankUser.createDM();
|
|
2679
|
+
this._dmChannelId = this.channel.id;
|
|
2680
|
+
this.log('success', `Background login OK (DM)`);
|
|
2681
|
+
} else {
|
|
2682
|
+
this.channel = await this.client.channels.fetch(this.account.channel_id).catch(() => null);
|
|
2683
|
+
if (this.channel) this.log('success', `Background login OK`);
|
|
2646
2684
|
}
|
|
2647
2685
|
} catch (err) {
|
|
2648
2686
|
this.log('error', `Background login failed: ${err?.message || err}`);
|
|
@@ -3024,11 +3062,10 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3024
3062
|
const cpm = globalCmdRate.getRate().toFixed(1);
|
|
3025
3063
|
console.log(`${c.bold}Total:${c.reset} +⏣${finalCoins.toLocaleString()} in ${formatUptime()} | ${finalCmds}cmds | ~${cpm}cmd/m | ${memFinal}MB`);
|
|
3026
3064
|
|
|
3027
|
-
// Stop workers
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
])));
|
|
3065
|
+
// Stop workers immediately (don't wait) — instant shutdown
|
|
3066
|
+
for (const wk of workers) {
|
|
3067
|
+
try { wk.stop(); } catch {}
|
|
3068
|
+
}
|
|
3032
3069
|
workerMap.clear();
|
|
3033
3070
|
|
|
3034
3071
|
// Release cluster claims
|
|
@@ -3054,13 +3091,14 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3054
3091
|
|
|
3055
3092
|
if (redis) { redis.disconnect().catch(() => {}); }
|
|
3056
3093
|
console.log(`${c.green}Goodbye!${c.reset}\n`);
|
|
3057
|
-
// Force exit
|
|
3058
|
-
setTimeout(() => process.exit(0),
|
|
3094
|
+
// Force exit so Ctrl+C always terminates immediately
|
|
3095
|
+
setTimeout(() => process.exit(0), 2000);
|
|
3059
3096
|
process.exit(0);
|
|
3060
3097
|
}
|
|
3061
3098
|
|
|
3062
3099
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
3063
3100
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
3101
|
+
process.on('SIGHUP', () => gracefulShutdown('SIGHUP'));
|
|
3064
3102
|
}
|
|
3065
3103
|
|
|
3066
3104
|
// ══════════════════════════════════════════════════════════════
|
package/lib/ui.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI Live Dashboard — cursor-positioned in-place row updates.
|
|
3
|
-
* Box drawn once
|
|
3
|
+
* Box drawn once. Rows updated in-place. Events below box.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
let _startTime = Date.now();
|
|
@@ -9,13 +9,15 @@ let _version = '0.0.0';
|
|
|
9
9
|
let _live = false;
|
|
10
10
|
let _phase = 'init';
|
|
11
11
|
|
|
12
|
-
// Terminal dimensions
|
|
12
|
+
// Terminal dimensions
|
|
13
13
|
let _W = 100;
|
|
14
14
|
let _inner = 98;
|
|
15
15
|
let _maxAccounts = 4;
|
|
16
16
|
|
|
17
|
-
// Row map: which terminal row each account
|
|
18
|
-
let _accountRows = []; // _accountRows[accountIdx] =
|
|
17
|
+
// Row map: which terminal row each account starts at (1-indexed)
|
|
18
|
+
let _accountRows = []; // _accountRows[accountIdx] = row
|
|
19
|
+
let _totalsRow = 0;
|
|
20
|
+
let _bottomRow = 0;
|
|
19
21
|
|
|
20
22
|
// ── Spinner frames ────────────────────────────────────────────
|
|
21
23
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
@@ -50,20 +52,21 @@ const c = {
|
|
|
50
52
|
white: '\x1b[37m',
|
|
51
53
|
};
|
|
52
54
|
const DIM = c.dim;
|
|
55
|
+
const B = '\x1b[38;2;77;212;238m'; // box border color
|
|
53
56
|
|
|
57
|
+
function row(n) { process.stdout.write(`\x1b[${n};1H`); }
|
|
58
|
+
function clrLine() { process.stdout.write('\x1b[2K'); }
|
|
54
59
|
function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
|
|
55
60
|
function padR(s, n) { return trunc(s, n).padEnd(n); }
|
|
56
61
|
function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
|
|
57
62
|
function stripAnsi(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, ''); }
|
|
58
|
-
function
|
|
63
|
+
function ln() { process.stdout.write('\n'); }
|
|
59
64
|
|
|
60
65
|
function fmtUptime() {
|
|
61
66
|
const s = Math.floor((Date.now() - _startTime) / 1000);
|
|
62
67
|
if (s < 60) return `${s}s`;
|
|
63
68
|
const m = Math.floor(s / 60);
|
|
64
69
|
const h = Math.floor(m / 60);
|
|
65
|
-
const d = Math.floor(h / 24);
|
|
66
|
-
if (d > 0) return `${d}d${h % 24}h`;
|
|
67
70
|
if (h > 0) return `${h}h${m % 60}m`;
|
|
68
71
|
return `${m}m`;
|
|
69
72
|
}
|
|
@@ -114,8 +117,24 @@ function fmtLevel(w) {
|
|
|
114
117
|
return c.cyan + lv + c.reset;
|
|
115
118
|
}
|
|
116
119
|
|
|
117
|
-
// ──
|
|
118
|
-
function
|
|
120
|
+
// ── Layout ────────────────────────────────────────────────────
|
|
121
|
+
function layout() {
|
|
122
|
+
_W = Math.min(process.stdout.columns || 100, 120);
|
|
123
|
+
_inner = _W - 2;
|
|
124
|
+
const rows = process.stdout.rows || 40;
|
|
125
|
+
_maxAccounts = Math.min(_workers.length, Math.max(3, rows - 16));
|
|
126
|
+
}
|
|
127
|
+
|
|
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;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Build account row string ─────────────────────────────────
|
|
137
|
+
function accountRow(w, wi, col) {
|
|
119
138
|
const col2 = wc(wi);
|
|
120
139
|
const stCol = statusColor(w);
|
|
121
140
|
const stTxt = statusText(w);
|
|
@@ -125,46 +144,54 @@ function buildAccountRow(w, wi, col) {
|
|
|
125
144
|
const lv = fmtLevel(w);
|
|
126
145
|
const name = padR(trunc(w.username || '?', col.name), col.name);
|
|
127
146
|
const doing = padR(w.lastStatus || 'idle', col.cmd);
|
|
147
|
+
const earnedStr = stripAnsi(earned);
|
|
148
|
+
const balStr = stripAnsi(bal);
|
|
149
|
+
|
|
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
153
|
|
|
129
154
|
return (
|
|
130
|
-
|
|
131
|
-
`\x1b[38;2;77;212;238m│\x1b[0m ` +
|
|
155
|
+
`${B}│\x1b[0m ` +
|
|
132
156
|
`${DIM}${padL(wi + 1, 2)}${c.reset} ` +
|
|
133
157
|
`${stCol}${padR(stTxt, col.st)}${c.reset} ` +
|
|
134
158
|
`${col2}${name}${c.reset} ` +
|
|
135
159
|
`${DIM}${doing}${c.reset} ` +
|
|
136
|
-
`${
|
|
160
|
+
`${balPadded} ` +
|
|
137
161
|
`${padL(ls, col.ls)} ` +
|
|
138
162
|
`${padL(lv, col.lv)} ` +
|
|
139
|
-
`${
|
|
140
|
-
|
|
163
|
+
`${earnedPadded} ` +
|
|
164
|
+
`${B}│\x1b[0m`
|
|
141
165
|
);
|
|
142
166
|
}
|
|
143
167
|
|
|
144
|
-
// ── Draw the FULL box (called once
|
|
168
|
+
// ── Draw the FULL box (called once) ─────────────────────────
|
|
145
169
|
function draw() {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
170
|
+
layout();
|
|
171
|
+
const col = getCol();
|
|
172
|
+
const bannerH = BANNER_LINES.length;
|
|
149
173
|
|
|
150
|
-
// Clear entire screen
|
|
174
|
+
// Clear entire screen + home
|
|
151
175
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
152
176
|
|
|
177
|
+
let r = 1;
|
|
178
|
+
|
|
153
179
|
// ── Top border ──
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
for (let i = 0; i <
|
|
160
|
-
|
|
161
|
-
const
|
|
180
|
+
row(r++); clrLine();
|
|
181
|
+
process.stdout.write(`${B}┌${'─'.repeat(_inner)}┐`);
|
|
182
|
+
ln();
|
|
183
|
+
|
|
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];
|
|
162
188
|
if (i < 2) {
|
|
163
189
|
const gradLine = gradientLine(line, 77, 212, 238, 255, 92, 147);
|
|
164
|
-
process.stdout.write(
|
|
190
|
+
process.stdout.write(`${B}│\x1b[0m ${gradLine}${' '.repeat(_inner - 2 - stripAnsi(gradLine) - 2)}${B}│`);
|
|
165
191
|
} else {
|
|
166
|
-
process.stdout.write(
|
|
192
|
+
process.stdout.write(`${B}│\x1b[0m ${DIM}${line}${c.reset}${' '.repeat(_inner - 2 - stripAnsi(line) - 2)}${B}│`);
|
|
167
193
|
}
|
|
194
|
+
ln();
|
|
168
195
|
}
|
|
169
196
|
|
|
170
197
|
// ── Status bar ──
|
|
@@ -179,34 +206,25 @@ function draw() {
|
|
|
179
206
|
`${DIM}Ctrl+C${c.reset}`,
|
|
180
207
|
].filter(Boolean);
|
|
181
208
|
const statusStr = statusParts.join(' ') + phaseLabel;
|
|
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`);
|
|
184
209
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
col.name += nameExtra;
|
|
210
|
+
row(r++); clrLine();
|
|
211
|
+
process.stdout.write(`${B}│\x1b[0m ${statusStr}${' '.repeat(_inner - 1 - stripAnsi(statusStr))}${B}│`);
|
|
212
|
+
ln();
|
|
189
213
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
process.stdout.write(`${
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
process.stdout.write(`${c.bold}${padL('LV', col.lv)}${c.reset}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 = [];
|
|
214
|
+
// ── Divider ──
|
|
215
|
+
row(r++); clrLine();
|
|
216
|
+
process.stdout.write(`${B}├${'─'.repeat(_inner)}┤`);
|
|
217
|
+
ln();
|
|
218
|
+
|
|
219
|
+
// ── Header ──
|
|
220
|
+
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}│`);
|
|
222
|
+
ln();
|
|
223
|
+
|
|
224
|
+
// ── HR ──
|
|
225
|
+
row(r++); clrLine();
|
|
226
|
+
process.stdout.write(`${B}│\x1b[0m ${'─'.repeat(_inner)} ${B}│`);
|
|
227
|
+
ln();
|
|
210
228
|
|
|
211
229
|
// ── Account rows ──
|
|
212
230
|
const sorted = [..._workers].sort((a, b) => {
|
|
@@ -218,14 +236,22 @@ function draw() {
|
|
|
218
236
|
});
|
|
219
237
|
const shown = sorted.slice(0, _maxAccounts);
|
|
220
238
|
|
|
239
|
+
_accountRows = [];
|
|
221
240
|
for (let si = 0; si < shown.length; si++) {
|
|
222
241
|
const w = shown[si];
|
|
223
242
|
const wi = _workers.indexOf(w);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
process.stdout.write(
|
|
243
|
+
_accountRows[wi] = r;
|
|
244
|
+
row(r++); clrLine();
|
|
245
|
+
process.stdout.write(accountRow(w, wi, col));
|
|
246
|
+
ln();
|
|
227
247
|
}
|
|
228
248
|
|
|
249
|
+
// ── Totals divider ──
|
|
250
|
+
_totalsRow = r;
|
|
251
|
+
row(r++); clrLine();
|
|
252
|
+
process.stdout.write(`${B}│\x1b[0m ${'─'.repeat(_inner)} ${B}│`);
|
|
253
|
+
ln();
|
|
254
|
+
|
|
229
255
|
// ── Totals ──
|
|
230
256
|
let totalCoins = 0, totalBal = 0;
|
|
231
257
|
for (const w of _workers) {
|
|
@@ -233,40 +259,35 @@ function draw() {
|
|
|
233
259
|
totalBal += w.stats.balance || 0;
|
|
234
260
|
}
|
|
235
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));
|
|
236
265
|
|
|
237
|
-
|
|
238
|
-
process.stdout.write(
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
process.stdout.write(`${' '.repeat(col.name)} `);
|
|
243
|
-
process.stdout.write(`${' '.repeat(col.cmd)} `);
|
|
244
|
-
process.stdout.write(`${padL(totalBal > 0 ? '⏣' + totalBal.toLocaleString() : '—', col.bal)} `);
|
|
245
|
-
process.stdout.write(`${' '.repeat(col.ls)} `);
|
|
246
|
-
process.stdout.write(`${' '.repeat(col.lv)} `);
|
|
247
|
-
process.stdout.write(`${fmtCoins(totalCoins)} `);
|
|
248
|
-
process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(_inner));
|
|
249
|
-
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
266
|
+
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
|
+
);
|
|
270
|
+
ln();
|
|
250
271
|
|
|
251
272
|
// ── Bottom ──
|
|
252
|
-
|
|
273
|
+
_bottomRow = r;
|
|
274
|
+
row(r++); clrLine();
|
|
275
|
+
process.stdout.write(`${B}└${'─'.repeat(_inner)}┘`);
|
|
276
|
+
ln();
|
|
253
277
|
}
|
|
254
278
|
|
|
255
|
-
// ── Update ONE account row in
|
|
279
|
+
// ── Update ONE account row in-place ─────────────────────────
|
|
256
280
|
function updateAccountRow(accountIdx) {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
281
|
+
if (!_live) return;
|
|
282
|
+
const rowNum = _accountRows[accountIdx];
|
|
283
|
+
if (!rowNum) return; // not visible
|
|
260
284
|
const w = _workers[accountIdx];
|
|
261
285
|
if (!w) return;
|
|
286
|
+
const col = getCol();
|
|
262
287
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
// Move cursor to this row, column 1
|
|
268
|
-
process.stdout.write(`\x1b[${row};1H`);
|
|
269
|
-
process.stdout.write(buildAccountRow(w, accountIdx, col));
|
|
288
|
+
row(rowNum); clrLine();
|
|
289
|
+
process.stdout.write(accountRow(w, accountIdx, col));
|
|
290
|
+
ln();
|
|
270
291
|
}
|
|
271
292
|
|
|
272
293
|
// ── Gradient line ─────────────────────────────────────────────
|
|
@@ -280,7 +301,7 @@ function gradientLine(text, r1, g1, b1, r2, g2, b2) {
|
|
|
280
301
|
}
|
|
281
302
|
|
|
282
303
|
// ── Event tracking ────────────────────────────────────────────
|
|
283
|
-
let _eventLines = [];
|
|
304
|
+
let _eventLines = [];
|
|
284
305
|
const MAX_EVENTS = 15;
|
|
285
306
|
|
|
286
307
|
// ── Public API ────────────────────────────────────────────────
|
|
@@ -307,7 +328,6 @@ function stop() {
|
|
|
307
328
|
function setLive(val) { _live = val; }
|
|
308
329
|
function setPhase(phase) { _phase = phase; }
|
|
309
330
|
|
|
310
|
-
// log: update account row in-place + append event below box
|
|
311
331
|
function log(accountIdx, msg) {
|
|
312
332
|
const now = new Date();
|
|
313
333
|
const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
|
|
@@ -316,13 +336,11 @@ function log(accountIdx, msg) {
|
|
|
316
336
|
if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
|
|
317
337
|
_eventLines[accountIdx].push({ text: msg, ts });
|
|
318
338
|
if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
|
|
319
|
-
// Update the row in place
|
|
320
339
|
if (_live) updateAccountRow(accountIdx);
|
|
321
340
|
}
|
|
322
341
|
|
|
323
342
|
if (!_live) return;
|
|
324
343
|
|
|
325
|
-
// Append event below the box (after the bottom border)
|
|
326
344
|
const col2 = accountIdx >= 0 ? wc(accountIdx) : c.cyan;
|
|
327
345
|
const name = accountIdx >= 0 ? trunc(_workers[accountIdx]?.username || '?', 14) : 'GLOBAL';
|
|
328
346
|
process.stdout.write(`${col2}${name}${c.reset} ${DIM}[${ts}]${c.reset} ${msg}\n`);
|
|
@@ -330,22 +348,17 @@ function log(accountIdx, msg) {
|
|
|
330
348
|
|
|
331
349
|
function logGlobal(msg) { log(-1, msg); }
|
|
332
350
|
|
|
333
|
-
//
|
|
334
|
-
function fullRedraw() { if (_live) draw(); }
|
|
335
|
-
|
|
336
|
-
// ── Periodic refresh: animate spinners & countdowns every 3s ──
|
|
351
|
+
// ── Refresh ──────────────────────────────────────────────────
|
|
337
352
|
let _refreshTimer = null;
|
|
338
353
|
function startRefresh() {
|
|
339
354
|
if (_refreshTimer) return;
|
|
340
355
|
_refreshTimer = setInterval(() => {
|
|
341
356
|
if (!_live) return;
|
|
342
|
-
for (let i = 0; i < _workers.length; i++)
|
|
343
|
-
updateAccountRow(i);
|
|
344
|
-
}
|
|
357
|
+
for (let i = 0; i < _workers.length; i++) updateAccountRow(i);
|
|
345
358
|
}, 1500);
|
|
346
359
|
}
|
|
347
360
|
function stopRefresh() {
|
|
348
361
|
if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
|
|
349
362
|
}
|
|
350
363
|
|
|
351
|
-
module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase, updateAccountRow,
|
|
364
|
+
module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop, setLive, setPhase, updateAccountRow, startRefresh, stopRefresh };
|