dankgrinder 8.34.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 +34 -24
- package/lib/ui.js +194 -102
- 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,7 +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
|
-
|
|
2793
|
+
ui.draw(); // draw initial table
|
|
2794
|
+
ui.logEvent(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
|
|
2787
2795
|
const BATCH_SIZE = 10;
|
|
2788
2796
|
for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
|
|
2789
2797
|
if (shutdownCalled) break;
|
|
@@ -2792,25 +2800,27 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2792
2800
|
await Promise.all(batch.map(async (acc, idx) => {
|
|
2793
2801
|
try {
|
|
2794
2802
|
if (idx > 0) await new Promise(r => setTimeout(r, 100 + Math.floor(Math.random() * 500)));
|
|
2795
|
-
console.log(` [${i + idx + 1}] starting: ${acc.label || acc.id}`);
|
|
2796
2803
|
const worker = new AccountWorker(acc, i + idx);
|
|
2797
2804
|
workers.push(worker);
|
|
2798
2805
|
workerMap.set(acc.id, worker);
|
|
2799
2806
|
await worker.start();
|
|
2800
|
-
console.log(` [${i + idx + 1}] done: ${acc.label || acc.id}`);
|
|
2801
2807
|
if (worker._tokenInvalid) {
|
|
2802
|
-
|
|
2808
|
+
worker.lastStatus = 'invalid token';
|
|
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';
|
|
2812
|
+
ui.logEvent(`${c.green}·${c.reset} [${i + idx + 1}] ${worker.username} connected`);
|
|
2813
|
+
ui.draw();
|
|
2805
2814
|
} else {
|
|
2806
|
-
|
|
2815
|
+
worker.lastStatus = 'timeout';
|
|
2816
|
+
ui.logEvent(`${c.yellow}·${c.reset} [${i + idx + 1}] ${acc.label || acc.id} — timeout`);
|
|
2807
2817
|
}
|
|
2808
2818
|
} catch (e) {
|
|
2809
|
-
|
|
2819
|
+
ui.logEvent(`${c.red}E${c.reset} [${i + idx + 1}] ERROR: ${e.message}`);
|
|
2810
2820
|
}
|
|
2811
2821
|
}));
|
|
2812
2822
|
} catch (e) {
|
|
2813
|
-
|
|
2823
|
+
ui.logEvent(`${c.red}!${c.reset} BATCH ERROR: ${e.message}`);
|
|
2814
2824
|
}
|
|
2815
2825
|
if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
|
|
2816
2826
|
hintGC();
|
|
@@ -2819,31 +2829,32 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2819
2829
|
const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
|
|
2820
2830
|
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
2821
2831
|
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
2822
|
-
|
|
2832
|
+
ui.logEvent(`${c.green}·${c.reset} Login: ${loginDone}/${accounts.length} connected`);
|
|
2823
2833
|
if (invalidWorkers.length > 0) {
|
|
2824
|
-
for (const w of invalidWorkers)
|
|
2834
|
+
for (const w of invalidWorkers) ui.logEvent(`${c.red}E${c.reset} invalid token: ${w.account.label || w.account.id}`);
|
|
2825
2835
|
}
|
|
2826
2836
|
if (timedOutWorkers.length > 0) {
|
|
2827
|
-
|
|
2837
|
+
ui.logEvent(`${c.yellow}~${c.reset} ${timedOutWorkers.length} timed out (will retry in background)`);
|
|
2828
2838
|
}
|
|
2839
|
+
ui.draw();
|
|
2829
2840
|
|
|
2830
2841
|
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
2831
2842
|
|
|
2832
2843
|
// ── Phase 2: Inventory check ────────────────────────────────────
|
|
2833
|
-
|
|
2844
|
+
ui.logEvent(`${c.dim}Checking inventory...${c.reset}`);
|
|
2834
2845
|
let invFailed = 0;
|
|
2835
2846
|
await Promise.all(activeWorkers.map(async (w) => {
|
|
2836
2847
|
try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
|
|
2837
2848
|
catch { invFailed++; }
|
|
2838
2849
|
}));
|
|
2839
2850
|
if (invFailed > 0) {
|
|
2840
|
-
|
|
2841
|
-
|
|
2851
|
+
ui.logEvent(`${c.red}!${c.reset} Inventory failed for ${invFailed} accounts`);
|
|
2852
|
+
} else {
|
|
2853
|
+
ui.logEvent(`${c.green}·${c.reset} Inventory OK`);
|
|
2842
2854
|
}
|
|
2843
|
-
console.log('Inventory check complete');
|
|
2844
2855
|
|
|
2845
2856
|
// ── Phase 2.5: Balance check ───────────────────────────────────
|
|
2846
|
-
|
|
2857
|
+
ui.logEvent(`${c.dim}Checking balances...${c.reset}`);
|
|
2847
2858
|
for (const w of activeWorkers) {
|
|
2848
2859
|
try { await w.checkBalance(true); } catch {}
|
|
2849
2860
|
}
|
|
@@ -2852,10 +2863,10 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2852
2863
|
totalCoins += w.stats?.balance || 0;
|
|
2853
2864
|
totalCoins += w.stats?.bankBalance || 0;
|
|
2854
2865
|
}
|
|
2855
|
-
|
|
2866
|
+
ui.logEvent(`${c.blue}$${c.reset} Balances: ${c.green}+⏣${totalCoins.toLocaleString()}${c.reset} across ${activeWorkers.length} accounts`);
|
|
2856
2867
|
|
|
2857
2868
|
// ── Phase 2.75: DM history check ────────────────────────────────
|
|
2858
|
-
|
|
2869
|
+
ui.logEvent(`${c.dim}Checking DM history...${c.reset}`);
|
|
2859
2870
|
let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
|
|
2860
2871
|
for (const w of activeWorkers) {
|
|
2861
2872
|
try {
|
|
@@ -2873,19 +2884,18 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2873
2884
|
}
|
|
2874
2885
|
} catch {}
|
|
2875
2886
|
}
|
|
2876
|
-
if (dmNoLs.length > 0)
|
|
2887
|
+
if (dmNoLs.length > 0) ui.logEvent(`${c.red}E${c.reset} No lifesavers: ${dmNoLs.join(', ')}`);
|
|
2877
2888
|
const parts = [];
|
|
2878
2889
|
if (dmDeaths > 0) parts.push(`${dmDeaths} deaths`);
|
|
2879
2890
|
if (dmLevelUps > 0) parts.push(`${dmLevelUps} level-ups`);
|
|
2880
|
-
|
|
2891
|
+
ui.logEvent(`${c.green}·${c.reset} DM: ${parts.length > 0 ? parts.join(', ') : c.green + 'clean' + c.reset}`);
|
|
2881
2892
|
|
|
2882
2893
|
// ── Phase 3: Start grind loops ───────────────────────────────────
|
|
2883
|
-
|
|
2894
|
+
ui.logEvent(`${c.green}·${c.reset} Starting ${activeWorkers.length} grind loops...`);
|
|
2884
2895
|
for (const w of activeWorkers) {
|
|
2885
2896
|
if (!shutdownCalled) w.grindLoop();
|
|
2886
2897
|
}
|
|
2887
|
-
ui.
|
|
2888
|
-
console.log(`${c.dim}All grind loops started. | Ctrl+C to stop${c.reset}`);
|
|
2898
|
+
ui.draw();
|
|
2889
2899
|
|
|
2890
2900
|
// Cluster heartbeat — lets other nodes see this node is alive
|
|
2891
2901
|
if (CLUSTER_ENABLED) {
|
package/lib/ui.js
CHANGED
|
@@ -1,119 +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.paused || w.dashboardPaused) return `${c.yellow}⏸${c.reset}`;
|
|
53
|
-
if (w.busy || w._invRunning || w._sellRunning) return `${c.cyan}⚙${c.reset}`;
|
|
54
|
-
if (Date.now() < w.globalCooldownUntil) return `${c.blue}⏳${c.reset}`;
|
|
55
|
-
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) + '…';
|
|
56
90
|
}
|
|
57
91
|
|
|
58
|
-
|
|
59
|
-
|
|
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));
|
|
60
97
|
const W = Math.min(process.stdout.columns || 120, 120);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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}`
|
|
70
125
|
);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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`
|
|
84
176
|
);
|
|
85
177
|
}
|
|
86
178
|
|
|
87
|
-
|
|
179
|
+
console.log(` ${hr}`);
|
|
88
180
|
|
|
89
181
|
// Totals
|
|
90
|
-
let totalCoins = 0, totalCmds = 0;
|
|
91
|
-
for (const w of
|
|
182
|
+
let totalCoins = 0, totalCmds = 0, totalOk = 0;
|
|
183
|
+
for (const w of _workers) {
|
|
92
184
|
totalCoins += w.stats.coins || 0;
|
|
93
185
|
totalCmds += w.stats.commands || 0;
|
|
186
|
+
totalOk += w.stats.successes || 0;
|
|
94
187
|
}
|
|
188
|
+
const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
|
|
95
189
|
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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}`
|
|
100
203
|
);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// Redraw in place
|
|
104
|
-
process.stdout.write(`\x1b[${_lineCount}A`);
|
|
105
|
-
process.stdout.write(lines.join('\n') + '\n');
|
|
106
|
-
_lineCount = lines.length - 1;
|
|
204
|
+
console.log('');
|
|
107
205
|
}
|
|
108
206
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
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;
|
|
117
217
|
}
|
|
118
218
|
|
|
119
219
|
// ── Public API ────────────────────────────────────────────────
|
|
@@ -122,34 +222,26 @@ function init({ workers, isShuttingDown }) {
|
|
|
122
222
|
_startTime = Date.now();
|
|
123
223
|
_workers = workers;
|
|
124
224
|
_isShuttingDown = isShuttingDown || (() => false);
|
|
225
|
+
_eventCount = 0;
|
|
125
226
|
}
|
|
126
227
|
|
|
127
|
-
function drawBanner(version) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
console.log('');
|
|
131
|
-
console.log(` ${gradient}${c.reset}`);
|
|
132
|
-
console.log(` ${DIM}Ctrl+C to stop${c.reset}`);
|
|
133
|
-
console.log('');
|
|
134
|
-
}
|
|
228
|
+
function drawBanner(version) {}
|
|
229
|
+
|
|
230
|
+
let _interval = null;
|
|
135
231
|
|
|
136
232
|
function start() {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
_render();
|
|
147
|
-
}, 10_000);
|
|
233
|
+
// No periodic refresh — dashboard re-draws on each event
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function draw() {
|
|
237
|
+
if (_isShuttingDown()) return;
|
|
238
|
+
const info = drawHeader(require('../package.json').version);
|
|
239
|
+
drawRows(info);
|
|
148
240
|
}
|
|
149
241
|
|
|
150
242
|
function stop() {
|
|
151
243
|
if (_interval) { clearInterval(_interval); _interval = null; }
|
|
152
|
-
|
|
244
|
+
process.stdout.write(c.reset + '\n');
|
|
153
245
|
}
|
|
154
246
|
|
|
155
|
-
module.exports = { init, drawBanner, start, stop };
|
|
247
|
+
module.exports = { init, drawBanner, start, draw, logEvent, workerColor, stop };
|