dankgrinder 7.69.0 → 7.72.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/dashboard.js +4 -269
- package/lib/grinder.js +45 -251
- package/lib/rawLogger.js +4 -1
- package/package.json +1 -1
package/lib/dashboard.js
CHANGED
|
@@ -1,271 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* Every render writes N rows from line 1 onward. Previous content is overwritten.
|
|
6
|
-
*
|
|
7
|
-
* Context:
|
|
8
|
-
* workers, dashboardStarted, dashboardRendering, dashboardLines,
|
|
9
|
-
* totalBalance, totalCoins, totalCommands, startTime,
|
|
10
|
-
* sessionPeakCoins, isNewHigh, recentLogs, globalCmdRate,
|
|
11
|
-
* CLOUD_MODE, CLUSTER_ENABLED, PKG_VERSION,
|
|
12
|
-
* AccountWorker, PULSE_CHARS, getSpinner, gradientText,
|
|
13
|
-
* rgb, c
|
|
2
|
+
* CLI Dashboard — REMOVED
|
|
3
|
+
* Dashboard functionality has been removed from the CLI.
|
|
4
|
+
* All startup output goes through simple console.log.
|
|
14
5
|
*/
|
|
15
|
-
|
|
16
|
-
'use strict';
|
|
17
|
-
|
|
18
|
-
// Local refs (set per render)
|
|
19
|
-
let _c, _rgb, _spinnerFn, _gradientFn;
|
|
20
|
-
|
|
21
|
-
function init(ctx) {
|
|
22
|
-
_c = ctx.c;
|
|
23
|
-
_rgb = ctx.rgb;
|
|
24
|
-
_spinnerFn = ctx.getSpinner;
|
|
25
|
-
_gradientFn = ctx.gradientText;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ── Helpers ─────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
function vis(s) {
|
|
31
|
-
return String(s).replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function pad(str, width) {
|
|
35
|
-
return str + ' '.repeat(Math.max(0, width - vis(str)));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function clearLine(content) {
|
|
39
|
-
return `${_c.clearLine}\r${content}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function fmtCoins(n) {
|
|
43
|
-
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
|
44
|
-
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
45
|
-
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
46
|
-
return n.toLocaleString();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function fmtUptime(startTime) {
|
|
50
|
-
const s = Math.floor((Date.now() - startTime) / 1000);
|
|
51
|
-
const h = Math.floor(s / 3600);
|
|
52
|
-
const m = Math.floor((s % 3600) / 60);
|
|
53
|
-
const sec = s % 60;
|
|
54
|
-
if (h > 0) return `${h}h ${m}m ${sec}s`;
|
|
55
|
-
if (m > 0) return `${m}m ${sec}s`;
|
|
56
|
-
return `${sec}s`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ── Render ──────────────────────────────────────────────────────
|
|
60
|
-
|
|
61
|
-
function renderDashboard(ctx) {
|
|
62
|
-
if (!ctx.dashboardStarted || ctx.workers.length === 0 || ctx.dashboardRendering) return;
|
|
63
|
-
|
|
64
|
-
init(ctx);
|
|
65
|
-
|
|
66
|
-
const tw = Math.max(process.stdout.columns || 80, 60);
|
|
67
|
-
const _ = _c.reset;
|
|
68
|
-
|
|
69
|
-
// Color shortcuts
|
|
70
|
-
const A = _rgb(139, 92, 246);
|
|
71
|
-
const G = _rgb(52, 211, 153);
|
|
72
|
-
const Au = _rgb(255, 215, 0);
|
|
73
|
-
const O = _rgb(251, 146, 60);
|
|
74
|
-
const Cy = _rgb(34, 211, 238);
|
|
75
|
-
const R = _rgb(239, 68, 68);
|
|
76
|
-
const Y = _rgb(251, 191, 36);
|
|
77
|
-
const W = _c.white;
|
|
78
|
-
const D = _c.dim;
|
|
79
|
-
|
|
80
|
-
// ── Aggregate stats ───────────────────────────────────────────
|
|
81
|
-
let aggBalance = 0, aggCoins = 0, aggCmds = 0, aggErrors = 0;
|
|
82
|
-
for (const w of ctx.workers) {
|
|
83
|
-
aggBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
|
|
84
|
-
aggCoins += w.stats.coins || 0;
|
|
85
|
-
aggCmds += w.stats.commands || 0;
|
|
86
|
-
aggErrors += w.stats.errors || 0;
|
|
87
|
-
}
|
|
88
|
-
const successRate = aggCmds > 0 ? Math.round(((aggCmds - aggErrors) / aggCmds) * 100) : 100;
|
|
89
|
-
const elapsedHrs = (Date.now() - ctx.startTime) / 3_600_000;
|
|
90
|
-
const perHr = elapsedHrs > 0.01 ? Math.round(aggCoins / elapsedHrs) : 0;
|
|
91
|
-
const cpmVal = ctx.globalCmdRate.getRate().toFixed(1);
|
|
92
|
-
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
93
|
-
const spin = _spinnerFn('braille');
|
|
94
|
-
|
|
95
|
-
const activeCount = ctx.workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
|
|
96
|
-
const invalidCount = ctx.workers.filter(w => w._tokenInvalid).length;
|
|
97
|
-
const pausedCount = ctx.workers.filter(w => w.paused || w.dashboardPaused).length;
|
|
98
|
-
const recovCount = ctx.workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
|
|
99
|
-
|
|
100
|
-
const modeTag = ctx.CLOUD_MODE ? `${Cy}CLOUD${_}` : (ctx.CLUSTER_ENABLED ? `${Cy}CLUSTER${_}` : `${D}local${_}`);
|
|
101
|
-
|
|
102
|
-
// ── Column layout ─────────────────────────────────────────────
|
|
103
|
-
const colNum = 4;
|
|
104
|
-
const colSts = 3;
|
|
105
|
-
const colName = Math.max(16, Math.min(22, Math.floor(tw * 0.22)));
|
|
106
|
-
const colBal = 10;
|
|
107
|
-
const colLvl = 5;
|
|
108
|
-
const colLs = 4;
|
|
109
|
-
const colEarn = 9;
|
|
110
|
-
const colAct = Math.max(8, tw - colNum - colSts - colName - colBal - colLvl - colLs - colEarn - 12);
|
|
111
|
-
const gap = ' ';
|
|
112
|
-
|
|
113
|
-
// ── Helpers to build rows ─────────────────────────────────────
|
|
114
|
-
const border = (char) => `${A}${char}${D}${'─'.repeat(tw - 2)}${A}${char}${_}`;
|
|
115
|
-
|
|
116
|
-
function mkRow(content, leftPad = ' ') {
|
|
117
|
-
const raw = content.replace(/\x1b\[[0-9;]*m/g, '');
|
|
118
|
-
const padLen = Math.max(0, tw - 4 - raw.length);
|
|
119
|
-
return `${A}│${_}${leftPad}${content}${' '.repeat(padLen)}${leftPad}${A}│${_}`;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ── Build all rows ────────────────────────────────────────────
|
|
123
|
-
const rows = [];
|
|
124
|
-
|
|
125
|
-
// HEADER
|
|
126
|
-
const title = _c.bold + _gradientFn(' DANK GRINDER ', [139, 92, 246], [52, 211, 153]) + _;
|
|
127
|
-
const h1 = [
|
|
128
|
-
title,
|
|
129
|
-
`${D}v${ctx.PKG_VERSION}${_}`,
|
|
130
|
-
`${G}${spin}${_}`,
|
|
131
|
-
`${D}up${_} ${W}${fmtUptime(ctx.startTime)}${_}`,
|
|
132
|
-
`${D}bal${_} ${Au}⏣${_}${W}${fmtCoins(aggBalance)}${_}`,
|
|
133
|
-
`${G}+⏣${fmtCoins(perHr)}${_}/h`,
|
|
134
|
-
`${G}${activeCount}${_}/${W}${ctx.workers.length}${_} active`,
|
|
135
|
-
invalidCount > 0 ? ` ${R}${invalidCount} inv${_}` : '',
|
|
136
|
-
pausedCount > 0 ? ` ${Y}${pausedCount} pause${_}` : '',
|
|
137
|
-
recovCount > 0 ? ` ${O}${recovCount} recov${_}` : '',
|
|
138
|
-
` ${D}${aggCmds}${_}cmds`,
|
|
139
|
-
` ${D}${cpmVal}${_}/min`,
|
|
140
|
-
` ${D}${memMB}MB`,
|
|
141
|
-
modeTag,
|
|
142
|
-
].filter(Boolean).join(' ');
|
|
143
|
-
|
|
144
|
-
rows.push(border('╔'));
|
|
145
|
-
rows.push(mkRow(h1));
|
|
146
|
-
rows.push(border('╠'));
|
|
147
|
-
rows.push(mkRow(`${D}session${_} ${G}${fmtCoins(aggCoins)}${_} coins ${G}${successRate}%${_} success ${W}${fmtCoins(ctx.sessionPeakCoins)}${_} peak${ctx.isNewHigh ? ` ${Au}★ NEW HIGH${_}` : ''}`));
|
|
148
|
-
rows.push(border('╚'));
|
|
149
|
-
|
|
150
|
-
// ACCOUNTS TABLE
|
|
151
|
-
const headers = [
|
|
152
|
-
`${D}${pad('#', colNum)}${_}`,
|
|
153
|
-
`${D}${pad('S', colSts)}${_}`,
|
|
154
|
-
`${_gradientFn(pad('Account', colName), [139, 92, 246], [96, 165, 250])}${_}`,
|
|
155
|
-
`${D}${pad('Balance', colBal)}${_}`,
|
|
156
|
-
`${D}${pad('Lvl', colLvl)}${_}`,
|
|
157
|
-
`${D}${pad('LS', colLs)}${_}`,
|
|
158
|
-
`${D}${pad('Earned', colEarn)}${_}`,
|
|
159
|
-
`${D}${pad('Activity', colAct)}${_}`,
|
|
160
|
-
].join(gap);
|
|
161
|
-
|
|
162
|
-
rows.push(border('╔'));
|
|
163
|
-
rows.push(mkRow(headers));
|
|
164
|
-
rows.push(border('╟'));
|
|
165
|
-
|
|
166
|
-
const sorted = [...ctx.workers].sort((a, b) => a.idx - b.idx);
|
|
167
|
-
const maxRows = Math.max(5, Math.min(sorted.length, Math.floor((process.stdout.rows || 24) - 12)));
|
|
168
|
-
const visible = sorted.slice(0, maxRows);
|
|
169
|
-
const extraRows = sorted.length - maxRows;
|
|
170
|
-
|
|
171
|
-
for (const wk of visible) {
|
|
172
|
-
const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
|
|
173
|
-
|
|
174
|
-
let stsIcon;
|
|
175
|
-
if (wk._tokenInvalid) stsIcon = `${R}✗${_}`;
|
|
176
|
-
else if (!wk.running) stsIcon = `${D}○${_}`;
|
|
177
|
-
else if (isRecov) stsIcon = `${O}${_spinnerFn('braille').substring(0, 1)}${_}`;
|
|
178
|
-
else if (wk.paused) stsIcon = `${R}⏸${_}`;
|
|
179
|
-
else if (wk.dashboardPaused) stsIcon = `${Y}⏸${_}`;
|
|
180
|
-
else if (wk.busy) stsIcon = `${G}${_spinnerFn('pulse').substring(0, 1)}${_}`;
|
|
181
|
-
else stsIcon = `${G}●${_}`;
|
|
182
|
-
|
|
183
|
-
const name = (wk.username || wk.account.label || '?').substring(0, colName);
|
|
184
|
-
const nameStr = `${wk.color}${name}${_}`;
|
|
185
|
-
const balStr = wk.stats.balance > 0 ? `${Au}⏣${_}${W}${fmtCoins(wk.stats.balance)}${_}` : `${D}⏣-${_}`;
|
|
186
|
-
const lvl = wk._level || 0;
|
|
187
|
-
const lvlStr = lvl > 0 ? `${Cy}L${lvl}${_}` : `${D}L???${_}`;
|
|
188
|
-
const ls = wk._lifesavers;
|
|
189
|
-
let lsStr;
|
|
190
|
-
if (ls === 0) lsStr = `${R}♥${ls}${_}`;
|
|
191
|
-
else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${_}`;
|
|
192
|
-
else if (ls != null) lsStr = `${G}♥${ls}${_}`;
|
|
193
|
-
else {
|
|
194
|
-
const p = ctx.PULSE_CHARS[Math.floor(Date.now() / 400) % ctx.PULSE_CHARS.length];
|
|
195
|
-
lsStr = `${D}${p}♥?${_}`;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const earn = wk.stats.coins || 0;
|
|
199
|
-
const earnStr = earn > 0 ? `${G}+${fmtCoins(earn)}${_}` : `${D}────${_}`;
|
|
200
|
-
const actRaw = (wk.lastStatus || 'ready').replace(/\x1b\[[0-9;]*m/g, '').substring(0, colAct);
|
|
201
|
-
const actStr = `${D}${pad(actRaw, colAct)}${_}`;
|
|
202
|
-
const numStr = `${D}${pad(String(wk.idx + 1), colNum)}${_}`;
|
|
203
|
-
|
|
204
|
-
const rowStr = [
|
|
205
|
-
numStr,
|
|
206
|
-
pad(stsIcon, colSts),
|
|
207
|
-
pad(nameStr, colName),
|
|
208
|
-
pad(balStr, colBal),
|
|
209
|
-
pad(lvlStr, colLvl),
|
|
210
|
-
pad(lsStr, colLs),
|
|
211
|
-
pad(earnStr, colEarn),
|
|
212
|
-
actStr,
|
|
213
|
-
].join(gap);
|
|
214
|
-
|
|
215
|
-
rows.push(mkRow(rowStr));
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
if (extraRows > 0) {
|
|
219
|
-
rows.push(mkRow(`${D}+${extraRows} more${_}`));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
rows.push(border('╚'));
|
|
223
|
-
|
|
224
|
-
// LIVE FEED
|
|
225
|
-
const logs = ctx.recentLogs.toArray();
|
|
226
|
-
if (logs.length > 0) {
|
|
227
|
-
const feedTitle = `${_gradientFn(' LIVE FEED ', [139, 92, 246], [52, 211, 153])}${_} ${G}${_spinnerFn('pulse')}${_}`;
|
|
228
|
-
rows.push(border('╔'));
|
|
229
|
-
rows.push(mkRow(feedTitle));
|
|
230
|
-
rows.push(border('╟'));
|
|
231
|
-
|
|
232
|
-
for (const entry of logs) {
|
|
233
|
-
let lineText;
|
|
234
|
-
if (typeof entry === 'string') {
|
|
235
|
-
lineText = entry;
|
|
236
|
-
} else if (entry && typeof entry === 'object') {
|
|
237
|
-
const ts = entry.ts ? new Date(entry.ts).toLocaleTimeString('en-US', { hour12: false }) : '';
|
|
238
|
-
const user = entry.username ? String(entry.username) : '';
|
|
239
|
-
const cmd = entry.command ? `[${entry.command}]` : '';
|
|
240
|
-
const resp = entry.response || '';
|
|
241
|
-
lineText = `${D}${ts}${_} ${entry.color || D}${user}${_} ${D}${cmd}${_} ${resp}`;
|
|
242
|
-
} else {
|
|
243
|
-
lineText = String(entry);
|
|
244
|
-
}
|
|
245
|
-
rows.push(mkRow(`${D}${lineText}${_}`));
|
|
246
|
-
}
|
|
247
|
-
rows.push(border('╚'));
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// FOOTER
|
|
251
|
-
rows.push(border('╔'));
|
|
252
|
-
rows.push(mkRow(`${modeTag} ${D}P=pause R=resume S=status Q=quit${_}`));
|
|
253
|
-
rows.push(border('╚'));
|
|
254
|
-
|
|
255
|
-
// ── Flush: overwrite rows from line 1, then blank remaining screen ──
|
|
256
|
-
const rowsOnScreen = process.stdout.rows || 24;
|
|
257
|
-
for (const row of rows) {
|
|
258
|
-
process.stdout.write(`${_c.clearLine}\r${row}\n`);
|
|
259
|
-
}
|
|
260
|
-
// Fill remaining screen lines with blanks so old startup logs don't bleed through
|
|
261
|
-
const extra = rowsOnScreen - rows.length;
|
|
262
|
-
for (let i = 0; i < extra; i++) {
|
|
263
|
-
process.stdout.write(`${_c.clearLine}\r${' '.repeat(tw)}\n`);
|
|
264
|
-
}
|
|
265
|
-
// Move cursor to end so terminal doesn't scroll on next \r write
|
|
266
|
-
process.stdout.write(`\x1b[${rows.length};1H`);
|
|
267
|
-
|
|
268
|
-
return rows.length;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
module.exports = { renderDashboard };
|
|
6
|
+
module.exports = {};
|
package/lib/grinder.js
CHANGED
|
@@ -3,7 +3,6 @@ const Redis = require('ioredis');
|
|
|
3
3
|
const commands = require('./commands');
|
|
4
4
|
const { setDashboardActive, isCV2, ensureCV2, stripAnsi } = require('./commands/utils');
|
|
5
5
|
const rawLogger = require('./rawLogger');
|
|
6
|
-
const { renderDashboard: renderDashboardImpl } = require('./dashboard');
|
|
7
6
|
const {
|
|
8
7
|
BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
|
|
9
8
|
AhoCorasick, LRUCache, StringPool, AsyncBatchQueue, JitterBackoff,
|
|
@@ -133,10 +132,25 @@ const CLUSTER_PREFIX = 'dkg:cluster:';
|
|
|
133
132
|
function initRedis() {
|
|
134
133
|
if (!redis && REDIS_URL) {
|
|
135
134
|
try {
|
|
136
|
-
redis = new Redis(REDIS_URL, {
|
|
137
|
-
|
|
135
|
+
redis = new Redis(REDIS_URL, {
|
|
136
|
+
maxRetriesPerRequest: 3,
|
|
137
|
+
retryStrategy: (times) => times > 5 ? null : Math.min(times * 500, 3000),
|
|
138
|
+
lazyConnect: true,
|
|
139
|
+
});
|
|
140
|
+
redis.connect().catch((e) => {
|
|
141
|
+
// Only warn once — don't spam on persistent connection failures
|
|
142
|
+
if (!redis || redis.status === 'wait') {
|
|
143
|
+
console.warn(`[Redis] connection failed: ${e.message} — continuing without Redis`);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
redis.on('error', (e) => {
|
|
147
|
+
// Suppress common transient errors from spamming stderr
|
|
148
|
+
const msg = e?.message || '';
|
|
149
|
+
if (msg.includes('ETIMEDOUT') || msg.includes('ECONNRESET') || msg.includes('ENOTFOUND') || msg.includes('connect')) return;
|
|
150
|
+
console.error(`[Redis] error: ${msg}`);
|
|
151
|
+
});
|
|
138
152
|
} catch (e) {
|
|
139
|
-
|
|
153
|
+
// Redis optional — continue without it
|
|
140
154
|
}
|
|
141
155
|
}
|
|
142
156
|
}
|
|
@@ -321,123 +335,17 @@ function colorBanner() {
|
|
|
321
335
|
return out;
|
|
322
336
|
}
|
|
323
337
|
|
|
324
|
-
// ──
|
|
325
|
-
let dashboardLines = 0;
|
|
326
|
-
let dashboardStarted = false;
|
|
327
|
-
let dashboardRendering = false;
|
|
328
|
-
let lastRenderTime = 0;
|
|
329
|
-
let renderPending = false;
|
|
330
|
-
let totalBalance = 0;
|
|
331
|
-
let totalCoins = 0;
|
|
332
|
-
let totalCommands = 0;
|
|
333
|
-
let startTime = Date.now();
|
|
334
|
-
let shutdownCalled = false;
|
|
335
|
-
let sessionPeakCoins = 0;
|
|
336
|
-
let isNewHigh = false;
|
|
337
|
-
// RingBuffer: O(1) push, bounded memory, no array shifting or GC pressure
|
|
338
|
-
const recentLogs = new RingBuffer(8);
|
|
339
|
-
const MAX_LOGS = 8;
|
|
340
|
-
const RENDER_THROTTLE_MS = 200;
|
|
341
|
-
// Earnings history for sparkline (sample every 10 seconds)
|
|
342
|
-
const earningsHistory = new RingBuffer(30);
|
|
343
|
-
let lastEarningsSample = 0;
|
|
344
|
-
// Per-command stats tracking
|
|
345
|
-
const cmdStats = new Map();
|
|
346
|
-
// Coins per minute history for rate graph
|
|
347
|
-
const cpmHistory = new RingBuffer(20);
|
|
348
|
-
let lastCpmSample = 0;
|
|
349
|
-
|
|
350
|
-
function formatUptime() {
|
|
351
|
-
const s = Math.floor((Date.now() - startTime) / 1000);
|
|
352
|
-
const h = Math.floor(s / 3600);
|
|
353
|
-
const m = Math.floor((s % 3600) / 60);
|
|
354
|
-
const sec = s % 60;
|
|
355
|
-
if (h > 0) return `${h}h ${m}m ${sec}s`;
|
|
356
|
-
if (m > 0) return `${m}m ${sec}s`;
|
|
357
|
-
return `${sec}s`;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function formatCoins(n) {
|
|
361
|
-
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
|
362
|
-
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
363
|
-
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
364
|
-
return n.toLocaleString();
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function scheduleRender() {
|
|
368
|
-
if (renderPending || !dashboardStarted) return;
|
|
369
|
-
const now = Date.now();
|
|
370
|
-
const elapsed = now - lastRenderTime;
|
|
371
|
-
if (elapsed >= RENDER_THROTTLE_MS) {
|
|
372
|
-
renderDashboard();
|
|
373
|
-
} else {
|
|
374
|
-
renderPending = true;
|
|
375
|
-
setTimeout(() => { renderPending = false; renderDashboard(); }, RENDER_THROTTLE_MS - elapsed);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// ── Dashboard ──────────────────────────────────────────────────────────────────
|
|
380
|
-
// Thin wrapper: aggregates stats then delegates to ./dashboard.js
|
|
381
|
-
function renderDashboard() {
|
|
382
|
-
if (!dashboardStarted || workers.length === 0 || dashboardRendering || shutdownCalled) return;
|
|
383
|
-
dashboardRendering = true;
|
|
384
|
-
lastRenderTime = Date.now();
|
|
385
|
-
|
|
386
|
-
// Aggregate session totals
|
|
387
|
-
totalBalance = 0; totalCoins = 0; totalCommands = 0;
|
|
388
|
-
let totalErrors = 0;
|
|
389
|
-
for (const w of workers) {
|
|
390
|
-
totalBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
|
|
391
|
-
totalCoins += w.stats.coins || 0;
|
|
392
|
-
totalCommands += w.stats.commands || 0;
|
|
393
|
-
totalErrors += w.stats.errors || 0;
|
|
394
|
-
}
|
|
395
|
-
if (totalCoins > sessionPeakCoins) {
|
|
396
|
-
sessionPeakCoins = totalCoins;
|
|
397
|
-
isNewHigh = true;
|
|
398
|
-
setTimeout(() => { isNewHigh = false; }, 3000);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Pass all state into the dashboard module
|
|
402
|
-
const newLines = renderDashboardImpl({
|
|
403
|
-
workers, dashboardStarted, dashboardRendering, dashboardLines,
|
|
404
|
-
totalBalance, totalCoins, totalCommands, startTime,
|
|
405
|
-
sessionPeakCoins, isNewHigh, recentLogs, globalCmdRate,
|
|
406
|
-
earningsHistory, lastEarningsSample,
|
|
407
|
-
CLOUD_MODE, CLUSTER_ENABLED, PKG_VERSION,
|
|
408
|
-
AccountWorker, PULSE_CHARS, getSpinner, gradientText,
|
|
409
|
-
rgb, c, BOX,
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
if (newLines != null) dashboardLines = Math.max(dashboardLines, newLines);
|
|
413
|
-
dashboardRendering = false;
|
|
414
|
-
}
|
|
415
|
-
|
|
338
|
+
// ── Simple Logging ─────────────────────────────────────────────
|
|
416
339
|
function log(type, msg, label) {
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
cmd:
|
|
340
|
+
const colorIcons = {
|
|
341
|
+
info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
|
|
342
|
+
error: `${rgb(239, 68, 68)}✗${c.reset}`, warn: `${rgb(251, 191, 36)}!${c.reset}`,
|
|
343
|
+
cmd: `${rgb(168, 85, 247)}▸${c.reset}`, coin: `${rgb(251, 191, 36)}$${c.reset}`,
|
|
344
|
+
buy: `${rgb(59, 130, 246)}♦${c.reset}`, bal: `${rgb(52, 211, 153)}◈${c.reset}`,
|
|
345
|
+
debug: `${c.dim}·${c.reset}`,
|
|
421
346
|
};
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
const tw = Math.max(process.stdout.columns || 80, 60);
|
|
425
|
-
if (dashboardStarted) {
|
|
426
|
-
const maxLen = tw - 8;
|
|
427
|
-
const entry = `${time} ${icons[type] || '·'} ${tagRaw ? tagRaw + ' ' : ''}${stripped}`;
|
|
428
|
-
recentLogs.push(entry.substring(0, maxLen));
|
|
429
|
-
scheduleRender();
|
|
430
|
-
} else {
|
|
431
|
-
const colorIcons = {
|
|
432
|
-
info: `${c.dim}·${c.reset}`, success: `${rgb(52, 211, 153)}✓${c.reset}`,
|
|
433
|
-
error: `${rgb(239, 68, 68)}✗${c.reset}`, warn: `${rgb(251, 191, 36)}!${c.reset}`,
|
|
434
|
-
cmd: `${rgb(168, 85, 247)}▸${c.reset}`, coin: `${rgb(251, 191, 36)}$${c.reset}`,
|
|
435
|
-
buy: `${rgb(59, 130, 246)}♦${c.reset}`, bal: `${rgb(52, 211, 153)}◈${c.reset}`,
|
|
436
|
-
debug: `${c.dim}·${c.reset}`,
|
|
437
|
-
};
|
|
438
|
-
const tagCol = label ? `${label} ` : '';
|
|
439
|
-
console.log(` ${colorIcons[type] || colorIcons.info} ${tagCol}${msg}`);
|
|
440
|
-
}
|
|
347
|
+
const tagCol = label ? `${label} ` : '';
|
|
348
|
+
console.log(` ${colorIcons[type] || colorIcons.info} ${tagCol}${msg}`);
|
|
441
349
|
}
|
|
442
350
|
|
|
443
351
|
async function fetchConfig(retries = 3, delayMs = 1500, opts = {}) {
|
|
@@ -869,7 +777,6 @@ class AccountWorker {
|
|
|
869
777
|
|
|
870
778
|
setStatus(text) {
|
|
871
779
|
this.lastStatus = stripAnsi(String(text || '')).replace(/\s+/g, ' ').trim();
|
|
872
|
-
if (dashboardStarted) scheduleRender();
|
|
873
780
|
}
|
|
874
781
|
|
|
875
782
|
waitForDankMemer(timeout = 15000) {
|
|
@@ -1485,20 +1392,19 @@ class AccountWorker {
|
|
|
1485
1392
|
if (currentLevel > 0) {
|
|
1486
1393
|
await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
|
|
1487
1394
|
this._level = currentLevel;
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1395
|
+
if (currentLevel > 0) {
|
|
1396
|
+
await redis.set(`dkg:level:${this.account.id}`, String(currentLevel), 'EX', 2592000);
|
|
1397
|
+
this._level = currentLevel;
|
|
1398
|
+
}
|
|
1399
|
+
if (lastLifesaverCount >= 0) {
|
|
1400
|
+
await redis.set(`dkg:lifesavers:${this.account.id}`, String(lastLifesaverCount), 'EX', 86400);
|
|
1401
|
+
this._lifesavers = lastLifesaverCount;
|
|
1402
|
+
if (lastLifesaverCount === 0) {
|
|
1403
|
+
await redis.set(`raw:alert:no-lifesaver:${dm.id}`, '1', 'EX', 86400);
|
|
1404
|
+
await redis.set(`raw:alert:no-lifesaver:${this.channel?.id}`, '1', 'EX', 86400);
|
|
1405
|
+
}
|
|
1498
1406
|
}
|
|
1499
1407
|
}
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
1408
|
return { deaths, levelUps, currentLevel, lifesavers: lastLifesaverCount, dmChannelId: dm.id };
|
|
1503
1409
|
} catch (e) {
|
|
1504
1410
|
lastError = e;
|
|
@@ -1507,7 +1413,6 @@ class AccountWorker {
|
|
|
1507
1413
|
}
|
|
1508
1414
|
}
|
|
1509
1415
|
}
|
|
1510
|
-
if (dashboardStarted) this.log('debug', `DM check failed after ${maxRetries} attempts: ${lastError.message}`);
|
|
1511
1416
|
return { deaths: 0, levelUps: 0, currentLevel: 0, lifesavers: -1 };
|
|
1512
1417
|
}
|
|
1513
1418
|
|
|
@@ -2872,8 +2777,6 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2872
2777
|
await new Promise((r) => setTimeout(r, 10000));
|
|
2873
2778
|
data = await fetchConfig(4, 2000, fetchOpts);
|
|
2874
2779
|
}
|
|
2875
|
-
console.log(`[DEBUG] fetched config, accounts: ${data?.accounts?.length || 0}`);
|
|
2876
|
-
|
|
2877
2780
|
if (data && data.error) {
|
|
2878
2781
|
log('error', `${data.error}`);
|
|
2879
2782
|
return;
|
|
@@ -2957,13 +2860,11 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2957
2860
|
}
|
|
2958
2861
|
}
|
|
2959
2862
|
}
|
|
2960
|
-
scheduleRender();
|
|
2961
2863
|
}
|
|
2962
2864
|
|
|
2963
2865
|
if (event.type === 'levelup') {
|
|
2964
2866
|
if (event.to > 0) {
|
|
2965
2867
|
w._level = event.to;
|
|
2966
|
-
scheduleRender();
|
|
2967
2868
|
}
|
|
2968
2869
|
}
|
|
2969
2870
|
}
|
|
@@ -3317,13 +3218,10 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3317
3218
|
const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
|
|
3318
3219
|
parts.push(`${D}${pulse}♥?${c.reset}`);
|
|
3319
3220
|
}
|
|
3320
|
-
if (parts.length > 0) {
|
|
3321
|
-
recentLogs.push({ ts: Date.now(), username: w.username, color: w.color, command: 'dm check', response: parts.join(' '), status: 'ok' });
|
|
3322
|
-
}
|
|
3323
3221
|
} catch {}
|
|
3324
3222
|
}
|
|
3325
3223
|
if (dmNoLs.length > 0) {
|
|
3326
|
-
|
|
3224
|
+
log('warn', `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
|
|
3327
3225
|
// Set Redis keys to block crime/search
|
|
3328
3226
|
for (const w of activeWorkers) {
|
|
3329
3227
|
if (dmNoLs.includes(w.username) && redis) {
|
|
@@ -3335,7 +3233,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3335
3233
|
}
|
|
3336
3234
|
}
|
|
3337
3235
|
if (dmUnknown.length > 0) {
|
|
3338
|
-
|
|
3236
|
+
log('warn', `⚠ Lifesavers unknown — live monitor: ${dmUnknown.join(', ')}`);
|
|
3339
3237
|
// Crime/search on these accounts will be skipped via safety hold until the live
|
|
3340
3238
|
// DM gateway listener detects a death (→ sets count) or confirms clean.
|
|
3341
3239
|
}
|
|
@@ -3343,41 +3241,18 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3343
3241
|
if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
|
|
3344
3242
|
if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
|
|
3345
3243
|
if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
|
|
3346
|
-
|
|
3244
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean — no deaths or level-ups'}`);
|
|
3347
3245
|
console.log('');
|
|
3348
3246
|
|
|
3349
3247
|
console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
|
|
3350
3248
|
|
|
3351
|
-
console.log(`[DEBUG] activeWorkers count: ${activeWorkers.length}`);
|
|
3352
|
-
for (let i = 0; i < Math.min(activeWorkers.length, 5); i++) {
|
|
3353
|
-
const w = activeWorkers[i];
|
|
3354
|
-
console.log(`[DEBUG] worker[${i}]: ${w.username}, running=${w.running}, client=${!!w.client}, channel=${!!w.channel}`);
|
|
3355
|
-
}
|
|
3356
|
-
|
|
3357
3249
|
// Phase 3: Start all grind loops (only for valid workers)
|
|
3358
3250
|
for (const w of activeWorkers) {
|
|
3359
|
-
console.log(`[DEBUG] calling grindLoop on: ${w.username}`);
|
|
3360
3251
|
if (!shutdownCalled) w.grindLoop();
|
|
3361
3252
|
}
|
|
3362
3253
|
|
|
3363
|
-
|
|
3364
|
-
console.log(`
|
|
3365
|
-
dashboardStarted = true;
|
|
3366
|
-
setDashboardActive(true);
|
|
3367
|
-
// Setup keyboard shortcuts
|
|
3368
|
-
setupKeyboardShortcuts();
|
|
3369
|
-
// Cursor hide to reduce visual noise during renders
|
|
3370
|
-
process.stdout.write(c.hide);
|
|
3371
|
-
|
|
3372
|
-
// Re-render on terminal resize so layout adapts to window size
|
|
3373
|
-
process.stdout.on('resize', () => {
|
|
3374
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
3375
|
-
dashboardLines = 0;
|
|
3376
|
-
scheduleRender();
|
|
3377
|
-
});
|
|
3378
|
-
|
|
3379
|
-
setInterval(() => scheduleRender(), 1000);
|
|
3380
|
-
scheduleRender();
|
|
3254
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} All grind loops started — ${activeWorkers.length} accounts active`);
|
|
3255
|
+
console.log(` v${PKG_VERSION} | press Ctrl+C to stop`);
|
|
3381
3256
|
|
|
3382
3257
|
// Cluster heartbeat — lets other nodes see this node is alive
|
|
3383
3258
|
if (CLUSTER_ENABLED) {
|
|
@@ -3434,7 +3309,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3434
3309
|
const before = workers.length;
|
|
3435
3310
|
// Keep ALL workers visible — never remove from array (user wants to see gaps)
|
|
3436
3311
|
// Only clean up workerMap entries for accounts fully removed from API
|
|
3437
|
-
if (workers.length !== before)
|
|
3312
|
+
if (workers.length !== before) { /* workers changed */ }
|
|
3438
3313
|
} catch {}
|
|
3439
3314
|
}, 10_000);
|
|
3440
3315
|
|
|
@@ -3443,18 +3318,9 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3443
3318
|
if (sigintHandled) return;
|
|
3444
3319
|
sigintHandled = true;
|
|
3445
3320
|
shutdownCalled = true;
|
|
3446
|
-
dashboardStarted = false;
|
|
3447
3321
|
setDashboardActive(false);
|
|
3448
3322
|
process.stdout.write(c.show);
|
|
3449
3323
|
|
|
3450
|
-
if (dashboardLines > 0) {
|
|
3451
|
-
process.stdout.write(c.cursorUp(dashboardLines));
|
|
3452
|
-
for (let i = 0; i < dashboardLines; i++) {
|
|
3453
|
-
process.stdout.write(c.clearLine + '\r\n');
|
|
3454
|
-
}
|
|
3455
|
-
process.stdout.write(c.cursorUp(dashboardLines));
|
|
3456
|
-
}
|
|
3457
|
-
|
|
3458
3324
|
const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
|
|
3459
3325
|
console.log('');
|
|
3460
3326
|
console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
|
|
@@ -3516,79 +3382,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3516
3382
|
}
|
|
3517
3383
|
|
|
3518
3384
|
// ══════════════════════════════════════════════════════════════
|
|
3519
|
-
//
|
|
3520
|
-
// ══════════════════════════════════════════════════════════════
|
|
3521
|
-
// Single-key shortcuts for common actions
|
|
3522
|
-
function setupKeyboardShortcuts() {
|
|
3523
|
-
if (process.stdin.isTTY) {
|
|
3524
|
-
process.stdin.setRawMode(true);
|
|
3525
|
-
process.stdin.resume();
|
|
3526
|
-
process.stdin.setEncoding('utf8');
|
|
3527
|
-
|
|
3528
|
-
// Premium styled keyboard shortcuts with gradient box
|
|
3529
|
-
const accent = rgb(139, 92, 246);
|
|
3530
|
-
const dim = c.dim;
|
|
3531
|
-
const kw = 60;
|
|
3532
|
-
console.log('');
|
|
3533
|
-
console.log(` ${accent}╭${'─'.repeat(kw)}╮${c.reset}`);
|
|
3534
|
-
console.log(` ${accent}│${c.reset} ${gradientText('KEYBOARD SHORTCUTS', [192, 132, 252], [52, 211, 153])}${' '.repeat(kw - 20)}${accent}│${c.reset}`);
|
|
3535
|
-
console.log(` ${accent}├${'─'.repeat(kw)}┤${c.reset}`);
|
|
3536
|
-
console.log(` ${accent}│${c.reset} ${rgb(96, 165, 250)}P${c.reset} ${dim}Pause all${c.reset} ${rgb(52, 211, 153)}R${c.reset} ${dim}Resume all${c.reset} ${rgb(251, 191, 36)}S${c.reset} ${dim}Status${c.reset} ${rgb(239, 68, 68)}Q${c.reset} ${dim}Quit${c.reset}${' '.repeat(Math.max(0, kw - 54))}${accent}│${c.reset}`);
|
|
3537
|
-
console.log(` ${accent}╰${'─'.repeat(kw)}╯${c.reset}`);
|
|
3538
|
-
console.log('');
|
|
3539
|
-
|
|
3540
|
-
process.stdin.on('data', (key) => {
|
|
3541
|
-
const k = key.toString().toLowerCase();
|
|
3542
|
-
|
|
3543
|
-
// Ctrl+C or q = quit
|
|
3544
|
-
if (k === '\u0003' || k === 'q') {
|
|
3545
|
-
process.stdout.write(c.show);
|
|
3546
|
-
console.log(`\n\n ${c.yellow}Shutting down gracefully...${c.reset}`);
|
|
3547
|
-
process.emit('SIGINT');
|
|
3548
|
-
return;
|
|
3549
|
-
}
|
|
3550
|
-
|
|
3551
|
-
// p = pause all accounts
|
|
3552
|
-
if (k === 'p') {
|
|
3553
|
-
let count = 0;
|
|
3554
|
-
workers.forEach(w => { if (w.running && !w.paused) { w.paused = true; count++; } });
|
|
3555
|
-
recentLogs.push(`>> PAUSED ${count} accounts (press R to resume)`);
|
|
3556
|
-
scheduleRender();
|
|
3557
|
-
return;
|
|
3558
|
-
}
|
|
3559
|
-
|
|
3560
|
-
// r = resume all accounts
|
|
3561
|
-
if (k === 'r') {
|
|
3562
|
-
let count = 0;
|
|
3563
|
-
workers.forEach(w => { if (w.paused) { w.paused = false; count++; } });
|
|
3564
|
-
recentLogs.push(`>> RESUMED ${count} accounts`);
|
|
3565
|
-
scheduleRender();
|
|
3566
|
-
return;
|
|
3567
|
-
}
|
|
3568
|
-
|
|
3569
|
-
// s = show status summary (pushed to log feed)
|
|
3570
|
-
if (k === 's') {
|
|
3571
|
-
const active = workers.filter(w => w.running && !w.paused).length;
|
|
3572
|
-
const paused = workers.filter(w => w.paused).length;
|
|
3573
|
-
const invalid = workers.filter(w => w._tokenInvalid).length;
|
|
3574
|
-
const offline = workers.filter(w => !w.running && !w._tokenInvalid).length;
|
|
3575
|
-
const recovering = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
|
|
3576
|
-
const totalEarn = workers.reduce((s, w) => s + (w.stats.coins || 0), 0);
|
|
3577
|
-
recentLogs.push(`>> STATUS: ${active} active, ${paused} paused, ${invalid} invalid, ${offline} offline, ${recovering} recovering`);
|
|
3578
|
-
recentLogs.push(`>> EARNINGS: +${formatCoins(totalEarn)} this session | BALANCE: ${formatCoins(totalBalance)}`);
|
|
3579
|
-
scheduleRender();
|
|
3580
|
-
return;
|
|
3581
|
-
}
|
|
3582
|
-
|
|
3583
|
-
// ? or h = show help
|
|
3584
|
-
if (k === '?' || k === 'h') {
|
|
3585
|
-
recentLogs.push('>> SHORTCUTS: P=pause R=resume S=status Q=quit ?=help');
|
|
3586
|
-
scheduleRender();
|
|
3587
|
-
return;
|
|
3588
|
-
}
|
|
3589
|
-
});
|
|
3590
|
-
}
|
|
3591
|
-
}
|
|
3385
|
+
// Keyboard shortcuts removed — no display to update
|
|
3592
3386
|
|
|
3593
3387
|
// Export the start function for CLI
|
|
3594
3388
|
module.exports = { start };
|
package/lib/rawLogger.js
CHANGED
|
@@ -54,7 +54,10 @@ async function init(redisUrl) {
|
|
|
54
54
|
redisReady = true;
|
|
55
55
|
console.log('[rawLogger] Redis connected');
|
|
56
56
|
redis.on('error', (e) => {
|
|
57
|
-
|
|
57
|
+
// Suppress common transient network errors from spamming stderr
|
|
58
|
+
const msg = e?.message || '';
|
|
59
|
+
if (msg.includes('ETIMEDOUT') || msg.includes('ECONNRESET') || msg.includes('ENOTFOUND') || msg.includes('read') || msg.includes('connect')) return;
|
|
60
|
+
console.error(`[rawLogger] Redis error: ${msg}`);
|
|
58
61
|
redisReady = false;
|
|
59
62
|
});
|
|
60
63
|
redis.on('close', () => {
|