dankgrinder 8.36.0 → 8.38.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 +26 -23
- package/lib/ui.js +156 -141
- package/package.json +1 -1
package/lib/grinder.js
CHANGED
|
@@ -2750,10 +2750,12 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2750
2750
|
const prev = w._lifesavers;
|
|
2751
2751
|
w._lifesavers = event.lifesaversLeft;
|
|
2752
2752
|
if (event.lifesaversLeft === 0) {
|
|
2753
|
-
w.
|
|
2753
|
+
w.lastStatus = 'DEAD';
|
|
2754
|
+
w._alert = { type: 'death' };
|
|
2754
2755
|
w.setCooldown?.('crime', 86400);
|
|
2755
2756
|
w.setCooldown?.('search', 86400);
|
|
2756
2757
|
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 — ${w.username} has 0 lifesavers! Crime/search disabled`);
|
|
2757
2759
|
} else {
|
|
2758
2760
|
w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
|
|
2759
2761
|
if (prev !== event.lifesaversLeft) {
|
|
@@ -2761,7 +2763,9 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2761
2763
|
w.setCooldown?.('search', 60);
|
|
2762
2764
|
}
|
|
2763
2765
|
if (event.lifesaversLeft <= 2) {
|
|
2766
|
+
w._alert = { type: 'lowls' };
|
|
2764
2767
|
sendWebhook?.('LOW LIFESAVERS', `**${w.username}** has only **${event.lifesaversLeft}** lifesaver(s) left!`, 0xfbbf24);
|
|
2768
|
+
ui.log(`${c.yellow}!${c.reset} ${w.username} — only ${event.lifesaversLeft} lifesavers!`);
|
|
2765
2769
|
}
|
|
2766
2770
|
}
|
|
2767
2771
|
}
|
|
@@ -2770,6 +2774,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2770
2774
|
if (event.type === 'levelup') {
|
|
2771
2775
|
if (event.to > 0) {
|
|
2772
2776
|
w._level = event.to;
|
|
2777
|
+
ui.log(`${c.blue}↑${c.reset} ${w.username} leveled up to ${event.to}`);
|
|
2773
2778
|
}
|
|
2774
2779
|
}
|
|
2775
2780
|
}
|
|
@@ -2783,8 +2788,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2783
2788
|
const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
|
|
2784
2789
|
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));
|
|
2785
2790
|
|
|
2786
|
-
ui.
|
|
2787
|
-
ui.printLogin(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
|
|
2791
|
+
ui.log(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
|
|
2788
2792
|
const BATCH_SIZE = 10;
|
|
2789
2793
|
for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
|
|
2790
2794
|
if (shutdownCalled) break;
|
|
@@ -2799,21 +2803,20 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2799
2803
|
await worker.start();
|
|
2800
2804
|
if (worker._tokenInvalid) {
|
|
2801
2805
|
worker.lastStatus = 'invalid token';
|
|
2802
|
-
ui.
|
|
2806
|
+
ui.log(`${c.red}E${c.reset} [${i + idx + 1}] ${acc.label || acc.id} — invalid token`);
|
|
2803
2807
|
} else if (worker.channel) {
|
|
2804
2808
|
worker.lastStatus = 'ready';
|
|
2805
|
-
ui.
|
|
2806
|
-
ui.render();
|
|
2809
|
+
ui.log(`${c.green}·${c.reset} [${i + idx + 1}] ${worker.username} connected`);
|
|
2807
2810
|
} else {
|
|
2808
2811
|
worker.lastStatus = 'timeout';
|
|
2809
|
-
ui.
|
|
2812
|
+
ui.log(`${c.yellow}·${c.reset} [${i + idx + 1}] ${acc.label || acc.id} — timeout`);
|
|
2810
2813
|
}
|
|
2811
2814
|
} catch (e) {
|
|
2812
|
-
ui.
|
|
2815
|
+
ui.log(`${c.red}E${c.reset} [${i + idx + 1}] ERROR: ${e.message}`);
|
|
2813
2816
|
}
|
|
2814
2817
|
}));
|
|
2815
2818
|
} catch (e) {
|
|
2816
|
-
ui.
|
|
2819
|
+
ui.log(`${c.red}!${c.reset} BATCH ERROR: ${e.message}`);
|
|
2817
2820
|
}
|
|
2818
2821
|
if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
|
|
2819
2822
|
hintGC();
|
|
@@ -2822,32 +2825,32 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2822
2825
|
const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
|
|
2823
2826
|
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
2824
2827
|
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
2825
|
-
ui.
|
|
2828
|
+
ui.log(`${c.green}·${c.reset} Login: ${loginDone}/${accounts.length} connected`);
|
|
2826
2829
|
if (invalidWorkers.length > 0) {
|
|
2827
|
-
for (const w of invalidWorkers) ui.
|
|
2830
|
+
for (const w of invalidWorkers) ui.log(`${c.red}E${c.reset} invalid token: ${w.account.label || w.account.id}`);
|
|
2828
2831
|
}
|
|
2829
2832
|
if (timedOutWorkers.length > 0) {
|
|
2830
|
-
ui.
|
|
2833
|
+
ui.log(`${c.yellow}~${c.reset} ${timedOutWorkers.length} timed out (will retry in background)`);
|
|
2831
2834
|
}
|
|
2832
|
-
ui.
|
|
2835
|
+
ui.draw();
|
|
2833
2836
|
|
|
2834
2837
|
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
2835
2838
|
|
|
2836
2839
|
// ── Phase 2: Inventory check ────────────────────────────────────
|
|
2837
|
-
ui.
|
|
2840
|
+
ui.log(`${c.dim}Checking inventory...${c.reset}`);
|
|
2838
2841
|
let invFailed = 0;
|
|
2839
2842
|
await Promise.all(activeWorkers.map(async (w) => {
|
|
2840
2843
|
try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
|
|
2841
2844
|
catch { invFailed++; }
|
|
2842
2845
|
}));
|
|
2843
2846
|
if (invFailed > 0) {
|
|
2844
|
-
ui.
|
|
2847
|
+
ui.log(`${c.red}!${c.reset} Inventory failed for ${invFailed} accounts`);
|
|
2845
2848
|
} else {
|
|
2846
|
-
ui.
|
|
2849
|
+
ui.log(`${c.green}·${c.reset} Inventory OK`);
|
|
2847
2850
|
}
|
|
2848
2851
|
|
|
2849
2852
|
// ── Phase 2.5: Balance check ───────────────────────────────────
|
|
2850
|
-
ui.
|
|
2853
|
+
ui.log(`${c.dim}Checking balances...${c.reset}`);
|
|
2851
2854
|
for (const w of activeWorkers) {
|
|
2852
2855
|
try { await w.checkBalance(true); } catch {}
|
|
2853
2856
|
}
|
|
@@ -2856,10 +2859,10 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2856
2859
|
totalCoins += w.stats?.balance || 0;
|
|
2857
2860
|
totalCoins += w.stats?.bankBalance || 0;
|
|
2858
2861
|
}
|
|
2859
|
-
ui.
|
|
2862
|
+
ui.log(`${c.blue}$${c.reset} Balances: ${c.green}+⏣${totalCoins.toLocaleString()}${c.reset} across ${activeWorkers.length} accounts`);
|
|
2860
2863
|
|
|
2861
2864
|
// ── Phase 2.75: DM history check ────────────────────────────────
|
|
2862
|
-
ui.
|
|
2865
|
+
ui.log(`${c.dim}Checking DM history...${c.reset}`);
|
|
2863
2866
|
let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
|
|
2864
2867
|
for (const w of activeWorkers) {
|
|
2865
2868
|
try {
|
|
@@ -2877,18 +2880,18 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2877
2880
|
}
|
|
2878
2881
|
} catch {}
|
|
2879
2882
|
}
|
|
2880
|
-
if (dmNoLs.length > 0) ui.
|
|
2883
|
+
if (dmNoLs.length > 0) ui.log(`${c.red}E${c.reset} No lifesavers: ${dmNoLs.join(', ')}`);
|
|
2881
2884
|
const parts = [];
|
|
2882
2885
|
if (dmDeaths > 0) parts.push(`${dmDeaths} deaths`);
|
|
2883
2886
|
if (dmLevelUps > 0) parts.push(`${dmLevelUps} level-ups`);
|
|
2884
|
-
ui.
|
|
2885
|
-
ui.clearLoginLines();
|
|
2887
|
+
ui.log(`${c.green}·${c.reset} DM: ${parts.length > 0 ? parts.join(', ') : c.green + 'clean' + c.reset}`);
|
|
2886
2888
|
|
|
2887
2889
|
// ── Phase 3: Start grind loops ───────────────────────────────────
|
|
2890
|
+
ui.log(`${c.green}·${c.reset} Starting ${activeWorkers.length} grind loops...`);
|
|
2888
2891
|
for (const w of activeWorkers) {
|
|
2889
2892
|
if (!shutdownCalled) w.grindLoop();
|
|
2890
2893
|
}
|
|
2891
|
-
ui.
|
|
2894
|
+
ui.draw();
|
|
2892
2895
|
|
|
2893
2896
|
// Cluster heartbeat — lets other nodes see this node is alive
|
|
2894
2897
|
if (CLUSTER_ENABLED) {
|
package/lib/ui.js
CHANGED
|
@@ -1,186 +1,201 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI Live Dashboard
|
|
3
|
-
*
|
|
4
|
-
* Shows per-account status, earned coins, commands, and session uptime.
|
|
2
|
+
* CLI Live Dashboard — one table, append-only events below.
|
|
3
|
+
* Draws once after login. Events stream below without touching the table.
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
let _startTime = Date.now();
|
|
8
7
|
let _workers = [];
|
|
9
8
|
let _isShuttingDown = () => false;
|
|
9
|
+
let _version = '0.0.0';
|
|
10
|
+
|
|
11
|
+
// ── Big ASCII art banner ──────────────────────────────────────
|
|
12
|
+
const BANNER = [
|
|
13
|
+
' ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗',
|
|
14
|
+
' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║',
|
|
15
|
+
' ██║ ██║██║ ██║██╔██╗ ██║██║ ███╗█████╗ ██║ ██║██╔██╗ ██║',
|
|
16
|
+
' ██║ ██║██║ ██║██║╚██╗██║██║ ██║██╔══╝ ██║ ██║██║╚██╗██║',
|
|
17
|
+
' ██████╔╝╚██████╔╝██║ ╚████║╚██████╔╝███████╗╚██████╔╝██║ ╚████║',
|
|
18
|
+
' ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝',
|
|
19
|
+
' ██╗██╗ ██╗███╗ ██╗ ██████╗ ██╗ ███████╗██████╗ ',
|
|
20
|
+
' ██║██║ ██║████╗ ██║██╔═══██╗██║ ██╔════╝██╔══██╗',
|
|
21
|
+
' ███████╗███████╗ ██║██║ ██║██╔██╗ ██║██║ ██║██║ █████╗ ██████╔╝',
|
|
22
|
+
' ╚════██║╚════██║ ██║██║ ██║██║╚██╗██║██║ ██║██║ ██╔══╝ ██╔══██╗',
|
|
23
|
+
' ██║ ██║ ██║╚██████╔╝██║ ╚████║╚██████╔╝███████╗███████╗██║ ██║',
|
|
24
|
+
' ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Gradient each banner line cyan→pink
|
|
28
|
+
function gradientLine(text, r1, g1, b1, r2, g2, b2) {
|
|
29
|
+
let out = '';
|
|
30
|
+
for (let i = 0; i < text.length; i++) {
|
|
31
|
+
const t = text.length <= 1 ? 0 : i / (text.length - 1);
|
|
32
|
+
const lr = Math.round(r1 + (r2 - r1) * t);
|
|
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]}`;
|
|
36
|
+
}
|
|
37
|
+
return out + '\x1b[0m';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Dark-theme palette ────────────────────────────────────────
|
|
41
|
+
const PALETTE = [
|
|
42
|
+
'\x1b[38;2;77;212;238m', // cyan
|
|
43
|
+
'\x1b[38;2;255;194;77m', // amber
|
|
44
|
+
'\x1b[38;2;130;210;100m', // lime
|
|
45
|
+
'\x1b[38;2;255;120;200m', // pink
|
|
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
|
|
54
|
+
];
|
|
55
|
+
function wc(idx) { return PALETTE[idx % PALETTE.length]; }
|
|
56
|
+
|
|
57
|
+
// ── ANSI helpers ──────────────────────────────────────────────
|
|
58
|
+
const c = {
|
|
59
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
60
|
+
green: '\x1b[38;2;80;255;120m',
|
|
61
|
+
red: '\x1b[38;2;255;80;100m',
|
|
62
|
+
yellow: '\x1b[38;2;255;220;80m',
|
|
63
|
+
cyan: '\x1b[38;2;80;220;255m',
|
|
64
|
+
blue: '\x1b[38;2;100;160;255m',
|
|
65
|
+
};
|
|
66
|
+
const DIM = c.dim;
|
|
67
|
+
|
|
68
|
+
function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
|
|
69
|
+
function padR(s, n) { return trunc(s, n).padEnd(n); }
|
|
70
|
+
function padL(s, n) { return String(s).padStart(n); }
|
|
10
71
|
|
|
11
|
-
function
|
|
72
|
+
function fmtUptime() {
|
|
12
73
|
const s = Math.floor((Date.now() - _startTime) / 1000);
|
|
13
74
|
if (s < 60) return `${s}s`;
|
|
14
75
|
const m = Math.floor(s / 60);
|
|
15
76
|
const h = Math.floor(m / 60);
|
|
16
77
|
const d = Math.floor(h / 24);
|
|
17
|
-
if (d > 0) return `${d}d
|
|
18
|
-
if (h > 0) return `${h}h
|
|
78
|
+
if (d > 0) return `${d}d${h % 24}h`;
|
|
79
|
+
if (h > 0) return `${h}h${m % 60}m`;
|
|
19
80
|
return `${m}m`;
|
|
20
81
|
}
|
|
21
82
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
28
|
-
const DIM = c.dim;
|
|
29
|
-
|
|
30
|
-
function rgb(r, g, b) { return `\x1b[38;2;${r};${g};${b}m`; }
|
|
31
|
-
function lerp(a, b, t) { return Math.round(a + (b - a) * t); }
|
|
32
|
-
function gradientLine(text, from, to) {
|
|
33
|
-
const fr = Array.isArray(from) ? from[0] : 128;
|
|
34
|
-
const fg = Array.isArray(from) ? from[1] : 128;
|
|
35
|
-
const fb = Array.isArray(from) ? from[2] : 128;
|
|
36
|
-
const tr = Array.isArray(to) ? to[0] : 255;
|
|
37
|
-
const tg = Array.isArray(to) ? to[1] : 255;
|
|
38
|
-
const tb = Array.isArray(to) ? to[2] : 255;
|
|
39
|
-
let out = '';
|
|
40
|
-
for (let i = 0; i < text.length; i++) {
|
|
41
|
-
const t = text.length <= 1 ? 0 : i / (text.length - 1);
|
|
42
|
-
out += `\x1b[38;2;${lerp(fr, tr, t)};${lerp(fg, tg, t)};${lerp(fb, tb, t)}m${text[i]}`;
|
|
83
|
+
function icon(w) {
|
|
84
|
+
if (!w.running || !w.channel) return `${c.red}E${c.reset}`;
|
|
85
|
+
if (w.paused || w.dashboardPaused) return `${c.yellow}P${c.reset}`;
|
|
86
|
+
if (w._alert) {
|
|
87
|
+
if (w._alert.type === 'death') return `${c.red}!${c.reset}`;
|
|
88
|
+
if (w._alert.type === 'lowls') return `${c.yellow}!${c.reset}`;
|
|
43
89
|
}
|
|
44
|
-
|
|
90
|
+
if (w._rateLimitHits > (w._prevRateLimits || 0)) return `${c.cyan}~${c.reset}`;
|
|
91
|
+
return `${c.green}·${c.reset}`;
|
|
45
92
|
}
|
|
46
93
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (Date.now() < w.globalCooldownUntil) return `${c.blue}⏳${c.reset}`;
|
|
56
|
-
return `${c.green}●${c.reset}`;
|
|
94
|
+
function sortedWorkers() {
|
|
95
|
+
return [..._workers].sort((a, b) => {
|
|
96
|
+
if (!a.channel !== !b.channel) return !a.channel ? 1 : -1;
|
|
97
|
+
const aA = a.running && a.channel && !a.paused && !a.dashboardPaused;
|
|
98
|
+
const bA = b.running && b.channel && !b.paused && !b.dashboardPaused;
|
|
99
|
+
if (aA !== bA) return bA ? 1 : -1;
|
|
100
|
+
return (b.stats.commands || 0) - (a.stats.commands || 0);
|
|
101
|
+
});
|
|
57
102
|
}
|
|
58
103
|
|
|
59
|
-
|
|
60
|
-
|
|
104
|
+
// ── Draw full table (call once after login) ──────────────────
|
|
105
|
+
function draw() {
|
|
61
106
|
const W = Math.min(process.stdout.columns || 120, 120);
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
107
|
+
const hr = '─'.repeat(W);
|
|
108
|
+
const running = _workers.filter(w => w.running && w.channel && !w.paused && !w.dashboardPaused).length;
|
|
109
|
+
const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
|
|
110
|
+
|
|
111
|
+
console.log('\x1b[2J\x1b[H');
|
|
112
|
+
console.log('');
|
|
113
|
+
|
|
114
|
+
// Big banner with gradient
|
|
115
|
+
for (const line of BANNER) {
|
|
116
|
+
console.log(gradientLine(line, 77, 212, 238, 255, 92, 147));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log('');
|
|
120
|
+
console.log(
|
|
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}`
|
|
71
127
|
);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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];
|
|
142
|
+
const wi = _workers.indexOf(w);
|
|
143
|
+
const col = wc(wi);
|
|
144
|
+
const earned = w.stats.coins > 0 ? `${c.green}+${w.stats.coins.toLocaleString()}${c.reset}` : DIM + '—' + c.reset;
|
|
145
|
+
const cmds = w.stats.commands || 0;
|
|
80
146
|
const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
|
|
81
147
|
|
|
82
|
-
|
|
83
|
-
`
|
|
84
|
-
`${
|
|
148
|
+
process.stdout.write(
|
|
149
|
+
` ${icon(w)} ` +
|
|
150
|
+
`${padL(wi + 1, 2)} ` +
|
|
151
|
+
`${col}${padR(w.username || '?', 18)}${c.reset} ` +
|
|
152
|
+
`${DIM}${padR(w.lastStatus || 'idle', 20)}${c.reset} ` +
|
|
153
|
+
`${padL(cmds, 5)} ` +
|
|
154
|
+
`${padL(rate, 3)}% ` +
|
|
155
|
+
`${earned.padStart(8)}\n`
|
|
85
156
|
);
|
|
86
157
|
}
|
|
87
158
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
lines.push(`${c.bold}${'─'.repeat(W)}${c.reset}`);
|
|
159
|
+
console.log(` ${hr}`);
|
|
91
160
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
for (const w of workers) {
|
|
161
|
+
let totalCoins = 0, totalCmds = 0, totalOk = 0;
|
|
162
|
+
for (const w of _workers) {
|
|
95
163
|
totalCoins += w.stats.coins || 0;
|
|
96
164
|
totalCmds += w.stats.commands || 0;
|
|
165
|
+
totalOk += w.stats.successes || 0;
|
|
97
166
|
}
|
|
167
|
+
const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
|
|
98
168
|
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
169
|
+
|
|
170
|
+
console.log(
|
|
171
|
+
` ${c.bold}Σ${c.reset} ` +
|
|
172
|
+
`${DIM}${padL(_workers.length, 2)} acc${c.reset} ` +
|
|
173
|
+
`${' '.repeat(18)}` +
|
|
174
|
+
`${DIM}${padL(totalCmds, 5)} cmds ${rate}% ` +
|
|
175
|
+
`${totalCoins > 0 ? c.green + '+' + totalCoins.toLocaleString() + c.reset : DIM + '—' + c.reset}` +
|
|
176
|
+
`${' '.repeat(Math.max(0, 8 - String(totalCoins).length - 1))}${DIM}${fmtUptime()} | ${memMB}MB${c.reset}`
|
|
103
177
|
);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
// Redraw in place — also clear any login progress lines above
|
|
107
|
-
const clearLines = _lineCount + _loginLines;
|
|
108
|
-
if (clearLines > 0) {
|
|
109
|
-
process.stdout.write(`\x1b[${clearLines}A`);
|
|
110
|
-
for (let i = 0; i < clearLines; i++) {
|
|
111
|
-
process.stdout.write(`\x1b[2K\r${i < clearLines - 1 ? '\x1b[1A' : ''}`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
process.stdout.write(lines.join('\n') + '\n');
|
|
115
|
-
_lineCount = lines.length - 1;
|
|
116
|
-
_loginLines = 0;
|
|
178
|
+
console.log(` ${hr}`);
|
|
179
|
+
console.log('');
|
|
117
180
|
}
|
|
118
181
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
process.stdout.write(`\x1b[2K\r${i < clearLines - 1 ? '\x1b[1A' : ''}`);
|
|
125
|
-
}
|
|
126
|
-
_lineCount = 0;
|
|
127
|
-
_loginLines = 0;
|
|
128
|
-
}
|
|
182
|
+
// ── Append event log line ────────────────────────────────────
|
|
183
|
+
function log(msg) {
|
|
184
|
+
const now = new Date();
|
|
185
|
+
const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
|
|
186
|
+
process.stdout.write(` ${DIM}[${ts}]${c.reset} ${msg}\n`);
|
|
129
187
|
}
|
|
130
188
|
|
|
131
189
|
// ── Public API ────────────────────────────────────────────────
|
|
132
|
-
|
|
133
190
|
function init({ workers, isShuttingDown }) {
|
|
134
191
|
_startTime = Date.now();
|
|
135
192
|
_workers = workers;
|
|
136
193
|
_isShuttingDown = isShuttingDown || (() => false);
|
|
194
|
+
_version = '0.0.0';
|
|
137
195
|
}
|
|
138
196
|
|
|
139
|
-
function drawBanner(version) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
console.log('');
|
|
143
|
-
console.log(` ${gradient}${c.reset}`);
|
|
144
|
-
console.log(` ${DIM}Ctrl+C to stop${c.reset}`);
|
|
145
|
-
console.log('');
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Show dashboard and start the 10s refresh interval
|
|
149
|
-
function start() {
|
|
150
|
-
if (_workers.length > 30) return;
|
|
151
|
-
_lineCount = 0;
|
|
152
|
-
_loginLines = 0;
|
|
153
|
-
_render();
|
|
154
|
-
_interval = setInterval(() => {
|
|
155
|
-
if (_isShuttingDown()) {
|
|
156
|
-
clearInterval(_interval);
|
|
157
|
-
_interval = null;
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
_render();
|
|
161
|
-
}, 10_000);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Immediately re-render the dashboard (use after worker status changes)
|
|
165
|
-
function render() {
|
|
166
|
-
if (_isShuttingDown()) return;
|
|
167
|
-
_render();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Print a login progress line and count it so it gets cleared on next render
|
|
171
|
-
function printLogin(msg) {
|
|
172
|
-
console.log(msg);
|
|
173
|
-
_loginLines++;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Clear login lines and re-render final state
|
|
177
|
-
function clearLoginLines() {
|
|
178
|
-
_render();
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function stop() {
|
|
182
|
-
if (_interval) { clearInterval(_interval); _interval = null; }
|
|
183
|
-
_clear();
|
|
184
|
-
}
|
|
197
|
+
function drawBanner(version) { _version = version; }
|
|
198
|
+
function start() {}
|
|
199
|
+
function stop() { process.stdout.write(c.reset + '\n'); }
|
|
185
200
|
|
|
186
|
-
module.exports = { init, drawBanner, start,
|
|
201
|
+
module.exports = { init, drawBanner, start, draw, log, workerColor: wc, stop };
|