dankgrinder 8.1.0 → 8.10.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 -245
- 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
|
|
|
@@ -2955,13 +2860,11 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
2955
2860
|
}
|
|
2956
2861
|
}
|
|
2957
2862
|
}
|
|
2958
|
-
scheduleRender();
|
|
2959
2863
|
}
|
|
2960
2864
|
|
|
2961
2865
|
if (event.type === 'levelup') {
|
|
2962
2866
|
if (event.to > 0) {
|
|
2963
2867
|
w._level = event.to;
|
|
2964
|
-
scheduleRender();
|
|
2965
2868
|
}
|
|
2966
2869
|
}
|
|
2967
2870
|
}
|
|
@@ -3315,13 +3218,10 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3315
3218
|
const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
|
|
3316
3219
|
parts.push(`${D}${pulse}♥?${c.reset}`);
|
|
3317
3220
|
}
|
|
3318
|
-
if (parts.length > 0) {
|
|
3319
|
-
recentLogs.push({ ts: Date.now(), username: w.username, color: w.color, command: 'dm check', response: parts.join(' '), status: 'ok' });
|
|
3320
|
-
}
|
|
3321
3221
|
} catch {}
|
|
3322
3222
|
}
|
|
3323
3223
|
if (dmNoLs.length > 0) {
|
|
3324
|
-
|
|
3224
|
+
log('warn', `⚠ No lifesavers: ${dmNoLs.join(', ')}`);
|
|
3325
3225
|
// Set Redis keys to block crime/search
|
|
3326
3226
|
for (const w of activeWorkers) {
|
|
3327
3227
|
if (dmNoLs.includes(w.username) && redis) {
|
|
@@ -3333,7 +3233,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3333
3233
|
}
|
|
3334
3234
|
}
|
|
3335
3235
|
if (dmUnknown.length > 0) {
|
|
3336
|
-
|
|
3236
|
+
log('warn', `⚠ Lifesavers unknown — live monitor: ${dmUnknown.join(', ')}`);
|
|
3337
3237
|
// Crime/search on these accounts will be skipped via safety hold until the live
|
|
3338
3238
|
// DM gateway listener detects a death (→ sets count) or confirms clean.
|
|
3339
3239
|
}
|
|
@@ -3341,37 +3241,18 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3341
3241
|
if (dmDeaths > 0) dmSummaryParts.push(`${dmDeaths} deaths`);
|
|
3342
3242
|
if (dmLevelUps > 0) dmSummaryParts.push(`${dmLevelUps} level-ups`);
|
|
3343
3243
|
if (dmUnknown.length > 0) dmSummaryParts.push(`${dmUnknown.length} pending`);
|
|
3344
|
-
|
|
3244
|
+
console.log(` ${rgb(52, 211, 153)}✓${c.reset} DM check: ${dmSummaryParts.length > 0 ? dmSummaryParts.join(', ') : 'clean — no deaths or level-ups'}`);
|
|
3345
3245
|
console.log('');
|
|
3346
3246
|
|
|
3347
3247
|
console.log(` ${rgb(139, 92, 246)}${c.bold}>>>${c.reset} ${gradientText('Starting grind loops...', [139, 92, 246], [52, 211, 153])}`);
|
|
3348
3248
|
|
|
3349
|
-
// DEBUG: activeWorkers confirmed
|
|
3350
|
-
|
|
3351
3249
|
// Phase 3: Start all grind loops (only for valid workers)
|
|
3352
3250
|
for (const w of activeWorkers) {
|
|
3353
3251
|
if (!shutdownCalled) w.grindLoop();
|
|
3354
3252
|
}
|
|
3355
3253
|
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
setDashboardActive(true);
|
|
3359
|
-
|
|
3360
|
-
// Clear screen and position cursor at top-left before dashboard takes over
|
|
3361
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
3362
|
-
|
|
3363
|
-
// Setup keyboard shortcuts
|
|
3364
|
-
setupKeyboardShortcuts();
|
|
3365
|
-
|
|
3366
|
-
// Re-render on terminal resize so layout adapts to window size
|
|
3367
|
-
process.stdout.on('resize', () => {
|
|
3368
|
-
process.stdout.write('\x1b[2J\x1b[H');
|
|
3369
|
-
dashboardLines = 0;
|
|
3370
|
-
scheduleRender();
|
|
3371
|
-
});
|
|
3372
|
-
|
|
3373
|
-
setInterval(() => scheduleRender(), 1000);
|
|
3374
|
-
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`);
|
|
3375
3256
|
|
|
3376
3257
|
// Cluster heartbeat — lets other nodes see this node is alive
|
|
3377
3258
|
if (CLUSTER_ENABLED) {
|
|
@@ -3428,7 +3309,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3428
3309
|
const before = workers.length;
|
|
3429
3310
|
// Keep ALL workers visible — never remove from array (user wants to see gaps)
|
|
3430
3311
|
// Only clean up workerMap entries for accounts fully removed from API
|
|
3431
|
-
if (workers.length !== before)
|
|
3312
|
+
if (workers.length !== before) { /* workers changed */ }
|
|
3432
3313
|
} catch {}
|
|
3433
3314
|
}, 10_000);
|
|
3434
3315
|
|
|
@@ -3437,18 +3318,9 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3437
3318
|
if (sigintHandled) return;
|
|
3438
3319
|
sigintHandled = true;
|
|
3439
3320
|
shutdownCalled = true;
|
|
3440
|
-
dashboardStarted = false;
|
|
3441
3321
|
setDashboardActive(false);
|
|
3442
3322
|
process.stdout.write(c.show);
|
|
3443
3323
|
|
|
3444
|
-
if (dashboardLines > 0) {
|
|
3445
|
-
process.stdout.write(c.cursorUp(dashboardLines));
|
|
3446
|
-
for (let i = 0; i < dashboardLines; i++) {
|
|
3447
|
-
process.stdout.write(c.clearLine + '\r\n');
|
|
3448
|
-
}
|
|
3449
|
-
process.stdout.write(c.cursorUp(dashboardLines));
|
|
3450
|
-
}
|
|
3451
|
-
|
|
3452
3324
|
const sepBar = rgb(139, 92, 246) + c.bold + '═'.repeat(tw) + c.reset;
|
|
3453
3325
|
console.log('');
|
|
3454
3326
|
console.log(` ${rgb(251, 191, 36)}${c.bold}Session Summary${c.reset}`);
|
|
@@ -3510,79 +3382,7 @@ async function start(apiKey, apiUrl, opts = {}) {
|
|
|
3510
3382
|
}
|
|
3511
3383
|
|
|
3512
3384
|
// ══════════════════════════════════════════════════════════════
|
|
3513
|
-
//
|
|
3514
|
-
// ══════════════════════════════════════════════════════════════
|
|
3515
|
-
// Single-key shortcuts for common actions
|
|
3516
|
-
function setupKeyboardShortcuts() {
|
|
3517
|
-
if (process.stdin.isTTY) {
|
|
3518
|
-
process.stdin.setRawMode(true);
|
|
3519
|
-
process.stdin.resume();
|
|
3520
|
-
process.stdin.setEncoding('utf8');
|
|
3521
|
-
|
|
3522
|
-
// Premium styled keyboard shortcuts with gradient box
|
|
3523
|
-
const accent = rgb(139, 92, 246);
|
|
3524
|
-
const dim = c.dim;
|
|
3525
|
-
const kw = 60;
|
|
3526
|
-
console.log('');
|
|
3527
|
-
console.log(` ${accent}╭${'─'.repeat(kw)}╮${c.reset}`);
|
|
3528
|
-
console.log(` ${accent}│${c.reset} ${gradientText('KEYBOARD SHORTCUTS', [192, 132, 252], [52, 211, 153])}${' '.repeat(kw - 20)}${accent}│${c.reset}`);
|
|
3529
|
-
console.log(` ${accent}├${'─'.repeat(kw)}┤${c.reset}`);
|
|
3530
|
-
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}`);
|
|
3531
|
-
console.log(` ${accent}╰${'─'.repeat(kw)}╯${c.reset}`);
|
|
3532
|
-
console.log('');
|
|
3533
|
-
|
|
3534
|
-
process.stdin.on('data', (key) => {
|
|
3535
|
-
const k = key.toString().toLowerCase();
|
|
3536
|
-
|
|
3537
|
-
// Ctrl+C or q = quit
|
|
3538
|
-
if (k === '\u0003' || k === 'q') {
|
|
3539
|
-
process.stdout.write(c.show);
|
|
3540
|
-
console.log(`\n\n ${c.yellow}Shutting down gracefully...${c.reset}`);
|
|
3541
|
-
process.emit('SIGINT');
|
|
3542
|
-
return;
|
|
3543
|
-
}
|
|
3544
|
-
|
|
3545
|
-
// p = pause all accounts
|
|
3546
|
-
if (k === 'p') {
|
|
3547
|
-
let count = 0;
|
|
3548
|
-
workers.forEach(w => { if (w.running && !w.paused) { w.paused = true; count++; } });
|
|
3549
|
-
recentLogs.push(`>> PAUSED ${count} accounts (press R to resume)`);
|
|
3550
|
-
scheduleRender();
|
|
3551
|
-
return;
|
|
3552
|
-
}
|
|
3553
|
-
|
|
3554
|
-
// r = resume all accounts
|
|
3555
|
-
if (k === 'r') {
|
|
3556
|
-
let count = 0;
|
|
3557
|
-
workers.forEach(w => { if (w.paused) { w.paused = false; count++; } });
|
|
3558
|
-
recentLogs.push(`>> RESUMED ${count} accounts`);
|
|
3559
|
-
scheduleRender();
|
|
3560
|
-
return;
|
|
3561
|
-
}
|
|
3562
|
-
|
|
3563
|
-
// s = show status summary (pushed to log feed)
|
|
3564
|
-
if (k === 's') {
|
|
3565
|
-
const active = workers.filter(w => w.running && !w.paused).length;
|
|
3566
|
-
const paused = workers.filter(w => w.paused).length;
|
|
3567
|
-
const invalid = workers.filter(w => w._tokenInvalid).length;
|
|
3568
|
-
const offline = workers.filter(w => !w.running && !w._tokenInvalid).length;
|
|
3569
|
-
const recovering = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
|
|
3570
|
-
const totalEarn = workers.reduce((s, w) => s + (w.stats.coins || 0), 0);
|
|
3571
|
-
recentLogs.push(`>> STATUS: ${active} active, ${paused} paused, ${invalid} invalid, ${offline} offline, ${recovering} recovering`);
|
|
3572
|
-
recentLogs.push(`>> EARNINGS: +${formatCoins(totalEarn)} this session | BALANCE: ${formatCoins(totalBalance)}`);
|
|
3573
|
-
scheduleRender();
|
|
3574
|
-
return;
|
|
3575
|
-
}
|
|
3576
|
-
|
|
3577
|
-
// ? or h = show help
|
|
3578
|
-
if (k === '?' || k === 'h') {
|
|
3579
|
-
recentLogs.push('>> SHORTCUTS: P=pause R=resume S=status Q=quit ?=help');
|
|
3580
|
-
scheduleRender();
|
|
3581
|
-
return;
|
|
3582
|
-
}
|
|
3583
|
-
});
|
|
3584
|
-
}
|
|
3585
|
-
}
|
|
3385
|
+
// Keyboard shortcuts removed — no display to update
|
|
3586
3386
|
|
|
3587
3387
|
// Export the start function for CLI
|
|
3588
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', () => {
|