dankgrinder 8.38.0 → 8.39.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 +195 -97
- 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
|
|
|
@@ -68,6 +60,11 @@ const DIM = c.dim;
|
|
|
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
62
|
function padL(s, n) { return String(s).padStart(n); }
|
|
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,134 @@ 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
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
124
|
+
process.stdout.write(`\x1b[38;2;77;212;238m┌─${T}─┐\x1b[0m\n`);
|
|
125
|
+
|
|
126
|
+
// ── Banner inside box ──
|
|
127
|
+
for (const line of BANNER_LINES) {
|
|
128
|
+
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`);
|
|
129
|
+
}
|
|
130
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
131
|
+
|
|
132
|
+
// ── Status bar inside box ──
|
|
108
133
|
const running = _workers.filter(w => w.running && w.channel && !w.paused && !w.dashboardPaused).length;
|
|
109
134
|
const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
|
|
135
|
+
const errors = _workers.filter(w => !w.running || !w.channel).length;
|
|
136
|
+
const statusLine = [
|
|
137
|
+
`${c.bold}v${_version}${c.reset}`,
|
|
138
|
+
`${c.green}●${c.reset} online`,
|
|
139
|
+
`${fmtUptime()}`,
|
|
140
|
+
`${c.green}·${c.reset}${running}`,
|
|
141
|
+
`${c.yellow}~${c.reset}${paused}`,
|
|
142
|
+
errors > 0 ? `${c.red}E${c.reset}${errors}` : null,
|
|
143
|
+
`${DIM}Ctrl+C${c.reset}`,
|
|
144
|
+
].filter(Boolean).join(' ');
|
|
145
|
+
const statusPad = W - 4 - statusLine.length;
|
|
146
|
+
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`);
|
|
147
|
+
process.stdout.write(`\x1b[38;2;77;212;238m├─${T}─┤\x1b[0m\n`);
|
|
110
148
|
|
|
111
|
-
|
|
112
|
-
|
|
149
|
+
// ── Table header ──
|
|
150
|
+
const col = {
|
|
151
|
+
st: 8, // Status
|
|
152
|
+
name: 18, // Account name
|
|
153
|
+
last: 20, // Last command
|
|
154
|
+
cmds: 6, // Commands
|
|
155
|
+
ok: 4, // OK%
|
|
156
|
+
earned: 10, // Earned
|
|
157
|
+
};
|
|
158
|
+
const nameExtra = Math.max(0, W - 4 - col.st - col.name - col.last - col.cmds - col.ok - col.earned - 14);
|
|
159
|
+
col.name += nameExtra;
|
|
113
160
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
161
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
|
|
162
|
+
process.stdout.write(`${c.bold}#${c.reset} `);
|
|
163
|
+
process.stdout.write(`${c.bold}${padR('STATUS', col.st)}${c.reset} `);
|
|
164
|
+
process.stdout.write(`${c.bold}${padR('ACCOUNT', col.name)}${c.reset} `);
|
|
165
|
+
process.stdout.write(`${c.bold}${padR('LAST COMMAND', col.last)}${c.reset} `);
|
|
166
|
+
process.stdout.write(`${c.bold}${padL('CMDS', col.cmds)}${c.reset} `);
|
|
167
|
+
process.stdout.write(`${c.bold}${padL('OK%', col.ok)}${c.reset} `);
|
|
168
|
+
process.stdout.write(`${c.bold}${padL('EARNED', col.earned)}${c.reset} `);
|
|
169
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
170
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
171
|
+
|
|
172
|
+
// ── Account rows ──
|
|
173
|
+
const sorted = [..._workers].sort((a, b) => {
|
|
174
|
+
if (!a.channel !== !b.channel) return !a.channel ? 1 : -1;
|
|
175
|
+
const aA = a.running && a.channel && !a.paused && !a.dashboardPaused;
|
|
176
|
+
const bA = b.running && b.channel && !b.paused && !b.dashboardPaused;
|
|
177
|
+
if (aA !== bA) return bA ? 1 : -1;
|
|
178
|
+
return (b.stats.commands || 0) - (a.stats.commands || 0);
|
|
179
|
+
});
|
|
180
|
+
const shown = sorted.slice(0, maxAccounts);
|
|
118
181
|
|
|
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];
|
|
182
|
+
for (let si = 0; si < shown.length; si++) {
|
|
183
|
+
const w = shown[si];
|
|
142
184
|
const wi = _workers.indexOf(w);
|
|
143
|
-
const
|
|
185
|
+
const col2 = wc(wi);
|
|
186
|
+
const stCol = statusColor(w);
|
|
187
|
+
const stText = statusText(w);
|
|
144
188
|
const earned = w.stats.coins > 0 ? `${c.green}+${w.stats.coins.toLocaleString()}${c.reset}` : DIM + '—' + c.reset;
|
|
145
189
|
const cmds = w.stats.commands || 0;
|
|
146
190
|
const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
|
|
147
191
|
|
|
148
|
-
process.stdout.write(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
);
|
|
192
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
|
|
193
|
+
process.stdout.write(`${DIM}${padL(wi + 1, 2)}${c.reset} `);
|
|
194
|
+
process.stdout.write(`${stCol}${padR(stText, col.st)}${c.reset} `);
|
|
195
|
+
process.stdout.write(`${col2}${padR(w.username || '?', col.name)}${c.reset} `);
|
|
196
|
+
process.stdout.write(`${DIM}${padR(w.lastStatus || 'idle', col.last)}${c.reset} `);
|
|
197
|
+
process.stdout.write(`${padL(cmds, col.cmds)} `);
|
|
198
|
+
process.stdout.write(`${padL(rate, col.ok)}% `);
|
|
199
|
+
process.stdout.write(`${padL(earned, col.earned)} `);
|
|
200
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
157
201
|
}
|
|
158
202
|
|
|
159
|
-
|
|
203
|
+
if (sorted.length > maxAccounts) {
|
|
204
|
+
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`);
|
|
205
|
+
}
|
|
160
206
|
|
|
207
|
+
// ── Totals ──
|
|
161
208
|
let totalCoins = 0, totalCmds = 0, totalOk = 0;
|
|
162
209
|
for (const w of _workers) {
|
|
163
210
|
totalCoins += w.stats.coins || 0;
|
|
@@ -167,35 +214,86 @@ function draw() {
|
|
|
167
214
|
const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
|
|
168
215
|
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
169
216
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
217
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
218
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
|
|
219
|
+
process.stdout.write(`${c.bold}Σ${c.reset} `);
|
|
220
|
+
process.stdout.write(`${DIM}${padL(_workers.length, 2)} acc${c.reset} `);
|
|
221
|
+
process.stdout.write(`${' '.repeat(col.name)} `);
|
|
222
|
+
process.stdout.write(`${DIM}${padR(w.lastStatus || '', col.last)}${c.reset} `);
|
|
223
|
+
process.stdout.write(`${padL(totalCmds, col.cmds)} `);
|
|
224
|
+
process.stdout.write(`${padL(rate, col.ok)}% `);
|
|
225
|
+
process.stdout.write(`${totalCoins > 0 ? c.green + padL('+' + totalCoins.toLocaleString(), col.earned) + c.reset : DIM + padL('—', col.earned) + c.reset} `);
|
|
226
|
+
process.stdout.write(`${DIM}${fmtUptime()} | ${memMB}MB${c.reset} `.padEnd(W - 4));
|
|
227
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
181
228
|
|
|
182
|
-
// ──
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
229
|
+
// ── Events section ──
|
|
230
|
+
process.stdout.write(`\x1b[38;2;77;212;238m├─${T}─┤\x1b[0m\n`);
|
|
231
|
+
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`);
|
|
232
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${'─'.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
233
|
+
|
|
234
|
+
// Show recent events per account
|
|
235
|
+
const evLines = [];
|
|
236
|
+
for (let i = 0; i < _workers.length; i++) {
|
|
237
|
+
if (_eventLines[i] && _eventLines[i].length > 0) {
|
|
238
|
+
const latest = _eventLines[i][_eventLines[i].length - 1];
|
|
239
|
+
const col2 = wc(i);
|
|
240
|
+
const name = trunc(_workers[i]?.username || '?', 14);
|
|
241
|
+
evLines.push({ i, text: latest.text, ts: latest.ts, col: col2, name });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for (let i = 0; i < Math.min(eventH, evLines.length); i++) {
|
|
246
|
+
const ev = evLines[evLines.length - 1 - i];
|
|
247
|
+
const now = new Date();
|
|
248
|
+
const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
|
|
249
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m `);
|
|
250
|
+
process.stdout.write(`${ev.col}${padR(ev.name, 14)}${c.reset} `);
|
|
251
|
+
process.stdout.write(`${DIM}[${ts}]${c.reset} ${ev.text}${' '.repeat(Math.max(0, W - 20 - ev.name.length - ev.text.length))}`);
|
|
252
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
for (let i = 0; i < Math.max(0, eventH - evLines.length); i++) {
|
|
256
|
+
process.stdout.write(`\x1b[38;2;77;212;238m│\x1b[0m ${' '.repeat(W - 4)} \x1b[38;2;77;212;238m│\x1b[0m\n`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Bottom of box ──
|
|
260
|
+
process.stdout.write(`\x1b[38;2;77;212;238m└─${T}─┘\x1b[0m\n`);
|
|
187
261
|
}
|
|
188
262
|
|
|
263
|
+
// ── Event tracking ────────────────────────────────────────────
|
|
264
|
+
let _eventLines = []; // [accountIdx] = [{text, ts}]
|
|
265
|
+
const MAX_EVENTS = 3;
|
|
266
|
+
|
|
189
267
|
// ── Public API ────────────────────────────────────────────────
|
|
268
|
+
|
|
190
269
|
function init({ workers, isShuttingDown }) {
|
|
191
270
|
_startTime = Date.now();
|
|
192
271
|
_workers = workers;
|
|
193
272
|
_isShuttingDown = isShuttingDown || (() => false);
|
|
194
273
|
_version = '0.0.0';
|
|
274
|
+
_eventLines = [];
|
|
195
275
|
}
|
|
196
276
|
|
|
197
277
|
function drawBanner(version) { _version = version; }
|
|
198
278
|
function start() {}
|
|
199
279
|
function stop() { process.stdout.write(c.reset + '\n'); }
|
|
200
280
|
|
|
201
|
-
|
|
281
|
+
function log(accountIdx, msg) {
|
|
282
|
+
const now = new Date();
|
|
283
|
+
const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
|
|
284
|
+
|
|
285
|
+
if (accountIdx >= 0) {
|
|
286
|
+
if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
|
|
287
|
+
_eventLines[accountIdx].push({ text: msg, ts });
|
|
288
|
+
if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
draw();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function logGlobal(msg) {
|
|
295
|
+
// Global events go to account 0
|
|
296
|
+
log(-1, msg);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop };
|