dankgrinder 8.36.0 → 8.37.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 +30 -23
- package/lib/ui.js +191 -130
- package/package.json +1 -1
package/lib/grinder.js
CHANGED
|
@@ -2750,10 +2750,13 @@ 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.logEvent(`${c.red}E${c.reset} DEATH — ${w.username} has 0 lifesavers! Crime/search disabled`);
|
|
2759
|
+
ui.draw();
|
|
2757
2760
|
} else {
|
|
2758
2761
|
w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
|
|
2759
2762
|
if (prev !== event.lifesaversLeft) {
|
|
@@ -2761,7 +2764,10 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2761
2764
|
w.setCooldown?.('search', 60);
|
|
2762
2765
|
}
|
|
2763
2766
|
if (event.lifesaversLeft <= 2) {
|
|
2767
|
+
w._alert = { type: 'lowls' };
|
|
2764
2768
|
sendWebhook?.('LOW LIFESAVERS', `**${w.username}** has only **${event.lifesaversLeft}** lifesaver(s) left!`, 0xfbbf24);
|
|
2769
|
+
ui.logEvent(`${c.yellow}!${c.reset} ${w.username} — only ${event.lifesaversLeft} lifesavers!`);
|
|
2770
|
+
ui.draw();
|
|
2765
2771
|
}
|
|
2766
2772
|
}
|
|
2767
2773
|
}
|
|
@@ -2770,6 +2776,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2770
2776
|
if (event.type === 'levelup') {
|
|
2771
2777
|
if (event.to > 0) {
|
|
2772
2778
|
w._level = event.to;
|
|
2779
|
+
ui.logEvent(`${c.blue}↑${c.reset} ${w.username} leveled up to ${event.to}`);
|
|
2773
2780
|
}
|
|
2774
2781
|
}
|
|
2775
2782
|
}
|
|
@@ -2783,8 +2790,8 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2783
2790
|
const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
|
|
2784
2791
|
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
2792
|
|
|
2786
|
-
ui.
|
|
2787
|
-
ui.
|
|
2793
|
+
ui.draw(); // draw initial table
|
|
2794
|
+
ui.logEvent(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
|
|
2788
2795
|
const BATCH_SIZE = 10;
|
|
2789
2796
|
for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
|
|
2790
2797
|
if (shutdownCalled) break;
|
|
@@ -2799,21 +2806,21 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2799
2806
|
await worker.start();
|
|
2800
2807
|
if (worker._tokenInvalid) {
|
|
2801
2808
|
worker.lastStatus = 'invalid token';
|
|
2802
|
-
ui.
|
|
2809
|
+
ui.logEvent(`${c.red}E${c.reset} [${i + idx + 1}] ${acc.label || acc.id} — invalid token`);
|
|
2803
2810
|
} else if (worker.channel) {
|
|
2804
2811
|
worker.lastStatus = 'ready';
|
|
2805
|
-
ui.
|
|
2806
|
-
ui.
|
|
2812
|
+
ui.logEvent(`${c.green}·${c.reset} [${i + idx + 1}] ${worker.username} connected`);
|
|
2813
|
+
ui.draw();
|
|
2807
2814
|
} else {
|
|
2808
2815
|
worker.lastStatus = 'timeout';
|
|
2809
|
-
ui.
|
|
2816
|
+
ui.logEvent(`${c.yellow}·${c.reset} [${i + idx + 1}] ${acc.label || acc.id} — timeout`);
|
|
2810
2817
|
}
|
|
2811
2818
|
} catch (e) {
|
|
2812
|
-
ui.
|
|
2819
|
+
ui.logEvent(`${c.red}E${c.reset} [${i + idx + 1}] ERROR: ${e.message}`);
|
|
2813
2820
|
}
|
|
2814
2821
|
}));
|
|
2815
2822
|
} catch (e) {
|
|
2816
|
-
ui.
|
|
2823
|
+
ui.logEvent(`${c.red}!${c.reset} BATCH ERROR: ${e.message}`);
|
|
2817
2824
|
}
|
|
2818
2825
|
if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
|
|
2819
2826
|
hintGC();
|
|
@@ -2822,32 +2829,32 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2822
2829
|
const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
|
|
2823
2830
|
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
2824
2831
|
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
2825
|
-
ui.
|
|
2832
|
+
ui.logEvent(`${c.green}·${c.reset} Login: ${loginDone}/${accounts.length} connected`);
|
|
2826
2833
|
if (invalidWorkers.length > 0) {
|
|
2827
|
-
for (const w of invalidWorkers) ui.
|
|
2834
|
+
for (const w of invalidWorkers) ui.logEvent(`${c.red}E${c.reset} invalid token: ${w.account.label || w.account.id}`);
|
|
2828
2835
|
}
|
|
2829
2836
|
if (timedOutWorkers.length > 0) {
|
|
2830
|
-
ui.
|
|
2837
|
+
ui.logEvent(`${c.yellow}~${c.reset} ${timedOutWorkers.length} timed out (will retry in background)`);
|
|
2831
2838
|
}
|
|
2832
|
-
ui.
|
|
2839
|
+
ui.draw();
|
|
2833
2840
|
|
|
2834
2841
|
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
2835
2842
|
|
|
2836
2843
|
// ── Phase 2: Inventory check ────────────────────────────────────
|
|
2837
|
-
ui.
|
|
2844
|
+
ui.logEvent(`${c.dim}Checking inventory...${c.reset}`);
|
|
2838
2845
|
let invFailed = 0;
|
|
2839
2846
|
await Promise.all(activeWorkers.map(async (w) => {
|
|
2840
2847
|
try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
|
|
2841
2848
|
catch { invFailed++; }
|
|
2842
2849
|
}));
|
|
2843
2850
|
if (invFailed > 0) {
|
|
2844
|
-
ui.
|
|
2851
|
+
ui.logEvent(`${c.red}!${c.reset} Inventory failed for ${invFailed} accounts`);
|
|
2845
2852
|
} else {
|
|
2846
|
-
ui.
|
|
2853
|
+
ui.logEvent(`${c.green}·${c.reset} Inventory OK`);
|
|
2847
2854
|
}
|
|
2848
2855
|
|
|
2849
2856
|
// ── Phase 2.5: Balance check ───────────────────────────────────
|
|
2850
|
-
ui.
|
|
2857
|
+
ui.logEvent(`${c.dim}Checking balances...${c.reset}`);
|
|
2851
2858
|
for (const w of activeWorkers) {
|
|
2852
2859
|
try { await w.checkBalance(true); } catch {}
|
|
2853
2860
|
}
|
|
@@ -2856,10 +2863,10 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2856
2863
|
totalCoins += w.stats?.balance || 0;
|
|
2857
2864
|
totalCoins += w.stats?.bankBalance || 0;
|
|
2858
2865
|
}
|
|
2859
|
-
ui.
|
|
2866
|
+
ui.logEvent(`${c.blue}$${c.reset} Balances: ${c.green}+⏣${totalCoins.toLocaleString()}${c.reset} across ${activeWorkers.length} accounts`);
|
|
2860
2867
|
|
|
2861
2868
|
// ── Phase 2.75: DM history check ────────────────────────────────
|
|
2862
|
-
ui.
|
|
2869
|
+
ui.logEvent(`${c.dim}Checking DM history...${c.reset}`);
|
|
2863
2870
|
let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
|
|
2864
2871
|
for (const w of activeWorkers) {
|
|
2865
2872
|
try {
|
|
@@ -2877,18 +2884,18 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2877
2884
|
}
|
|
2878
2885
|
} catch {}
|
|
2879
2886
|
}
|
|
2880
|
-
if (dmNoLs.length > 0) ui.
|
|
2887
|
+
if (dmNoLs.length > 0) ui.logEvent(`${c.red}E${c.reset} No lifesavers: ${dmNoLs.join(', ')}`);
|
|
2881
2888
|
const parts = [];
|
|
2882
2889
|
if (dmDeaths > 0) parts.push(`${dmDeaths} deaths`);
|
|
2883
2890
|
if (dmLevelUps > 0) parts.push(`${dmLevelUps} level-ups`);
|
|
2884
|
-
ui.
|
|
2885
|
-
ui.clearLoginLines();
|
|
2891
|
+
ui.logEvent(`${c.green}·${c.reset} DM: ${parts.length > 0 ? parts.join(', ') : c.green + 'clean' + c.reset}`);
|
|
2886
2892
|
|
|
2887
2893
|
// ── Phase 3: Start grind loops ───────────────────────────────────
|
|
2894
|
+
ui.logEvent(`${c.green}·${c.reset} Starting ${activeWorkers.length} grind loops...`);
|
|
2888
2895
|
for (const w of activeWorkers) {
|
|
2889
2896
|
if (!shutdownCalled) w.grindLoop();
|
|
2890
2897
|
}
|
|
2891
|
-
ui.
|
|
2898
|
+
ui.draw();
|
|
2892
2899
|
|
|
2893
2900
|
// Cluster heartbeat — lets other nodes see this node is alive
|
|
2894
2901
|
if (CLUSTER_ENABLED) {
|
package/lib/ui.js
CHANGED
|
@@ -1,131 +1,219 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI Live Dashboard
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* CLI Live Dashboard — compact, colorful, append-only.
|
|
3
|
+
* Shows top-N active accounts that fit in the window.
|
|
4
|
+
* Each account has its own color. Events logged below.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
let _startTime = Date.now();
|
|
8
8
|
let _workers = [];
|
|
9
9
|
let _isShuttingDown = () => false;
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
// ── Dark-theme palette (light/bright colors on dark bg) ───────
|
|
12
|
+
const PALETTE = [
|
|
13
|
+
'\x1b[38;2;77;212;238m', // cyan
|
|
14
|
+
'\x1b[38;2;255;194;77m', // amber
|
|
15
|
+
'\x1b[38;2;130;210;100m', // lime
|
|
16
|
+
'\x1b[38;2;255;120;200m', // pink
|
|
17
|
+
'\x1b[38;2;120;180;255m', // sky
|
|
18
|
+
'\x1b[38;2;255;180;80m', // orange
|
|
19
|
+
'\x1b[38;2;180;130;255m', // lavender
|
|
20
|
+
'\x1b[38;2;100;255;180m', // teal
|
|
21
|
+
'\x1b[38;2;255;150;100m', // peach
|
|
22
|
+
'\x1b[38;2;150;255;200m', // mint
|
|
23
|
+
'\x1b[38;2;255;255;120m', // yellow
|
|
24
|
+
'\x1b[38;2;200;150;255m', // violet
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function workerColor(idx) { return PALETTE[idx % PALETTE.length]; }
|
|
28
|
+
|
|
29
|
+
// ── ANSI helpers ──────────────────────────────────────────────
|
|
30
|
+
const c = {
|
|
31
|
+
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
32
|
+
green: '\x1b[38;2;80;255;120m',
|
|
33
|
+
red: '\x1b[38;2;255;80;100m',
|
|
34
|
+
yellow: '\x1b[38;2;255;220;80m',
|
|
35
|
+
cyan: '\x1b[38;2;80;220;255m',
|
|
36
|
+
blue: '\x1b[38;2;100;160;255m',
|
|
37
|
+
gray: '\x1b[38;2;120;130;150m',
|
|
38
|
+
white: '\x1b[38;2;220;220;240m',
|
|
39
|
+
};
|
|
40
|
+
const DIM = c.dim;
|
|
41
|
+
|
|
42
|
+
// ── Uptime ───────────────────────────────────────────────────
|
|
43
|
+
function fmtUptime() {
|
|
12
44
|
const s = Math.floor((Date.now() - _startTime) / 1000);
|
|
13
45
|
if (s < 60) return `${s}s`;
|
|
14
46
|
const m = Math.floor(s / 60);
|
|
15
47
|
const h = Math.floor(m / 60);
|
|
16
48
|
const d = Math.floor(h / 24);
|
|
17
|
-
if (d > 0) return `${d}d
|
|
18
|
-
if (h > 0) return `${h}h
|
|
49
|
+
if (d > 0) return `${d}d${h % 24}h`;
|
|
50
|
+
if (h > 0) return `${h}h${m % 60}m`;
|
|
19
51
|
return `${m}m`;
|
|
20
52
|
}
|
|
21
53
|
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
54
|
+
// ── Column widths based on terminal width ────────────────────
|
|
55
|
+
function colWidths(totalW) {
|
|
56
|
+
const minW = 80;
|
|
57
|
+
const w = Math.max(totalW, minW);
|
|
58
|
+
// Columns: # | Status | Account | LastCmd | Cmds | OK% | Earned
|
|
59
|
+
// Reserve: 1+2+1 + 1+3+1 + 1+16+1 + 1+20+1 + 1+5+1 + 1+3+1 + 1+12+1 = ~72 fixed
|
|
60
|
+
const fixed = 74; // 1+2+1 + 1+3+1 + 1+16+1 + 1+20+1 + 1+5+1 + 1+3+1 + 1+12+1
|
|
61
|
+
const extra = Math.max(0, w - fixed);
|
|
62
|
+
return {
|
|
63
|
+
status: 3,
|
|
64
|
+
num: 2,
|
|
65
|
+
name: 16,
|
|
66
|
+
lastCmd: 20 + Math.floor(extra * 0.5),
|
|
67
|
+
cmds: 5,
|
|
68
|
+
ok: 3,
|
|
69
|
+
earned: 12,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
29
72
|
|
|
30
|
-
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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]}`;
|
|
73
|
+
// ── Status icon ─────────────────────────────────────────────
|
|
74
|
+
function icon(w) {
|
|
75
|
+
if (!w.running || !w.channel) return `${c.red}E${c.reset}`;
|
|
76
|
+
if (w.paused || w.dashboardPaused) return `${c.yellow}P${c.reset}`;
|
|
77
|
+
if (w._alert) {
|
|
78
|
+
if (w._alert.type === 'death') return `${c.red}!${c.reset}`;
|
|
79
|
+
if (w._alert.type === 'lowls') return `${c.yellow}!${c.reset}`;
|
|
43
80
|
}
|
|
44
|
-
|
|
81
|
+
if (w._rateLimitHits > (w._prevRateLimits || 0)) return `${c.cyan}~${c.reset}`;
|
|
82
|
+
return `${c.green}·${c.reset}`;
|
|
45
83
|
}
|
|
46
84
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!w.running || !w.channel) return `${c.red}✗${c.reset}`;
|
|
53
|
-
if (w.paused || w.dashboardPaused) return `${c.yellow}⏸${c.reset}`;
|
|
54
|
-
if (w.busy || w._invRunning || w._sellRunning) return `${c.cyan}⚙${c.reset}`;
|
|
55
|
-
if (Date.now() < w.globalCooldownUntil) return `${c.blue}⏳${c.reset}`;
|
|
56
|
-
return `${c.green}●${c.reset}`;
|
|
85
|
+
// ── Truncate with ellipsis ───────────────────────────────────
|
|
86
|
+
function trunc(str, len) {
|
|
87
|
+
str = String(str || '');
|
|
88
|
+
if (str.length <= len) return str;
|
|
89
|
+
return str.substring(0, len - 1) + '…';
|
|
57
90
|
}
|
|
58
91
|
|
|
59
|
-
|
|
60
|
-
|
|
92
|
+
// ── Draw header ─────────────────────────────────────────────
|
|
93
|
+
function drawHeader(version) {
|
|
94
|
+
const rows = 8; // banner(3) + 1gap + header(1) + hr(1) + N rows + total(1) + gap(1)
|
|
95
|
+
const availRows = process.stdout.rows || 40;
|
|
96
|
+
const visible = Math.min(_workers.length, Math.max(5, availRows - rows));
|
|
61
97
|
const W = Math.min(process.stdout.columns || 120, 120);
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
98
|
+
|
|
99
|
+
const cols = colWidths(W);
|
|
100
|
+
const hr = '─'.repeat(W);
|
|
101
|
+
|
|
102
|
+
const running = _workers.filter(w => w.running && w.channel).length;
|
|
103
|
+
const paused = _workers.filter(w => w.paused || w.dashboardPaused).length;
|
|
104
|
+
const active = running + paused;
|
|
105
|
+
|
|
106
|
+
// Top bar
|
|
107
|
+
console.log('');
|
|
108
|
+
console.log(` ${c.bold}DANK${c.reset}${c.blue}GRINDER${c.reset} ${DIM}v${version}${c.reset} ${c.green}●${c.reset}online ${DIM}|${c.reset} ${fmtUptime()} ${c.green}·${c.reset}${running} ${c.yellow}~${c.reset}${paused} ${c.dim}Ctrl+C${c.reset}`);
|
|
109
|
+
|
|
110
|
+
// Account summary if many
|
|
111
|
+
if (_workers.length > visible) {
|
|
112
|
+
console.log(` ${c.dim}${_workers.length} accounts | showing top ${visible} by activity | scroll: web dashboard${c.reset}`);
|
|
113
|
+
}
|
|
114
|
+
console.log('');
|
|
115
|
+
|
|
116
|
+
// Column headers
|
|
117
|
+
console.log(
|
|
118
|
+
` ${c.bold}#${c.reset} ` +
|
|
119
|
+
`${c.bold}${trunc('Status', cols.status).padEnd(cols.status)}${c.reset} ` +
|
|
120
|
+
`${c.bold}${trunc('Account', cols.name).padEnd(cols.name)}${c.reset} ` +
|
|
121
|
+
`${c.bold}${trunc('Last Command', cols.lastCmd).padEnd(cols.lastCmd)}${c.reset} ` +
|
|
122
|
+
`${c.bold}${trunc('Cmds', cols.cmds).padStart(cols.cmds)}${c.reset} ` +
|
|
123
|
+
`${c.bold}${trunc('OK%', cols.ok).padStart(cols.ok)}${c.reset} ` +
|
|
124
|
+
`${c.bold}${trunc('Earned', cols.earned).padStart(cols.earned)}${c.reset}`
|
|
71
125
|
);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
126
|
+
console.log(` ${hr}`);
|
|
127
|
+
|
|
128
|
+
return { visible, W, cols };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Sort: running first, then by activity ──────────────────
|
|
132
|
+
function sortedWorkers(visible) {
|
|
133
|
+
return [..._workers]
|
|
134
|
+
.sort((a, b) => {
|
|
135
|
+
// Invalid/timeout at bottom
|
|
136
|
+
if (!a.channel && !!b.channel) return 1;
|
|
137
|
+
if (!b.channel && !!a.channel) return -1;
|
|
138
|
+
// Running/active first
|
|
139
|
+
const aActive = a.running && a.channel && !a.paused && !a.dashboardPaused;
|
|
140
|
+
const bActive = b.running && b.channel && !b.paused && !b.dashboardPaused;
|
|
141
|
+
if (aActive !== bActive) return bActive ? 1 : -1;
|
|
142
|
+
// Sort by total activity
|
|
143
|
+
const aScore = (a.stats.commands || 0) + (a.stats.coins || 0);
|
|
144
|
+
const bScore = (b.stats.commands || 0) + (b.stats.coins || 0);
|
|
145
|
+
return bScore - aScore;
|
|
146
|
+
})
|
|
147
|
+
.slice(0, visible);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Draw all rows ───────────────────────────────────────────
|
|
151
|
+
function drawRows({ visible, W, cols }) {
|
|
152
|
+
const hr = '─'.repeat(W);
|
|
153
|
+
const sorted = sortedWorkers(visible);
|
|
154
|
+
|
|
155
|
+
for (let si = 0; si < sorted.length; si++) {
|
|
156
|
+
const w = sorted[si];
|
|
157
|
+
const wi = _workers.indexOf(w); // original index for color
|
|
158
|
+
const col = workerColor(wi);
|
|
159
|
+
|
|
160
|
+
const earned = w.stats.coins > 0
|
|
161
|
+
? `${c.green}+${w.stats.coins.toLocaleString()}${c.reset}`
|
|
162
|
+
: DIM + '—' + c.reset;
|
|
163
|
+
const cmds = w.stats.commands || 0;
|
|
164
|
+
const rate = w.stats.commands > 0
|
|
165
|
+
? Math.round((w.stats.successes / w.stats.commands) * 100)
|
|
166
|
+
: 0;
|
|
167
|
+
|
|
168
|
+
process.stdout.write(
|
|
169
|
+
` ${icon(w)} ` +
|
|
170
|
+
`${DIM}${String(wi + 1).padStart(cols.num)}${c.reset} ` +
|
|
171
|
+
`${col}${trunc(w.username || '?', cols.name).padEnd(cols.name)}${c.reset} ` +
|
|
172
|
+
`${DIM}${trunc(w.lastStatus || 'idle', cols.lastCmd).padEnd(cols.lastCmd)}${c.reset} ` +
|
|
173
|
+
`${String(cmds).padStart(cols.cmds)} ` +
|
|
174
|
+
`${String(rate).padStart(cols.ok)}% ` +
|
|
175
|
+
`${earned.padStart(cols.earned)}\n`
|
|
85
176
|
);
|
|
86
177
|
}
|
|
87
178
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
lines.push(`${c.bold}${'─'.repeat(W)}${c.reset}`);
|
|
179
|
+
console.log(` ${hr}`);
|
|
91
180
|
|
|
92
181
|
// Totals
|
|
93
|
-
let totalCoins = 0, totalCmds = 0;
|
|
94
|
-
for (const w of
|
|
182
|
+
let totalCoins = 0, totalCmds = 0, totalOk = 0;
|
|
183
|
+
for (const w of _workers) {
|
|
95
184
|
totalCoins += w.stats.coins || 0;
|
|
96
185
|
totalCmds += w.stats.commands || 0;
|
|
186
|
+
totalOk += w.stats.successes || 0;
|
|
97
187
|
}
|
|
188
|
+
const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
|
|
98
189
|
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
190
|
+
const extra = Math.max(0, W - 74);
|
|
191
|
+
const namePad = 16 + Math.floor(extra * 0.5);
|
|
192
|
+
|
|
193
|
+
console.log(
|
|
194
|
+
` ${c.bold}Σ${c.reset} ` +
|
|
195
|
+
`${DIM}${' '.repeat(cols.num)}${c.reset} ` +
|
|
196
|
+
`${c.bold}${_workers.length} accounts${c.reset}` +
|
|
197
|
+
`${' '.repeat(Math.max(0, namePad - 12))}` +
|
|
198
|
+
`${String(totalCmds).padStart(cols.cmds)} ` +
|
|
199
|
+
`${String(rate).padStart(cols.ok)}% ` +
|
|
200
|
+
`${totalCoins > 0 ? c.green + '+' + totalCoins.toLocaleString() + c.reset : DIM + '—' + c.reset}` +
|
|
201
|
+
`${' '.repeat(Math.max(0, cols.earned - String(totalCoins).length - 1))}` +
|
|
202
|
+
` ${DIM}${fmtUptime()} | ${memMB}MB${c.reset}`
|
|
103
203
|
);
|
|
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;
|
|
204
|
+
console.log('');
|
|
117
205
|
}
|
|
118
206
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
207
|
+
// ── Event log ────────────────────────────────────────────────
|
|
208
|
+
let _eventCount = 0;
|
|
209
|
+
const MAX_EVENTS = 6;
|
|
210
|
+
|
|
211
|
+
function logEvent(msg) {
|
|
212
|
+
const now = new Date();
|
|
213
|
+
const ts = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
214
|
+
process.stdout.write(` ${DIM}[${ts}]${c.reset} ${msg}\n`);
|
|
215
|
+
_eventCount++;
|
|
216
|
+
if (_eventCount > MAX_EVENTS) _eventCount = MAX_EVENTS;
|
|
129
217
|
}
|
|
130
218
|
|
|
131
219
|
// ── Public API ────────────────────────────────────────────────
|
|
@@ -134,53 +222,26 @@ function init({ workers, isShuttingDown }) {
|
|
|
134
222
|
_startTime = Date.now();
|
|
135
223
|
_workers = workers;
|
|
136
224
|
_isShuttingDown = isShuttingDown || (() => false);
|
|
225
|
+
_eventCount = 0;
|
|
137
226
|
}
|
|
138
227
|
|
|
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
|
-
}
|
|
228
|
+
function drawBanner(version) {}
|
|
229
|
+
|
|
230
|
+
let _interval = null;
|
|
147
231
|
|
|
148
|
-
// Show dashboard and start the 10s refresh interval
|
|
149
232
|
function start() {
|
|
150
|
-
|
|
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);
|
|
233
|
+
// No periodic refresh — dashboard re-draws on each event
|
|
162
234
|
}
|
|
163
235
|
|
|
164
|
-
|
|
165
|
-
function render() {
|
|
236
|
+
function draw() {
|
|
166
237
|
if (_isShuttingDown()) return;
|
|
167
|
-
|
|
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();
|
|
238
|
+
const info = drawHeader(require('../package.json').version);
|
|
239
|
+
drawRows(info);
|
|
179
240
|
}
|
|
180
241
|
|
|
181
242
|
function stop() {
|
|
182
243
|
if (_interval) { clearInterval(_interval); _interval = null; }
|
|
183
|
-
|
|
244
|
+
process.stdout.write(c.reset + '\n');
|
|
184
245
|
}
|
|
185
246
|
|
|
186
|
-
module.exports = { init, drawBanner, start,
|
|
247
|
+
module.exports = { init, drawBanner, start, draw, logEvent, workerColor, stop };
|