dankgrinder 8.38.0 → 8.40.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 +57 -26
- package/lib/ui.js +198 -98
- package/package.json +1 -1
package/lib/grinder.js
CHANGED
|
@@ -260,14 +260,32 @@ function colorBanner() {
|
|
|
260
260
|
return gradientLine(title, [77, 142, 255], [255, 92, 147]);
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
// ── Simple Logging
|
|
263
|
+
// ── Simple Logging (routed through UI) ────────────────────────
|
|
264
|
+
const _logQueue = []; // buffers logs before UI is ready
|
|
265
|
+
|
|
264
266
|
function log(type, msg, label) {
|
|
265
267
|
const icons = {
|
|
266
268
|
info: '.', success: '[OK]', error: '[X]', warn: '[!]',
|
|
267
269
|
cmd: '>', coin: '$', buy: '#', bal: '*', debug: '.',
|
|
268
270
|
};
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
+
const icon = icons[type] || icons.info;
|
|
272
|
+
const text = label ? `${icon} ${label} ${msg}` : `${icon} ${msg}`;
|
|
273
|
+
|
|
274
|
+
// Drain queue: UI is ready (called after ui.init)
|
|
275
|
+
while (_logQueue.length > 0) {
|
|
276
|
+
const item = _logQueue.shift();
|
|
277
|
+
if (typeof ui !== 'undefined') {
|
|
278
|
+
try { ui.log(-1, item); } catch { process.stdout.write(item + '\n'); }
|
|
279
|
+
} else {
|
|
280
|
+
process.stdout.write(item + '\n');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (typeof ui !== 'undefined') {
|
|
285
|
+
try { ui.log(-1, text); } catch { process.stdout.write(text + '\n'); }
|
|
286
|
+
} else {
|
|
287
|
+
_logQueue.push(text);
|
|
288
|
+
}
|
|
271
289
|
}
|
|
272
290
|
|
|
273
291
|
async function fetchConfig(retries = 3, delayMs = 1500, opts = {}) {
|
|
@@ -694,7 +712,15 @@ class AccountWorker {
|
|
|
694
712
|
log(type, msg) {
|
|
695
713
|
const stripped = msg.replace(/\x1b\[[0-9;]*m/g, '');
|
|
696
714
|
if (type !== 'debug') this.lastStatus = stripped.substring(0, 28);
|
|
697
|
-
|
|
715
|
+
// Route through UI
|
|
716
|
+
const icons = { info: '.', success: '[OK]', error: '[X]', warn: '[!]', cmd: '>', coin: '$', buy: '#', bal: '*', debug: '.' };
|
|
717
|
+
const icon = icons[type] || icons.info;
|
|
718
|
+
const text = `${icon} ${this.username} ${msg}`;
|
|
719
|
+
if (typeof ui !== 'undefined') {
|
|
720
|
+
try { ui.log(this.idx, text); } catch { process.stdout.write(text + '\n'); }
|
|
721
|
+
} else {
|
|
722
|
+
process.stdout.write(text + '\n');
|
|
723
|
+
}
|
|
698
724
|
}
|
|
699
725
|
|
|
700
726
|
setStatus(text) {
|
|
@@ -2448,7 +2474,12 @@ class AccountWorker {
|
|
|
2448
2474
|
}
|
|
2449
2475
|
|
|
2450
2476
|
async start() {
|
|
2451
|
-
|
|
2477
|
+
// Worker start — route through UI
|
|
2478
|
+
if (typeof ui !== 'undefined') {
|
|
2479
|
+
try { ui.log(this.idx, `starting...`); } catch { process.stdout.write(`[WORKER START] ${this.account.label || this.account.id}\n`); }
|
|
2480
|
+
} else {
|
|
2481
|
+
process.stdout.write(`[WORKER START] ${this.account.label || this.account.id}\n`);
|
|
2482
|
+
}
|
|
2452
2483
|
if (!this.account.discord_token) { this.log('error', 'No token'); return; }
|
|
2453
2484
|
if (!this.account.channel_id) { this.log('error', 'No channel'); return; }
|
|
2454
2485
|
|
|
@@ -2755,7 +2786,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2755
2786
|
w.setCooldown?.('crime', 86400);
|
|
2756
2787
|
w.setCooldown?.('search', 86400);
|
|
2757
2788
|
sendWebhook?.('DEATH ALERT (DM)', `**${w.username}** died in DMs! **0 lifesavers!**\nCrime/search auto-disabled.`, 0xef4444);
|
|
2758
|
-
ui.log(`${c.red}E${c.reset} DEATH —
|
|
2789
|
+
ui.log(workers.indexOf(w), `${c.red}E${c.reset} DEATH — 0 lifesavers! Crime/search disabled`);
|
|
2759
2790
|
} else {
|
|
2760
2791
|
w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
|
|
2761
2792
|
if (prev !== event.lifesaversLeft) {
|
|
@@ -2765,7 +2796,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2765
2796
|
if (event.lifesaversLeft <= 2) {
|
|
2766
2797
|
w._alert = { type: 'lowls' };
|
|
2767
2798
|
sendWebhook?.('LOW LIFESAVERS', `**${w.username}** has only **${event.lifesaversLeft}** lifesaver(s) left!`, 0xfbbf24);
|
|
2768
|
-
ui.log(
|
|
2799
|
+
ui.log(workers.indexOf(w), `⚠ ${event.lifesaversLeft} lifesavers left!`);
|
|
2769
2800
|
}
|
|
2770
2801
|
}
|
|
2771
2802
|
}
|
|
@@ -2774,7 +2805,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2774
2805
|
if (event.type === 'levelup') {
|
|
2775
2806
|
if (event.to > 0) {
|
|
2776
2807
|
w._level = event.to;
|
|
2777
|
-
ui.log(
|
|
2808
|
+
ui.log(workers.indexOf(w), `↑ level ${event.to}`);
|
|
2778
2809
|
}
|
|
2779
2810
|
}
|
|
2780
2811
|
}
|
|
@@ -2788,7 +2819,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2788
2819
|
const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
|
|
2789
2820
|
const randomLoginGap = () => LOGIN_GAP_MAX_MS <= LOGIN_GAP_MIN_MS ? LOGIN_GAP_MIN_MS : LOGIN_GAP_MIN_MS + Math.floor(Math.random() * (LOGIN_GAP_MAX_MS - LOGIN_GAP_MIN_MS + 1));
|
|
2790
2821
|
|
|
2791
|
-
ui.log(
|
|
2822
|
+
ui.log(-1, `Logging in ${accounts.length} accounts...`);
|
|
2792
2823
|
const BATCH_SIZE = 10;
|
|
2793
2824
|
for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
|
|
2794
2825
|
if (shutdownCalled) break;
|
|
@@ -2803,20 +2834,20 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2803
2834
|
await worker.start();
|
|
2804
2835
|
if (worker._tokenInvalid) {
|
|
2805
2836
|
worker.lastStatus = 'invalid token';
|
|
2806
|
-
ui.log(
|
|
2837
|
+
ui.log(worker.idx, `✗ invalid token`);
|
|
2807
2838
|
} else if (worker.channel) {
|
|
2808
2839
|
worker.lastStatus = 'ready';
|
|
2809
|
-
ui.log(
|
|
2840
|
+
ui.log(worker.idx, `✓ connected`);
|
|
2810
2841
|
} else {
|
|
2811
2842
|
worker.lastStatus = 'timeout';
|
|
2812
|
-
ui.log(
|
|
2843
|
+
ui.log(worker.idx, `✗ timeout`);
|
|
2813
2844
|
}
|
|
2814
2845
|
} catch (e) {
|
|
2815
|
-
ui.log(
|
|
2846
|
+
ui.log(i + idx, `ERROR: ${e.message}`);
|
|
2816
2847
|
}
|
|
2817
2848
|
}));
|
|
2818
2849
|
} catch (e) {
|
|
2819
|
-
ui.log(
|
|
2850
|
+
ui.log(-1, `BATCH ERROR: ${e.message}`);
|
|
2820
2851
|
}
|
|
2821
2852
|
if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
|
|
2822
2853
|
hintGC();
|
|
@@ -2825,32 +2856,32 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2825
2856
|
const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
|
|
2826
2857
|
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
2827
2858
|
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
2828
|
-
ui.log(
|
|
2859
|
+
ui.log(-1, `Login: ${loginDone}/${accounts.length} connected`);
|
|
2829
2860
|
if (invalidWorkers.length > 0) {
|
|
2830
|
-
for (const w of invalidWorkers) ui.log(
|
|
2861
|
+
for (const w of invalidWorkers) ui.log(workers.indexOf(w), `✗ invalid token`);
|
|
2831
2862
|
}
|
|
2832
2863
|
if (timedOutWorkers.length > 0) {
|
|
2833
|
-
ui.log(`${
|
|
2864
|
+
ui.log(-1, `${timedOutWorkers.length} timed out (retrying in background)`);
|
|
2834
2865
|
}
|
|
2835
2866
|
ui.draw();
|
|
2836
2867
|
|
|
2837
2868
|
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
2838
2869
|
|
|
2839
2870
|
// ── Phase 2: Inventory check ────────────────────────────────────
|
|
2840
|
-
ui.log(
|
|
2871
|
+
ui.log(-1, `Checking inventory...`);
|
|
2841
2872
|
let invFailed = 0;
|
|
2842
2873
|
await Promise.all(activeWorkers.map(async (w) => {
|
|
2843
2874
|
try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
|
|
2844
2875
|
catch { invFailed++; }
|
|
2845
2876
|
}));
|
|
2846
2877
|
if (invFailed > 0) {
|
|
2847
|
-
ui.log(
|
|
2878
|
+
ui.log(-1, `Inventory failed for ${invFailed} accounts`);
|
|
2848
2879
|
} else {
|
|
2849
|
-
ui.log(
|
|
2880
|
+
ui.log(-1, `Inventory OK`);
|
|
2850
2881
|
}
|
|
2851
2882
|
|
|
2852
2883
|
// ── Phase 2.5: Balance check ───────────────────────────────────
|
|
2853
|
-
ui.log(
|
|
2884
|
+
ui.log(-1, `Checking balances...`);
|
|
2854
2885
|
for (const w of activeWorkers) {
|
|
2855
2886
|
try { await w.checkBalance(true); } catch {}
|
|
2856
2887
|
}
|
|
@@ -2859,10 +2890,10 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2859
2890
|
totalCoins += w.stats?.balance || 0;
|
|
2860
2891
|
totalCoins += w.stats?.bankBalance || 0;
|
|
2861
2892
|
}
|
|
2862
|
-
ui.log(
|
|
2893
|
+
ui.log(-1, `Balance: +⏣${totalCoins.toLocaleString()} across ${activeWorkers.length} accounts`);
|
|
2863
2894
|
|
|
2864
2895
|
// ── Phase 2.75: DM history check ────────────────────────────────
|
|
2865
|
-
ui.log(
|
|
2896
|
+
ui.log(-1, `Checking DM history...`);
|
|
2866
2897
|
let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
|
|
2867
2898
|
for (const w of activeWorkers) {
|
|
2868
2899
|
try {
|
|
@@ -2880,14 +2911,14 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2880
2911
|
}
|
|
2881
2912
|
} catch {}
|
|
2882
2913
|
}
|
|
2883
|
-
if (dmNoLs.length > 0) ui.log(
|
|
2914
|
+
if (dmNoLs.length > 0) ui.log(-1, `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
|
|
2884
2915
|
const parts = [];
|
|
2885
2916
|
if (dmDeaths > 0) parts.push(`${dmDeaths} deaths`);
|
|
2886
2917
|
if (dmLevelUps > 0) parts.push(`${dmLevelUps} level-ups`);
|
|
2887
|
-
ui.log(
|
|
2918
|
+
ui.log(-1, `DM: ${parts.length > 0 ? parts.join(', ') : 'clean'}`);
|
|
2888
2919
|
|
|
2889
2920
|
// ── Phase 3: Start grind loops ───────────────────────────────────
|
|
2890
|
-
ui.log(
|
|
2921
|
+
ui.log(-1, `Starting ${activeWorkers.length} grind loops...`);
|
|
2891
2922
|
for (const w of activeWorkers) {
|
|
2892
2923
|
if (!shutdownCalled) w.grindLoop();
|
|
2893
2924
|
}
|
package/lib/ui.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI Live Dashboard —
|
|
3
|
-
*
|
|
2
|
+
* CLI Live Dashboard — box design, loading animations, per-account status.
|
|
3
|
+
* Everything inside a single box. Events stream below. No duplicate output.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
let _startTime = Date.now();
|
|
@@ -8,8 +8,12 @@ let _workers = [];
|
|
|
8
8
|
let _isShuttingDown = () => false;
|
|
9
9
|
let _version = '0.0.0';
|
|
10
10
|
|
|
11
|
+
// ── Spinner frames ────────────────────────────────────────────
|
|
12
|
+
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
13
|
+
function spinnerFrame() { return SPINNER[Math.floor(Date.now() / 150) % SPINNER.length]; }
|
|
14
|
+
|
|
11
15
|
// ── Big ASCII art banner ──────────────────────────────────────
|
|
12
|
-
const
|
|
16
|
+
const BANNER_LINES = [
|
|
13
17
|
' ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗',
|
|
14
18
|
' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║',
|
|
15
19
|
' ██║ ██║██║ ██║██╔██╗ ██║██║ ███╗█████╗ ██║ ██║██╔██╗ ██║',
|
|
@@ -24,33 +28,21 @@ const BANNER = [
|
|
|
24
28
|
' ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝',
|
|
25
29
|
];
|
|
26
30
|
|
|
27
|
-
// Gradient each banner line cyan→pink
|
|
28
31
|
function gradientLine(text, r1, g1, b1, r2, g2, b2) {
|
|
29
32
|
let out = '';
|
|
30
33
|
for (let i = 0; i < text.length; i++) {
|
|
31
34
|
const t = text.length <= 1 ? 0 : i / (text.length - 1);
|
|
32
|
-
|
|
33
|
-
const lg = Math.round(g1 + (g2 - g1) * t);
|
|
34
|
-
const lb = Math.round(b1 + (b2 - b1) * t);
|
|
35
|
-
out += `\x1b[38;2;${lr};${lg};${lb}m${text[i]}`;
|
|
35
|
+
out += `\x1b[38;2;${Math.round(r1 + (r2 - r1) * t)};${Math.round(g1 + (g2 - g1) * t)};${Math.round(b1 + (b2 - b1) * t)}m${text[i]}`;
|
|
36
36
|
}
|
|
37
37
|
return out + '\x1b[0m';
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// ──
|
|
40
|
+
// ── Palette ───────────────────────────────────────────────────
|
|
41
41
|
const PALETTE = [
|
|
42
|
-
'\x1b[38;2;77;212;238m',
|
|
43
|
-
'\x1b[38;2;255;
|
|
44
|
-
'\x1b[38;2;130;
|
|
45
|
-
'\x1b[38;2;255;
|
|
46
|
-
'\x1b[38;2;120;180;255m', // sky
|
|
47
|
-
'\x1b[38;2;255;180;80m', // orange
|
|
48
|
-
'\x1b[38;2;180;130;255m', // lavender
|
|
49
|
-
'\x1b[38;2;100;255;180m', // teal
|
|
50
|
-
'\x1b[38;2;255;150;100m', // peach
|
|
51
|
-
'\x1b[38;2;150;255;200m', // mint
|
|
52
|
-
'\x1b[38;2;255;255;120m', // yellow
|
|
53
|
-
'\x1b[38;2;200;150;255m', // violet
|
|
42
|
+
'\x1b[38;2;77;212;238m', '\x1b[38;2;255;194;77m', '\x1b[38;2;130;210;100m',
|
|
43
|
+
'\x1b[38;2;255;120;200m', '\x1b[38;2;120;180;255m', '\x1b[38;2;255;180;80m',
|
|
44
|
+
'\x1b[38;2;180;130;255m', '\x1b[38;2;100;255;180m', '\x1b[38;2;255;150;100m',
|
|
45
|
+
'\x1b[38;2;150;255;200m', '\x1b[38;2;255;255;120m', '\x1b[38;2;200;150;255m',
|
|
54
46
|
];
|
|
55
47
|
function wc(idx) { return PALETTE[idx % PALETTE.length]; }
|
|
56
48
|
|
|
@@ -67,7 +59,12 @@ const DIM = c.dim;
|
|
|
67
59
|
|
|
68
60
|
function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
|
|
69
61
|
function padR(s, n) { return trunc(s, n).padEnd(n); }
|
|
70
|
-
function padL(s, n) { return String(s).padStart(n); }
|
|
62
|
+
function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
|
|
63
|
+
function padC(s, n) {
|
|
64
|
+
s = String(s || '');
|
|
65
|
+
const pad = Math.max(0, n - s.length);
|
|
66
|
+
return ' '.repeat(Math.floor(pad / 2)) + s + ' '.repeat(Math.ceil(pad / 2));
|
|
67
|
+
}
|
|
71
68
|
|
|
72
69
|
function fmtUptime() {
|
|
73
70
|
const s = Math.floor((Date.now() - _startTime) / 1000);
|
|
@@ -80,84 +77,136 @@ function fmtUptime() {
|
|
|
80
77
|
return `${m}m`;
|
|
81
78
|
}
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
if (w.
|
|
86
|
-
if (w.
|
|
87
|
-
|
|
88
|
-
|
|
80
|
+
// ── Status helpers ─────────────────────────────────────────────
|
|
81
|
+
function statusColor(w) {
|
|
82
|
+
if (!w.running || !w.channel) return c.red;
|
|
83
|
+
if (w.paused || w.dashboardPaused) return c.yellow;
|
|
84
|
+
if (w.busy || w._invRunning || w._sellRunning) return c.yellow;
|
|
85
|
+
if (Date.now() < w.globalCooldownUntil) return c.blue;
|
|
86
|
+
return c.green;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function statusText(w) {
|
|
90
|
+
if (!w.running || !w.channel) return 'ERROR';
|
|
91
|
+
if (w.paused) return 'PAUSED';
|
|
92
|
+
if (w.dashboardPaused) return 'HELD';
|
|
93
|
+
if (w._alert?.type === 'death') return 'DEAD';
|
|
94
|
+
if (w._alert?.type === 'lowls') return 'LOW LS';
|
|
95
|
+
if (w.busy || w._invRunning || w._sellRunning) return spinnerFrame() + ' BUSY';
|
|
96
|
+
if (Date.now() < w.globalCooldownUntil) {
|
|
97
|
+
const wait = Math.ceil((w.globalCooldownUntil - Date.now()) / 1000);
|
|
98
|
+
return '⏳' + wait + 's';
|
|
89
99
|
}
|
|
90
|
-
|
|
91
|
-
return `${c.green}·${c.reset}`;
|
|
100
|
+
return '● READY';
|
|
92
101
|
}
|
|
93
102
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
// ── Layout ────────────────────────────────────────────────────
|
|
104
|
+
function layout() {
|
|
105
|
+
const W = Math.min(process.stdout.columns || 120, 120);
|
|
106
|
+
const rows = process.stdout.rows || 40;
|
|
107
|
+
const bannerH = BANNER_LINES.length + 1; // +1 for blank line
|
|
108
|
+
const statusH = 1;
|
|
109
|
+
const headerH = 3; // blank + headers + hr
|
|
110
|
+
const totalsH = 2; // totals + hr
|
|
111
|
+
const footerH = 1; // blank
|
|
112
|
+
const maxAccounts = Math.min(_workers.length, Math.max(3, rows - bannerH - statusH - headerH - totalsH - footerH - 8));
|
|
113
|
+
const eventH = Math.max(3, rows - bannerH - statusH - headerH - totalsH - footerH - maxAccounts);
|
|
114
|
+
return { W, bannerH, statusH, headerH, totalsH, footerH, maxAccounts, eventH };
|
|
102
115
|
}
|
|
103
116
|
|
|
104
|
-
// ── Draw full
|
|
117
|
+
// ── Draw the full box ────────────────────────────────────────
|
|
105
118
|
function draw() {
|
|
106
|
-
const W
|
|
107
|
-
const
|
|
119
|
+
const { W, bannerH, statusH, headerH, totalsH, footerH, maxAccounts, eventH } = layout();
|
|
120
|
+
const T = '─'.repeat(W - 2); // inner width
|
|
121
|
+
|
|
122
|
+
// ── Top of box ──
|
|
123
|
+
// Move cursor to top-left of box area and overwrite in place (no full clear)
|
|
124
|
+
process.stdout.write(`\x1b[${1};1H`);
|
|
125
|
+
process.stdout.write(`\x1b[0J`); // clear from cursor to end of screen
|
|
126
|
+
process.stdout.write(`\x1b[38;2;77;212;238m┌─${T}─┐\x1b[0m\n`);
|
|
127
|
+
|
|
128
|
+
// ── Banner inside box ──
|
|
129
|
+
for (const line of BANNER_LINES) {
|
|
130
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${gradientLine(line, 77, 212, 238, 255, 92, 147)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
131
|
+
}
|
|
132
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
133
|
+
|
|
134
|
+
// ── Status bar inside box ──
|
|
108
135
|
const running = _workers.filter(w => w.running && w.channel && !w.paused && !w.dashboardPaused).length;
|
|
109
136
|
const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
|
|
137
|
+
const errors = _workers.filter(w => !w.running || !w.channel).length;
|
|
138
|
+
const statusLine = [
|
|
139
|
+
`${c.bold}v${_version}${c.reset}`,
|
|
140
|
+
`${c.green}●${c.reset} online`,
|
|
141
|
+
`${fmtUptime()}`,
|
|
142
|
+
`${c.green}·${c.reset}${running}`,
|
|
143
|
+
`${c.yellow}~${c.reset}${paused}`,
|
|
144
|
+
errors > 0 ? `${c.red}E${c.reset}${errors}` : null,
|
|
145
|
+
`${DIM}Ctrl+C${c.reset}`,
|
|
146
|
+
].filter(Boolean).join(' ');
|
|
147
|
+
const statusPad = W - 4 - statusLine.length;
|
|
148
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${statusLine}${' '.repeat(Math.max(0, statusPad))} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
149
|
+
process.stdout.write(`\x1b[38;2;77;212;238m├─${T}─┤\x1b[0m\n`);
|
|
110
150
|
|
|
111
|
-
|
|
112
|
-
|
|
151
|
+
// ── Table header ──
|
|
152
|
+
const col = {
|
|
153
|
+
st: 8, // Status
|
|
154
|
+
name: 18, // Account name
|
|
155
|
+
last: 20, // Last command
|
|
156
|
+
cmds: 6, // Commands
|
|
157
|
+
ok: 4, // OK%
|
|
158
|
+
earned: 10, // Earned
|
|
159
|
+
};
|
|
160
|
+
const nameExtra = Math.max(0, W - 4 - col.st - col.name - col.last - col.cmds - col.ok - col.earned - 14);
|
|
161
|
+
col.name += nameExtra;
|
|
113
162
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
163
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
|
|
164
|
+
process.stdout.write(`${c.bold}#${c.reset} `);
|
|
165
|
+
process.stdout.write(`${c.bold}${padR('STATUS', col.st)}${c.reset} `);
|
|
166
|
+
process.stdout.write(`${c.bold}${padR('ACCOUNT', col.name)}${c.reset} `);
|
|
167
|
+
process.stdout.write(`${c.bold}${padR('LAST COMMAND', col.last)}${c.reset} `);
|
|
168
|
+
process.stdout.write(`${c.bold}${padL('CMDS', col.cmds)}${c.reset} `);
|
|
169
|
+
process.stdout.write(`${c.bold}${padL('OK%', col.ok)}${c.reset} `);
|
|
170
|
+
process.stdout.write(`${c.bold}${padL('EARNED', col.earned)}${c.reset} `);
|
|
171
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
172
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
173
|
+
|
|
174
|
+
// ── Account rows ──
|
|
175
|
+
const sorted = [..._workers].sort((a, b) => {
|
|
176
|
+
if (!a.channel !== !b.channel) return !a.channel ? 1 : -1;
|
|
177
|
+
const aA = a.running && a.channel && !a.paused && !a.dashboardPaused;
|
|
178
|
+
const bA = b.running && b.channel && !b.paused && !b.dashboardPaused;
|
|
179
|
+
if (aA !== bA) return bA ? 1 : -1;
|
|
180
|
+
return (b.stats.commands || 0) - (a.stats.commands || 0);
|
|
181
|
+
});
|
|
182
|
+
const shown = sorted.slice(0, maxAccounts);
|
|
118
183
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
` ${c.bold}v${_version}${c.reset} ` +
|
|
122
|
-
`${c.green}●${c.reset}online ` +
|
|
123
|
-
`${fmtUptime()} ` +
|
|
124
|
-
`${c.green}·${c.reset}${running} ` +
|
|
125
|
-
`${c.yellow}~${c.reset}${paused} ` +
|
|
126
|
-
`${DIM}Ctrl+C${c.reset}`
|
|
127
|
-
);
|
|
128
|
-
console.log(` ${hr}`);
|
|
129
|
-
console.log(
|
|
130
|
-
` ${c.bold}#${c.reset} ${c.bold}${padR('St', 2)}${c.reset} ` +
|
|
131
|
-
`${c.bold}${padR('Account', 18)}${c.reset} ` +
|
|
132
|
-
`${c.bold}${padR('Last Command', 20)}${c.reset} ` +
|
|
133
|
-
`${c.bold}${padL('Cmds', 5)}${c.reset} ` +
|
|
134
|
-
`${c.bold}${padL('OK%', 3)}${c.reset} ` +
|
|
135
|
-
`${c.bold}${padL('Earned', 8)}${c.reset}`
|
|
136
|
-
);
|
|
137
|
-
console.log(` ${hr}`);
|
|
138
|
-
|
|
139
|
-
const sorted = sortedWorkers();
|
|
140
|
-
for (let si = 0; si < sorted.length; si++) {
|
|
141
|
-
const w = sorted[si];
|
|
184
|
+
for (let si = 0; si < shown.length; si++) {
|
|
185
|
+
const w = shown[si];
|
|
142
186
|
const wi = _workers.indexOf(w);
|
|
143
|
-
const
|
|
187
|
+
const col2 = wc(wi);
|
|
188
|
+
const stCol = statusColor(w);
|
|
189
|
+
const stText = statusText(w);
|
|
144
190
|
const earned = w.stats.coins > 0 ? `${c.green}+${w.stats.coins.toLocaleString()}${c.reset}` : DIM + '—' + c.reset;
|
|
145
191
|
const cmds = w.stats.commands || 0;
|
|
146
192
|
const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
|
|
147
193
|
|
|
148
|
-
process.stdout.write(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
);
|
|
194
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
|
|
195
|
+
process.stdout.write(`${DIM}${padL(wi + 1, 2)}${c.reset} `);
|
|
196
|
+
process.stdout.write(`${stCol}${padR(stText, col.st)}${c.reset} `);
|
|
197
|
+
process.stdout.write(`${col2}${padR(w.username || '?', col.name)}${c.reset} `);
|
|
198
|
+
process.stdout.write(`${DIM}${padR(w.lastStatus || 'idle', col.last)}${c.reset} `);
|
|
199
|
+
process.stdout.write(`${padL(cmds, col.cmds)} `);
|
|
200
|
+
process.stdout.write(`${padL(rate, col.ok)}% `);
|
|
201
|
+
process.stdout.write(`${padL(earned, col.earned)} `);
|
|
202
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
157
203
|
}
|
|
158
204
|
|
|
159
|
-
|
|
205
|
+
if (sorted.length > maxAccounts) {
|
|
206
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${DIM}+${sorted.length - maxAccounts} more accounts${' '.repeat(Math.max(0, W - 24 - String(sorted.length - maxAccounts).length))}${c.reset} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
207
|
+
}
|
|
160
208
|
|
|
209
|
+
// ── Totals ──
|
|
161
210
|
let totalCoins = 0, totalCmds = 0, totalOk = 0;
|
|
162
211
|
for (const w of _workers) {
|
|
163
212
|
totalCoins += w.stats.coins || 0;
|
|
@@ -167,35 +216,86 @@ function draw() {
|
|
|
167
216
|
const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
|
|
168
217
|
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
169
218
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
219
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
220
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
|
|
221
|
+
process.stdout.write(`${c.bold}Σ${c.reset} `);
|
|
222
|
+
process.stdout.write(`${DIM}${padL(_workers.length, 2)} acc${c.reset} `);
|
|
223
|
+
process.stdout.write(`${' '.repeat(col.name)} `);
|
|
224
|
+
process.stdout.write(`${DIM}${' '.repeat(col.last)}${c.reset} `);
|
|
225
|
+
process.stdout.write(`${padL(totalCmds, col.cmds)} `);
|
|
226
|
+
process.stdout.write(`${padL(rate, col.ok)}% `);
|
|
227
|
+
process.stdout.write(`${totalCoins > 0 ? c.green + padL('+' + totalCoins.toLocaleString(), col.earned) + c.reset : DIM + padL('—', col.earned) + c.reset} `);
|
|
228
|
+
process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(W - 4));
|
|
229
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
181
230
|
|
|
182
|
-
// ──
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
231
|
+
// ── Events section ──
|
|
232
|
+
process.stdout.write(`\x1b[38;2;77;212;238m├─${T}─┤\x1b[0m\n`);
|
|
233
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${c.bold}EVENTS${c.reset}${' '.repeat(Math.max(0, W - 10))}\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
234
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
235
|
+
|
|
236
|
+
// Show recent events per account
|
|
237
|
+
const evLines = [];
|
|
238
|
+
for (let i = 0; i < _workers.length; i++) {
|
|
239
|
+
if (_eventLines[i] && _eventLines[i].length > 0) {
|
|
240
|
+
const latest = _eventLines[i][_eventLines[i].length - 1];
|
|
241
|
+
const col2 = wc(i);
|
|
242
|
+
const name = trunc(_workers[i]?.username || '?', 14);
|
|
243
|
+
evLines.push({ i, text: latest.text, ts: latest.ts, col: col2, name });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (let i = 0; i < Math.min(eventH, evLines.length); i++) {
|
|
248
|
+
const ev = evLines[evLines.length - 1 - i];
|
|
249
|
+
const now = new Date();
|
|
250
|
+
const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
|
|
251
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
|
|
252
|
+
process.stdout.write(`${ev.col}${padR(ev.name, 14)}${c.reset} `);
|
|
253
|
+
process.stdout.write(`${DIM}[${ts}]${c.reset} ${ev.text}${' '.repeat(Math.max(0, W - 20 - ev.name.length - ev.text.length))}`);
|
|
254
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < Math.max(0, eventH - evLines.length); i++) {
|
|
258
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${' '.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ── Bottom of box ──
|
|
262
|
+
process.stdout.write(`\x1b[38;2;77;212;238m└─${T}─┘\x1b[0m\n`);
|
|
187
263
|
}
|
|
188
264
|
|
|
265
|
+
// ── Event tracking ────────────────────────────────────────────
|
|
266
|
+
let _eventLines = []; // [accountIdx] = [{text, ts}]
|
|
267
|
+
const MAX_EVENTS = 3;
|
|
268
|
+
|
|
189
269
|
// ── Public API ────────────────────────────────────────────────
|
|
270
|
+
|
|
190
271
|
function init({ workers, isShuttingDown }) {
|
|
191
272
|
_startTime = Date.now();
|
|
192
273
|
_workers = workers;
|
|
193
274
|
_isShuttingDown = isShuttingDown || (() => false);
|
|
194
275
|
_version = '0.0.0';
|
|
276
|
+
_eventLines = [];
|
|
195
277
|
}
|
|
196
278
|
|
|
197
279
|
function drawBanner(version) { _version = version; }
|
|
198
280
|
function start() {}
|
|
199
281
|
function stop() { process.stdout.write(c.reset + '\n'); }
|
|
200
282
|
|
|
201
|
-
|
|
283
|
+
function log(accountIdx, msg) {
|
|
284
|
+
const now = new Date();
|
|
285
|
+
const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
|
|
286
|
+
|
|
287
|
+
if (accountIdx >= 0) {
|
|
288
|
+
if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
|
|
289
|
+
_eventLines[accountIdx].push({ text: msg, ts });
|
|
290
|
+
if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
draw();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function logGlobal(msg) {
|
|
297
|
+
// Global events go to account 0
|
|
298
|
+
log(-1, msg);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop };
|