dankgrinder 8.37.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 -30
- package/lib/ui.js +225 -173
- 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,8 +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.
|
|
2759
|
-
ui.draw();
|
|
2789
|
+
ui.log(workers.indexOf(w), `${c.red}E${c.reset} DEATH — 0 lifesavers! Crime/search disabled`);
|
|
2760
2790
|
} else {
|
|
2761
2791
|
w.log?.('warn', `DEATH in DMs — ${event.lifesaversLeft} lifesavers remaining`);
|
|
2762
2792
|
if (prev !== event.lifesaversLeft) {
|
|
@@ -2766,8 +2796,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2766
2796
|
if (event.lifesaversLeft <= 2) {
|
|
2767
2797
|
w._alert = { type: 'lowls' };
|
|
2768
2798
|
sendWebhook?.('LOW LIFESAVERS', `**${w.username}** has only **${event.lifesaversLeft}** lifesaver(s) left!`, 0xfbbf24);
|
|
2769
|
-
ui.
|
|
2770
|
-
ui.draw();
|
|
2799
|
+
ui.log(workers.indexOf(w), `⚠ ${event.lifesaversLeft} lifesavers left!`);
|
|
2771
2800
|
}
|
|
2772
2801
|
}
|
|
2773
2802
|
}
|
|
@@ -2776,7 +2805,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2776
2805
|
if (event.type === 'levelup') {
|
|
2777
2806
|
if (event.to > 0) {
|
|
2778
2807
|
w._level = event.to;
|
|
2779
|
-
ui.
|
|
2808
|
+
ui.log(workers.indexOf(w), `↑ level ${event.to}`);
|
|
2780
2809
|
}
|
|
2781
2810
|
}
|
|
2782
2811
|
}
|
|
@@ -2790,8 +2819,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2790
2819
|
const LOGIN_GAP_MAX_MS = Number.isFinite(parsedGapMax) && parsedGapMax >= LOGIN_GAP_MIN_MS ? parsedGapMax : Math.max(parsedGapMin, 150);
|
|
2791
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));
|
|
2792
2821
|
|
|
2793
|
-
ui.
|
|
2794
|
-
ui.logEvent(`${c.dim}Logging in ${accounts.length} accounts...${c.reset}`);
|
|
2822
|
+
ui.log(-1, `Logging in ${accounts.length} accounts...`);
|
|
2795
2823
|
const BATCH_SIZE = 10;
|
|
2796
2824
|
for (let i = 0; i < accounts.length; i += BATCH_SIZE) {
|
|
2797
2825
|
if (shutdownCalled) break;
|
|
@@ -2806,21 +2834,20 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2806
2834
|
await worker.start();
|
|
2807
2835
|
if (worker._tokenInvalid) {
|
|
2808
2836
|
worker.lastStatus = 'invalid token';
|
|
2809
|
-
ui.
|
|
2837
|
+
ui.log(worker.idx, `✗ invalid token`);
|
|
2810
2838
|
} else if (worker.channel) {
|
|
2811
2839
|
worker.lastStatus = 'ready';
|
|
2812
|
-
ui.
|
|
2813
|
-
ui.draw();
|
|
2840
|
+
ui.log(worker.idx, `✓ connected`);
|
|
2814
2841
|
} else {
|
|
2815
2842
|
worker.lastStatus = 'timeout';
|
|
2816
|
-
ui.
|
|
2843
|
+
ui.log(worker.idx, `✗ timeout`);
|
|
2817
2844
|
}
|
|
2818
2845
|
} catch (e) {
|
|
2819
|
-
ui.
|
|
2846
|
+
ui.log(i + idx, `ERROR: ${e.message}`);
|
|
2820
2847
|
}
|
|
2821
2848
|
}));
|
|
2822
2849
|
} catch (e) {
|
|
2823
|
-
ui.
|
|
2850
|
+
ui.log(-1, `BATCH ERROR: ${e.message}`);
|
|
2824
2851
|
}
|
|
2825
2852
|
if (i + BATCH_SIZE < accounts.length) await new Promise(r => setTimeout(r, randomLoginGap()));
|
|
2826
2853
|
hintGC();
|
|
@@ -2829,32 +2856,32 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2829
2856
|
const loginDone = workers.filter(w => !w._tokenInvalid && w.channel).length;
|
|
2830
2857
|
const invalidWorkers = workers.filter(w => w._tokenInvalid);
|
|
2831
2858
|
const timedOutWorkers = workers.filter(w => !w.channel && !w._tokenInvalid);
|
|
2832
|
-
ui.
|
|
2859
|
+
ui.log(-1, `Login: ${loginDone}/${accounts.length} connected`);
|
|
2833
2860
|
if (invalidWorkers.length > 0) {
|
|
2834
|
-
for (const w of invalidWorkers) ui.
|
|
2861
|
+
for (const w of invalidWorkers) ui.log(workers.indexOf(w), `✗ invalid token`);
|
|
2835
2862
|
}
|
|
2836
2863
|
if (timedOutWorkers.length > 0) {
|
|
2837
|
-
ui.
|
|
2864
|
+
ui.log(-1, `${timedOutWorkers.length} timed out (retrying in background)`);
|
|
2838
2865
|
}
|
|
2839
2866
|
ui.draw();
|
|
2840
2867
|
|
|
2841
2868
|
const activeWorkers = workers.filter(w => !w._tokenInvalid);
|
|
2842
2869
|
|
|
2843
2870
|
// ── Phase 2: Inventory check ────────────────────────────────────
|
|
2844
|
-
ui.
|
|
2871
|
+
ui.log(-1, `Checking inventory...`);
|
|
2845
2872
|
let invFailed = 0;
|
|
2846
2873
|
await Promise.all(activeWorkers.map(async (w) => {
|
|
2847
2874
|
try { await w.checkInventory({ force: true, requireComplete: true, maxAttempts: 3, silent: true }); }
|
|
2848
2875
|
catch { invFailed++; }
|
|
2849
2876
|
}));
|
|
2850
2877
|
if (invFailed > 0) {
|
|
2851
|
-
ui.
|
|
2878
|
+
ui.log(-1, `Inventory failed for ${invFailed} accounts`);
|
|
2852
2879
|
} else {
|
|
2853
|
-
ui.
|
|
2880
|
+
ui.log(-1, `Inventory OK`);
|
|
2854
2881
|
}
|
|
2855
2882
|
|
|
2856
2883
|
// ── Phase 2.5: Balance check ───────────────────────────────────
|
|
2857
|
-
ui.
|
|
2884
|
+
ui.log(-1, `Checking balances...`);
|
|
2858
2885
|
for (const w of activeWorkers) {
|
|
2859
2886
|
try { await w.checkBalance(true); } catch {}
|
|
2860
2887
|
}
|
|
@@ -2863,10 +2890,10 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2863
2890
|
totalCoins += w.stats?.balance || 0;
|
|
2864
2891
|
totalCoins += w.stats?.bankBalance || 0;
|
|
2865
2892
|
}
|
|
2866
|
-
ui.
|
|
2893
|
+
ui.log(-1, `Balance: +⏣${totalCoins.toLocaleString()} across ${activeWorkers.length} accounts`);
|
|
2867
2894
|
|
|
2868
2895
|
// ── Phase 2.75: DM history check ────────────────────────────────
|
|
2869
|
-
ui.
|
|
2896
|
+
ui.log(-1, `Checking DM history...`);
|
|
2870
2897
|
let dmDeaths = 0, dmLevelUps = 0, dmNoLs = [];
|
|
2871
2898
|
for (const w of activeWorkers) {
|
|
2872
2899
|
try {
|
|
@@ -2884,14 +2911,14 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2884
2911
|
}
|
|
2885
2912
|
} catch {}
|
|
2886
2913
|
}
|
|
2887
|
-
if (dmNoLs.length > 0) ui.
|
|
2914
|
+
if (dmNoLs.length > 0) ui.log(-1, `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
|
|
2888
2915
|
const parts = [];
|
|
2889
2916
|
if (dmDeaths > 0) parts.push(`${dmDeaths} deaths`);
|
|
2890
2917
|
if (dmLevelUps > 0) parts.push(`${dmLevelUps} level-ups`);
|
|
2891
|
-
ui.
|
|
2918
|
+
ui.log(-1, `DM: ${parts.length > 0 ? parts.join(', ') : 'clean'}`);
|
|
2892
2919
|
|
|
2893
2920
|
// ── Phase 3: Start grind loops ───────────────────────────────────
|
|
2894
|
-
ui.
|
|
2921
|
+
ui.log(-1, `Starting ${activeWorkers.length} grind loops...`);
|
|
2895
2922
|
for (const w of activeWorkers) {
|
|
2896
2923
|
if (!shutdownCalled) w.grindLoop();
|
|
2897
2924
|
}
|
package/lib/ui.js
CHANGED
|
@@ -1,30 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI Live Dashboard —
|
|
3
|
-
*
|
|
4
|
-
* Each account has its own color. Events logged below.
|
|
2
|
+
* CLI Live Dashboard — box design, loading animations, per-account status.
|
|
3
|
+
* Everything inside a single box. Events stream below. No duplicate output.
|
|
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
|
+
// ── Spinner frames ────────────────────────────────────────────
|
|
12
|
+
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
13
|
+
function spinnerFrame() { return SPINNER[Math.floor(Date.now() / 150) % SPINNER.length]; }
|
|
14
|
+
|
|
15
|
+
// ── Big ASCII art banner ──────────────────────────────────────
|
|
16
|
+
const BANNER_LINES = [
|
|
17
|
+
' ██████╗ ██╗ ██╗███╗ ██╗ ██████╗ ███████╗ ██████╗ ███╗ ██╗',
|
|
18
|
+
' ██╔══██╗██║ ██║████╗ ██║██╔════╝ ██╔════╝██╔═══██╗████╗ ██║',
|
|
19
|
+
' ██║ ██║██║ ██║██╔██╗ ██║██║ ███╗█████╗ ██║ ██║██╔██╗ ██║',
|
|
20
|
+
' ██║ ██║██║ ██║██║╚██╗██║██║ ██║██╔══╝ ██║ ██║██║╚██╗██║',
|
|
21
|
+
' ██████╔╝╚██████╔╝██║ ╚████║╚██████╔╝███████╗╚██████╔╝██║ ╚████║',
|
|
22
|
+
' ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝',
|
|
23
|
+
' ██╗██╗ ██╗███╗ ██╗ ██████╗ ██╗ ███████╗██████╗ ',
|
|
24
|
+
' ██║██║ ██║████╗ ██║██╔═══██╗██║ ██╔════╝██╔══██╗',
|
|
25
|
+
' ███████╗███████╗ ██║██║ ██║██╔██╗ ██║██║ ██║██║ █████╗ ██████╔╝',
|
|
26
|
+
' ╚════██║╚════██║ ██║██║ ██║██║╚██╗██║██║ ██║██║ ██╔══╝ ██╔══██╗',
|
|
27
|
+
' ██║ ██║ ██║╚██████╔╝██║ ╚████║╚██████╔╝███████╗███████╗██║ ██║',
|
|
28
|
+
' ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝',
|
|
29
|
+
];
|
|
10
30
|
|
|
11
|
-
|
|
31
|
+
function gradientLine(text, r1, g1, b1, r2, g2, b2) {
|
|
32
|
+
let out = '';
|
|
33
|
+
for (let i = 0; i < text.length; i++) {
|
|
34
|
+
const t = text.length <= 1 ? 0 : i / (text.length - 1);
|
|
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
|
+
}
|
|
37
|
+
return out + '\x1b[0m';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Palette ───────────────────────────────────────────────────
|
|
12
41
|
const PALETTE = [
|
|
13
|
-
'\x1b[38;2;77;212;238m',
|
|
14
|
-
'\x1b[38;2;255;
|
|
15
|
-
'\x1b[38;2;130;
|
|
16
|
-
'\x1b[38;2;255;
|
|
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
|
|
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',
|
|
25
46
|
];
|
|
26
|
-
|
|
27
|
-
function workerColor(idx) { return PALETTE[idx % PALETTE.length]; }
|
|
47
|
+
function wc(idx) { return PALETTE[idx % PALETTE.length]; }
|
|
28
48
|
|
|
29
49
|
// ── ANSI helpers ──────────────────────────────────────────────
|
|
30
50
|
const c = {
|
|
@@ -34,12 +54,18 @@ const c = {
|
|
|
34
54
|
yellow: '\x1b[38;2;255;220;80m',
|
|
35
55
|
cyan: '\x1b[38;2;80;220;255m',
|
|
36
56
|
blue: '\x1b[38;2;100;160;255m',
|
|
37
|
-
gray: '\x1b[38;2;120;130;150m',
|
|
38
|
-
white: '\x1b[38;2;220;220;240m',
|
|
39
57
|
};
|
|
40
58
|
const DIM = c.dim;
|
|
41
59
|
|
|
42
|
-
|
|
60
|
+
function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
|
|
61
|
+
function padR(s, n) { return trunc(s, n).padEnd(n); }
|
|
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
|
+
}
|
|
68
|
+
|
|
43
69
|
function fmtUptime() {
|
|
44
70
|
const s = Math.floor((Date.now() - _startTime) / 1000);
|
|
45
71
|
if (s < 60) return `${s}s`;
|
|
@@ -51,134 +77,134 @@ function fmtUptime() {
|
|
|
51
77
|
return `${m}m`;
|
|
52
78
|
}
|
|
53
79
|
|
|
54
|
-
// ──
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
};
|
|
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;
|
|
71
87
|
}
|
|
72
88
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
if (w.
|
|
77
|
-
if (w._alert)
|
|
78
|
-
|
|
79
|
-
|
|
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';
|
|
80
99
|
}
|
|
81
|
-
|
|
82
|
-
return `${c.green}·${c.reset}`;
|
|
100
|
+
return '● READY';
|
|
83
101
|
}
|
|
84
102
|
|
|
85
|
-
// ──
|
|
86
|
-
function
|
|
87
|
-
str = String(str || '');
|
|
88
|
-
if (str.length <= len) return str;
|
|
89
|
-
return str.substring(0, len - 1) + '…';
|
|
90
|
-
}
|
|
91
|
-
|
|
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));
|
|
103
|
+
// ── Layout ────────────────────────────────────────────────────
|
|
104
|
+
function layout() {
|
|
97
105
|
const W = Math.min(process.stdout.columns || 120, 120);
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
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}`
|
|
125
|
-
);
|
|
126
|
-
console.log(` ${hr}`);
|
|
127
|
-
|
|
128
|
-
return { visible, W, cols };
|
|
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 };
|
|
129
115
|
}
|
|
130
116
|
|
|
131
|
-
// ──
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
}
|
|
117
|
+
// ── Draw the full box ────────────────────────────────────────
|
|
118
|
+
function draw() {
|
|
119
|
+
const { W, bannerH, statusH, headerH, totalsH, footerH, maxAccounts, eventH } = layout();
|
|
120
|
+
const T = '─'.repeat(W - 2); // inner width
|
|
149
121
|
|
|
150
|
-
// ──
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const sorted = sortedWorkers(visible);
|
|
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`);
|
|
154
125
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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`);
|
|
159
131
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
132
|
+
// ── Status bar inside box ──
|
|
133
|
+
const running = _workers.filter(w => w.running && w.channel && !w.paused && !w.dashboardPaused).length;
|
|
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`);
|
|
148
|
+
|
|
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;
|
|
160
|
+
|
|
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);
|
|
181
|
+
|
|
182
|
+
for (let si = 0; si < shown.length; si++) {
|
|
183
|
+
const w = shown[si];
|
|
184
|
+
const wi = _workers.indexOf(w);
|
|
185
|
+
const col2 = wc(wi);
|
|
186
|
+
const stCol = statusColor(w);
|
|
187
|
+
const stText = statusText(w);
|
|
188
|
+
const earned = w.stats.coins > 0 ? `${c.green}+${w.stats.coins.toLocaleString()}${c.reset}` : DIM + '—' + c.reset;
|
|
163
189
|
const cmds = w.stats.commands || 0;
|
|
164
|
-
const rate = w.stats.commands > 0
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
process.stdout.write(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
`${earned.padStart(cols.earned)}\n`
|
|
176
|
-
);
|
|
190
|
+
const rate = w.stats.commands > 0 ? Math.round((w.stats.successes / w.stats.commands) * 100) : 0;
|
|
191
|
+
|
|
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`);
|
|
177
201
|
}
|
|
178
202
|
|
|
179
|
-
|
|
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
|
+
}
|
|
180
206
|
|
|
181
|
-
// Totals
|
|
207
|
+
// ── Totals ──
|
|
182
208
|
let totalCoins = 0, totalCmds = 0, totalOk = 0;
|
|
183
209
|
for (const w of _workers) {
|
|
184
210
|
totalCoins += w.stats.coins || 0;
|
|
@@ -187,61 +213,87 @@ function drawRows({ visible, W, cols }) {
|
|
|
187
213
|
}
|
|
188
214
|
const rate = totalCmds > 0 ? Math.round((totalOk / totalCmds) * 100) : 0;
|
|
189
215
|
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
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}`
|
|
203
|
-
);
|
|
204
|
-
console.log('');
|
|
205
|
-
}
|
|
206
216
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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`);
|
|
228
|
+
|
|
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
|
+
}
|
|
210
244
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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`);
|
|
217
261
|
}
|
|
218
262
|
|
|
263
|
+
// ── Event tracking ────────────────────────────────────────────
|
|
264
|
+
let _eventLines = []; // [accountIdx] = [{text, ts}]
|
|
265
|
+
const MAX_EVENTS = 3;
|
|
266
|
+
|
|
219
267
|
// ── Public API ────────────────────────────────────────────────
|
|
220
268
|
|
|
221
269
|
function init({ workers, isShuttingDown }) {
|
|
222
270
|
_startTime = Date.now();
|
|
223
271
|
_workers = workers;
|
|
224
272
|
_isShuttingDown = isShuttingDown || (() => false);
|
|
225
|
-
|
|
273
|
+
_version = '0.0.0';
|
|
274
|
+
_eventLines = [];
|
|
226
275
|
}
|
|
227
276
|
|
|
228
|
-
function drawBanner(version) {}
|
|
277
|
+
function drawBanner(version) { _version = version; }
|
|
278
|
+
function start() {}
|
|
279
|
+
function stop() { process.stdout.write(c.reset + '\n'); }
|
|
229
280
|
|
|
230
|
-
|
|
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')}`;
|
|
231
284
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
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
|
+
}
|
|
235
290
|
|
|
236
|
-
|
|
237
|
-
if (_isShuttingDown()) return;
|
|
238
|
-
const info = drawHeader(require('../package.json').version);
|
|
239
|
-
drawRows(info);
|
|
291
|
+
draw();
|
|
240
292
|
}
|
|
241
293
|
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
|
|
294
|
+
function logGlobal(msg) {
|
|
295
|
+
// Global events go to account 0
|
|
296
|
+
log(-1, msg);
|
|
245
297
|
}
|
|
246
298
|
|
|
247
|
-
module.exports = { init, drawBanner, start, draw,
|
|
299
|
+
module.exports = { init, drawBanner, start, draw, log, logGlobal, workerColor: wc, stop };
|