dankgrinder 7.63.0 → 7.66.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 +287 -0
- package/lib/grinder.js +17 -185
- package/package.json +1 -1
package/lib/dashboard.js
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DankGrinder CLI Dashboard
|
|
3
|
+
*
|
|
4
|
+
* Modern terminal UI with box-drawing characters, gradient accents, and real-time stats.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* const { renderDashboard } = require('./dashboard');
|
|
8
|
+
* renderDashboard(context); // called from grinder.js render loop
|
|
9
|
+
*
|
|
10
|
+
* Context shape:
|
|
11
|
+
* {
|
|
12
|
+
* workers, dashboardStarted, dashboardRendering, dashboardLines,
|
|
13
|
+
* totalBalance, totalCoins, totalCommands, startTime,
|
|
14
|
+
* sessionPeakCoins, isNewHigh, recentLogs, globalCmdRate,
|
|
15
|
+
* earningsHistory, lastEarningsSample,
|
|
16
|
+
* CLOUD_MODE, CLUSTER_ENABLED, PKG_VERSION,
|
|
17
|
+
* AccountWorker, PULSE_CHARS, getSpinner, gradientText,
|
|
18
|
+
* rgb, c, BOX,
|
|
19
|
+
* }
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
// ── Color shortcuts (resolved from context) ────────────────────
|
|
25
|
+
let _c, _rgb, _BOX, _getSpinner, _gradientText;
|
|
26
|
+
|
|
27
|
+
function initColors(ctx) {
|
|
28
|
+
_c = ctx.c;
|
|
29
|
+
_rgb = ctx.rgb;
|
|
30
|
+
_BOX = ctx.BOX;
|
|
31
|
+
_getSpinner = ctx.getSpinner;
|
|
32
|
+
_gradientText = ctx.gradientText;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pad(str, width) {
|
|
36
|
+
const raw = str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
37
|
+
return str + ' '.repeat(Math.max(0, width - raw.length));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Formatters ─────────────────────────────────────────────────
|
|
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 || ctx.shutdownCalled) return;
|
|
63
|
+
|
|
64
|
+
initColors(ctx);
|
|
65
|
+
|
|
66
|
+
const tw = Math.max(process.stdout.columns || 80, 60);
|
|
67
|
+
|
|
68
|
+
// Color palette (resolved)
|
|
69
|
+
const A = _rgb(139, 92, 246); // purple
|
|
70
|
+
const G = _rgb(52, 211, 153); // green
|
|
71
|
+
const B = _rgb(96, 165, 250); // blue
|
|
72
|
+
const Au = _rgb(255, 215, 0); // gold
|
|
73
|
+
const O = _rgb(251, 146, 60); // orange
|
|
74
|
+
const Cy = _rgb(34, 211, 238); // cyan
|
|
75
|
+
const R = _rgb(239, 68, 68); // red
|
|
76
|
+
const Y = _rgb(251, 191, 36); // yellow
|
|
77
|
+
const W = _c.white;
|
|
78
|
+
const D = _c.dim;
|
|
79
|
+
const _ = _c.reset;
|
|
80
|
+
const BOX = _BOX;
|
|
81
|
+
|
|
82
|
+
const rows = [];
|
|
83
|
+
|
|
84
|
+
// ── Aggregate stats ──────────────────────────────────────────
|
|
85
|
+
let aggBalance = 0, aggCoins = 0, aggCommands = 0, aggErrors = 0;
|
|
86
|
+
for (const w of ctx.workers) {
|
|
87
|
+
aggBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
|
|
88
|
+
aggCoins += w.stats.coins || 0;
|
|
89
|
+
aggCommands += w.stats.commands || 0;
|
|
90
|
+
aggErrors += w.stats.errors || 0;
|
|
91
|
+
}
|
|
92
|
+
const successRate = aggCommands > 0 ? Math.round(((aggCommands - aggErrors) / aggCommands) * 100) : 100;
|
|
93
|
+
|
|
94
|
+
const elapsedHrs = (Date.now() - ctx.startTime) / 3_600_000;
|
|
95
|
+
const coinsPerHr = elapsedHrs > 0.01 ? Math.round(aggCoins / elapsedHrs) : 0;
|
|
96
|
+
const cpmVal = ctx.globalCmdRate.getRate().toFixed(1);
|
|
97
|
+
|
|
98
|
+
const activeCount = ctx.workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
|
|
99
|
+
const invalidCount = ctx.workers.filter(w => w._tokenInvalid).length;
|
|
100
|
+
const pausedCount = ctx.workers.filter(w => w.paused || w.dashboardPaused).length;
|
|
101
|
+
const recovCount = ctx.workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
|
|
102
|
+
|
|
103
|
+
const netQ = ctx.workers.length > 0
|
|
104
|
+
? ctx.workers.reduce((s, w) => s + (w._lastPing < 200 ? 1 : w._lastPing < 500 ? 0.5 : 0), 0) / ctx.workers.length : 1;
|
|
105
|
+
const netDot = netQ > 0.8 ? `${G}●${_}` : netQ > 0.5 ? `${Y}●${_}` : `${R}●${_}`;
|
|
106
|
+
|
|
107
|
+
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
108
|
+
const spin = _getSpinner('braille');
|
|
109
|
+
|
|
110
|
+
// ── HEADER BOX ───────────────────────────────────────────────
|
|
111
|
+
const titleBar = `${_c.bold}${_gradientText(' DANK GRINDER ', [139, 92, 246], [52, 211, 153])}${_}`;
|
|
112
|
+
const versionTag = `${D}v${ctx.PKG_VERSION}${_}`;
|
|
113
|
+
const modeTag = ctx.CLOUD_MODE ? `${Cy}CLOUD${_}` : (ctx.CLUSTER_ENABLED ? `${Cy}CLUSTER${_}` : `${D}local${_}`);
|
|
114
|
+
|
|
115
|
+
// Top border
|
|
116
|
+
rows.push(`${A}${BOX.dtl}${BOX.dh.repeat(tw - 2)}${BOX.dtr}${_}`);
|
|
117
|
+
|
|
118
|
+
// Main header line
|
|
119
|
+
const h1 = [
|
|
120
|
+
titleBar, versionTag, spin,
|
|
121
|
+
`${D}up${_} ${W}${fmtUptime(ctx.startTime)}${_}`,
|
|
122
|
+
netDot, `${D}net${_}`,
|
|
123
|
+
`${Au}⏣${_}${W}${fmtCoins(aggBalance)}${_} ${D}bal${_}`,
|
|
124
|
+
`${G}${coinsPerHr >= 0 ? '+' : ''}⏣${fmtCoins(coinsPerHr)}${_}/h`,
|
|
125
|
+
`${G}${activeCount}${_}/${W}${ctx.workers.length}${_} ${D}active${_}`,
|
|
126
|
+
invalidCount > 0 ? ` ${R}${invalidCount} inv${_}` : '',
|
|
127
|
+
pausedCount > 0 ? ` ${Y}${pausedCount} pause${_}` : '',
|
|
128
|
+
recovCount > 0 ? ` ${O}${recovCount} recov${_}` : '',
|
|
129
|
+
` ${D}${aggCommands}${_} cmds`,
|
|
130
|
+
` ${D}${cpmVal}${_}/min`,
|
|
131
|
+
` ${D}mem${_} ${memMB}MB`,
|
|
132
|
+
modeTag,
|
|
133
|
+
].filter(Boolean).join(' ');
|
|
134
|
+
|
|
135
|
+
rows.push(`${A}${BOX.dv}${_} ${pad(h1, tw - 4)} ${A}${BOX.dv}${_}`);
|
|
136
|
+
|
|
137
|
+
// Divider
|
|
138
|
+
rows.push(`${A}${BOX.teeD}${BOX.dh.repeat(tw - 2)}${BOX.teeU}${_}`);
|
|
139
|
+
|
|
140
|
+
// Stats row
|
|
141
|
+
const statsRow = [
|
|
142
|
+
`${G}${fmtCoins(aggCoins)}${_} ${D}coins${_}`,
|
|
143
|
+
`${G}${successRate}%${_} ${D}success${_}`,
|
|
144
|
+
`${W}${fmtCoins(ctx.sessionPeakCoins)}${_} ${D}peak${_}`,
|
|
145
|
+
ctx.isNewHigh ? `${Au}★ NEW HIGH${_}` : '',
|
|
146
|
+
].filter(Boolean).join(` ${D}│${_} `);
|
|
147
|
+
|
|
148
|
+
const statLine = ` ${D}session${_} ${statsRow}`;
|
|
149
|
+
rows.push(`${A}${BOX.dv}${_}${pad(statLine, tw - 4)}${A}${BOX.dv}${_}`);
|
|
150
|
+
|
|
151
|
+
// Bottom header border
|
|
152
|
+
rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
|
|
153
|
+
|
|
154
|
+
// ── ACCOUNTS TABLE ──────────────────────────────────────────
|
|
155
|
+
const colNum = 4;
|
|
156
|
+
const colSts = 3;
|
|
157
|
+
const colName = Math.max(14, Math.min(22, Math.floor(tw * 0.20)));
|
|
158
|
+
const colBal = 10;
|
|
159
|
+
const colLvl = 5;
|
|
160
|
+
const colLs = 4;
|
|
161
|
+
const colEarn = 9;
|
|
162
|
+
const colAct = Math.max(6, tw - colNum - colSts - colName - colBal - colLvl - colLs - colEarn - 18);
|
|
163
|
+
const gap = 2;
|
|
164
|
+
const colGap = ' '.repeat(gap);
|
|
165
|
+
|
|
166
|
+
// Column header
|
|
167
|
+
const headers = [
|
|
168
|
+
`${D}${pad('#', colNum)}${_}`,
|
|
169
|
+
`${D}${pad('S', colSts)}${_}`,
|
|
170
|
+
`${_gradientText(pad('Account', colName), [139, 92, 246], [96, 165, 250])}${_}`,
|
|
171
|
+
`${D}${pad('Balance', colBal)}${_}`,
|
|
172
|
+
`${D}${pad('Lvl', colLvl)}${_}`,
|
|
173
|
+
`${D}${pad('LS', colLs)}${_}`,
|
|
174
|
+
`${D}${pad('Earned', colEarn)}${_}`,
|
|
175
|
+
`${D}${pad('Activity', colAct)}${_}`,
|
|
176
|
+
].join(colGap);
|
|
177
|
+
|
|
178
|
+
rows.push(`${A}${BOX.dtl}${BOX.dh.repeat(tw - 2)}${BOX.dtr}${_}`);
|
|
179
|
+
rows.push(`${A}${BOX.dv}${_} ${headers} ${A}${BOX.dv}${_}`);
|
|
180
|
+
rows.push(`${A}${BOX.teeD}${BOX.h.repeat(tw - 2)}${BOX.teeU}${_}`);
|
|
181
|
+
|
|
182
|
+
const sorted = [...ctx.workers].sort((a, b) => a.idx - b.idx);
|
|
183
|
+
const maxRows = Math.max(4, Math.min(sorted.length, Math.floor((process.stdout.rows || 24) - 14)));
|
|
184
|
+
const visible = sorted.slice(0, maxRows);
|
|
185
|
+
|
|
186
|
+
for (const wk of visible) {
|
|
187
|
+
const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
|
|
188
|
+
|
|
189
|
+
let stsIcon;
|
|
190
|
+
if (wk._tokenInvalid) stsIcon = `${R}✗${_}`;
|
|
191
|
+
else if (!wk.running) stsIcon = `${D}○${_}`;
|
|
192
|
+
else if (isRecov) stsIcon = `${O}${_getSpinner('braille').substring(0, 1)}${_}`;
|
|
193
|
+
else if (wk.paused) stsIcon = `${R}⏸${_}`;
|
|
194
|
+
else if (wk.dashboardPaused) stsIcon = `${Y}⏸${_}`;
|
|
195
|
+
else if (wk.busy) stsIcon = `${G}${_getSpinner('pulse').substring(0, 1)}${_}`;
|
|
196
|
+
else stsIcon = `${G}●${_}`;
|
|
197
|
+
|
|
198
|
+
const name = (wk.username || wk.account.label || '?').substring(0, colName);
|
|
199
|
+
const nameStr = `${wk.color}${name}${_}`;
|
|
200
|
+
const balStr = wk.stats.balance > 0 ? `${Au}⏣${_}${W}${fmtCoins(wk.stats.balance)}${_}` : `${D}⏣-${_}`;
|
|
201
|
+
const lvl = wk._level || 0;
|
|
202
|
+
const lvlStr = lvl > 0 ? `${Cy}L${lvl}${_}` : `${D}L???${_}`;
|
|
203
|
+
const ls = wk._lifesavers;
|
|
204
|
+
let lsStr;
|
|
205
|
+
if (ls === 0) lsStr = `${R}♥${ls}${_}`;
|
|
206
|
+
else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${_}`;
|
|
207
|
+
else if (ls != null) lsStr = `${G}♥${ls}${_}`;
|
|
208
|
+
else {
|
|
209
|
+
const p = ctx.PULSE_CHARS[Math.floor(Date.now() / 400) % ctx.PULSE_CHARS.length];
|
|
210
|
+
lsStr = `${D}${p}♥?${_}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const earn = wk.stats.coins || 0;
|
|
214
|
+
const earnStr = earn > 0 ? `${G}+${fmtCoins(earn)}${_}` : `${D}────${_}`;
|
|
215
|
+
const actRaw = (wk.lastStatus || 'ready').replace(/\x1b\[[0-9;]*m/g, '').substring(0, colAct);
|
|
216
|
+
const actStr = `${D}${pad(actRaw, colAct)}${_}`;
|
|
217
|
+
const numStr = `${D}${pad(String(wk.idx + 1), colNum)}${_}`;
|
|
218
|
+
|
|
219
|
+
const row = [
|
|
220
|
+
numStr, pad(stsIcon, colSts), pad(nameStr, colName),
|
|
221
|
+
pad(balStr, colBal), pad(lvlStr, colLvl), pad(lsStr, colLs),
|
|
222
|
+
pad(earnStr, colEarn), actStr,
|
|
223
|
+
].join(colGap);
|
|
224
|
+
|
|
225
|
+
rows.push(`${A}${BOX.dv}${_} ${pad(row, tw - 4)} ${A}${BOX.dv}${_}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (sorted.length > maxRows) {
|
|
229
|
+
const rest = sorted.length - maxRows;
|
|
230
|
+
rows.push(`${A}${BOX.dv}${_} ${D}+${rest} more${_}${' '.repeat(Math.max(0, tw - 18))}${A}${BOX.dv}${_}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
|
|
234
|
+
|
|
235
|
+
// ── LIVE FEED ────────────────────────────────────────────────
|
|
236
|
+
const logs = ctx.recentLogs.toArray();
|
|
237
|
+
if (logs.length > 0) {
|
|
238
|
+
const feedTitle = `${_gradientText(' LIVE FEED ', [139, 92, 246], [52, 211, 153])}${_} ${G}${_getSpinner('pulse')}${_}`;
|
|
239
|
+
|
|
240
|
+
rows.push(`${A}${BOX.dtl}${BOX.dh.repeat(tw - 2)}${BOX.dtr}${_}`);
|
|
241
|
+
rows.push(`${A}${BOX.dv}${_} ${pad(feedTitle, tw - 4)} ${A}${BOX.dv}${_}`);
|
|
242
|
+
rows.push(`${A}${BOX.teeD}${BOX.h.repeat(tw - 2)}${BOX.teeU}${_}`);
|
|
243
|
+
|
|
244
|
+
for (const entry of logs) {
|
|
245
|
+
let lineText;
|
|
246
|
+
if (typeof entry === 'string') {
|
|
247
|
+
lineText = entry;
|
|
248
|
+
} else if (entry && typeof entry === 'object') {
|
|
249
|
+
const ts = entry.ts ? new Date(entry.ts).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
|
|
250
|
+
const user = entry.username ? String(entry.username) : '';
|
|
251
|
+
const cmd = entry.command ? `[${entry.command}]` : '';
|
|
252
|
+
const resp = entry.response || '';
|
|
253
|
+
lineText = `${D}${ts}${_} ${entry.color || D}${user}${_} ${D}${cmd}${_} ${resp}`;
|
|
254
|
+
} else {
|
|
255
|
+
lineText = String(entry);
|
|
256
|
+
}
|
|
257
|
+
rows.push(`${A}${BOX.dv}${_} ${D}${pad(lineText, tw - 4)}${_} ${A}${BOX.dv}${_}`);
|
|
258
|
+
}
|
|
259
|
+
rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ── FOOTER ───────────────────────────────────────────────────
|
|
263
|
+
const footerLine = `${_c.dim}P=pause R=resume S=status Q=quit${_}`;
|
|
264
|
+
const modeLabel = ctx.CLOUD_MODE ? `${Cy}☁ CLOUD${_}` : (ctx.CLUSTER_ENABLED ? `${Cy}⎔ CLUSTER${_}` : `${D}○ local${_}`);
|
|
265
|
+
const footerFull = `${modeLabel} ${footerLine}`;
|
|
266
|
+
|
|
267
|
+
rows.push(`${A}${BOX.dtl}${BOX.dh.repeat(tw - 2)}${BOX.dtr}${_}`);
|
|
268
|
+
rows.push(`${A}${BOX.dv}${_} ${pad(footerFull, tw - 4)} ${A}${BOX.dv}${_}`);
|
|
269
|
+
rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
|
|
270
|
+
|
|
271
|
+
// ── Flush ────────────────────────────────────────────────────
|
|
272
|
+
// Clear entire screen buffer first so old startup logs don't bleed through
|
|
273
|
+
process.stdout.write('\x1b[2J\x1b[H');
|
|
274
|
+
process.stdout.write(_c.hide);
|
|
275
|
+
for (const row of rows) {
|
|
276
|
+
process.stdout.write(`${_c.clearLine}\r${row}\n`);
|
|
277
|
+
}
|
|
278
|
+
process.stdout.write(_c.show);
|
|
279
|
+
const leftover = ctx.dashboardLines - rows.length;
|
|
280
|
+
if (leftover > 0) {
|
|
281
|
+
process.stdout.write(`\x1b[${leftover}B\x1b[2K`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return rows.length;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
module.exports = { renderDashboard };
|
package/lib/grinder.js
CHANGED
|
@@ -3,6 +3,7 @@ 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');
|
|
6
7
|
const {
|
|
7
8
|
BloomFilter, RingBuffer, TokenBucket, EMA, SlidingWindowCounter,
|
|
8
9
|
AhoCorasick, LRUCache, StringPool, AsyncBatchQueue, JitterBackoff,
|
|
@@ -375,209 +376,40 @@ function scheduleRender() {
|
|
|
375
376
|
}
|
|
376
377
|
}
|
|
377
378
|
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
// ═ Structure: [3-line header] [accounts table] [live feed] [footer]
|
|
381
|
-
// ║ Uses full-width box borders, ANSI cursor positioning,
|
|
382
|
-
// ═ per-row ANSI-aware padding for stable display.
|
|
383
|
-
// ══════════════════════════════════════════════════════════════
|
|
379
|
+
// ── Dashboard ──────────────────────────────────────────────────────────────────
|
|
380
|
+
// Thin wrapper: aggregates stats then delegates to ./dashboard.js
|
|
384
381
|
function renderDashboard() {
|
|
385
382
|
if (!dashboardStarted || workers.length === 0 || dashboardRendering || shutdownCalled) return;
|
|
386
383
|
dashboardRendering = true;
|
|
387
384
|
lastRenderTime = Date.now();
|
|
388
385
|
|
|
389
|
-
//
|
|
386
|
+
// Aggregate session totals
|
|
390
387
|
totalBalance = 0; totalCoins = 0; totalCommands = 0;
|
|
391
388
|
let totalErrors = 0;
|
|
392
|
-
let totalLosses = 0;
|
|
393
389
|
for (const w of workers) {
|
|
394
390
|
totalBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
|
|
395
|
-
totalCoins
|
|
391
|
+
totalCoins += w.stats.coins || 0;
|
|
396
392
|
totalCommands += w.stats.commands || 0;
|
|
397
|
-
totalErrors
|
|
398
|
-
totalLosses += w.stats.losses || 0;
|
|
393
|
+
totalErrors += w.stats.errors || 0;
|
|
399
394
|
}
|
|
400
|
-
const successRate = totalCommands > 0 ? Math.round(((totalCommands - totalErrors) / totalCommands) * 100) : 100;
|
|
401
395
|
if (totalCoins > sessionPeakCoins) {
|
|
402
396
|
sessionPeakCoins = totalCoins;
|
|
403
397
|
isNewHigh = true;
|
|
404
398
|
setTimeout(() => { isNewHigh = false; }, 3000);
|
|
405
399
|
}
|
|
406
400
|
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const O = rgb(251, 146, 60); // orange
|
|
418
|
-
const Cy = rgb(34, 211, 238); // cyan
|
|
419
|
-
const R = rgb(239, 68, 68); // red
|
|
420
|
-
const Y = rgb(251, 191, 36); // yellow
|
|
421
|
-
const W = c.white;
|
|
422
|
-
const D = c.dim;
|
|
423
|
-
const _ = c.reset;
|
|
424
|
-
|
|
425
|
-
// ── Build rows array ──────────────────────────────────────────
|
|
426
|
-
const rows = [];
|
|
427
|
-
|
|
428
|
-
// ── HEADER ────────────────────────────────────────────────────
|
|
429
|
-
const spin = getSpinner('braille');
|
|
430
|
-
const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
|
|
431
|
-
const invalidCount = workers.filter(w => w._tokenInvalid).length;
|
|
432
|
-
const pausedCount = workers.filter(w => w.paused || w.dashboardPaused).length;
|
|
433
|
-
const recovCount = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
|
|
434
|
-
const netQ = workers.length > 0
|
|
435
|
-
? workers.reduce((s, w) => s + (w._lastPing < 200 ? 1 : w._lastPing < 500 ? 0.5 : 0), 0) / workers.length : 1;
|
|
436
|
-
const netDot = netQ > 0.8 ? `${G}●${_}` : netQ > 0.5 ? `${Y}●${_}` : `${R}●${_}`;
|
|
437
|
-
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
438
|
-
const elapsedHrs = (Date.now() - startTime) / 3_600_000;
|
|
439
|
-
const perHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
|
|
440
|
-
const cpmVal = globalCmdRate.getRate().toFixed(1);
|
|
441
|
-
const now = Date.now();
|
|
442
|
-
if (now - lastEarningsSample > 8000) { earningsHistory.push(totalCoins); lastEarningsSample = now; }
|
|
443
|
-
|
|
444
|
-
const topLine = `${A}┌${'─'.repeat(tw - 2)}┐${_}`;
|
|
445
|
-
rows.push(topLine);
|
|
446
|
-
|
|
447
|
-
const title = `${c.bold}${gradientText(' DANK GRINDER ', [139, 92, 246], [52, 211, 153])}${_}`;
|
|
448
|
-
const modeLabel = CLOUD_MODE ? `${Cy}CLOUD${_}` : (CLUSTER_ENABLED ? `${Cy}CLUSTER${_}` : `${D}local${_}`);
|
|
449
|
-
const row1Content = `${title} ${D}v${PKG_VERSION}${_} ${G}${spin}${_}` +
|
|
450
|
-
` ${D}up${_} ${Y}${formatUptime()}${_}` +
|
|
451
|
-
` ${netDot} ${D}net${_}` +
|
|
452
|
-
` ${Au}⏣${_}${W}${formatCoins(totalBalance)}${_} ${D}bal${_}` +
|
|
453
|
-
` ${G}${perHr >= 0 ? '+' : ''}⏣${formatCoins(perHr)}${_}/h${_}` +
|
|
454
|
-
` ${G}${activeCount}${_}/${W}${workers.length}${_} ${D}active${_}` +
|
|
455
|
-
(invalidCount > 0 ? ` ${R}${invalidCount} invalid${_}` : '') +
|
|
456
|
-
(pausedCount > 0 ? ` ${Y}${pausedCount} paused${_}` : '') +
|
|
457
|
-
(recovCount > 0 ? ` ${O}${recovCount} recov${_}` : '') +
|
|
458
|
-
` ${D}${totalCommands}${_} cmds` +
|
|
459
|
-
` ${D}mem ${memMB}MB${_}`;
|
|
460
|
-
rows.push(`${A}│${_} ${pad(row1Content, tw - 4)} ${A}│${_}`);
|
|
461
|
-
rows.push(`${A}└${'─'.repeat(tw - 2)}┘${_}`);
|
|
462
|
-
|
|
463
|
-
// ── ACCOUNTS TABLE ────────────────────────────────────────────
|
|
464
|
-
// Columns: #, S, Name, Balance, Lvl, LS, Earned, Activity
|
|
465
|
-
const colNum = 3; // "#1"
|
|
466
|
-
const colSts = 2; // "●"
|
|
467
|
-
const colName = Math.max(14, Math.min(20, Math.floor(tw * 0.22)));
|
|
468
|
-
const colBal = 9; // "⏣ 1.2M"
|
|
469
|
-
const colLvl = 5; // "L273"
|
|
470
|
-
const colLs = 4; // "♥37"
|
|
471
|
-
const colEarn = 8; // "+500K"
|
|
472
|
-
const colAct = Math.max(6, tw - colNum - colSts - colName - colBal - colLvl - colLs - colEarn - 16);
|
|
473
|
-
const gap = 2;
|
|
474
|
-
const colGap = ' '.repeat(gap);
|
|
475
|
-
|
|
476
|
-
const hNum = `${D}${pad('#', colNum)}${_}`;
|
|
477
|
-
const hSts = `${D}${pad('S', colSts)}${_}`;
|
|
478
|
-
const hName = `${gradientText(pad('Account', colName), [139, 92, 246], [96, 165, 250])}${_}`;
|
|
479
|
-
const hBal = `${D}${pad('Balance', colBal)}${_}`;
|
|
480
|
-
const hLvl = `${D}${pad('Lvl', colLvl)}${_}`;
|
|
481
|
-
const hLs = `${D}${pad('LS', colLs)}${_}`;
|
|
482
|
-
const hEarn = `${D}${pad('Earned', colEarn)}${_}`;
|
|
483
|
-
const hAct = `${D}${pad('Activity', colAct)}${_}`;
|
|
484
|
-
|
|
485
|
-
rows.push(`${A}┌${'─'.repeat(tw - 2)}┐${_}`);
|
|
486
|
-
rows.push(`${A}│${_} ${hNum}${colGap}${hSts}${colGap}${hName}${colGap}${hBal}${colGap}${hLvl}${colGap}${hLs}${colGap}${hEarn}${colGap}${hAct} ${A}│${_}`);
|
|
487
|
-
rows.push(`${A}├${'─'.repeat(tw - 2)}┤${_}`);
|
|
488
|
-
|
|
489
|
-
const sortedWorkers = [...workers].sort((a, b) => a.idx - b.idx);
|
|
490
|
-
const topEarners = [...workers]
|
|
491
|
-
.filter(w => (w.stats.coins || 0) > 0)
|
|
492
|
-
.sort((a, b) => (b.stats.coins || 0) - (a.stats.coins || 0))
|
|
493
|
-
.slice(0, 3);
|
|
494
|
-
const topIds = new Map();
|
|
495
|
-
topEarners.forEach((w, i) => topIds.set(w.account.id, i));
|
|
496
|
-
|
|
497
|
-
// Adapt row count to available terminal height
|
|
498
|
-
const maxRows = Math.max(4, Math.min(sortedWorkers.length, Math.floor((process.stdout.rows || 24) - 14)));
|
|
499
|
-
const visibleWorkers = sortedWorkers.slice(0, maxRows);
|
|
500
|
-
|
|
501
|
-
for (const wk of visibleWorkers) {
|
|
502
|
-
const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
|
|
503
|
-
let stsIcon;
|
|
504
|
-
if (wk._tokenInvalid) stsIcon = `${R}✗${_}`;
|
|
505
|
-
else if (!wk.running) stsIcon = `${D}○${_}`;
|
|
506
|
-
else if (isRecov) stsIcon = `${O}${getSpinner('braille').substring(0, 1)}${_}`;
|
|
507
|
-
else if (wk.paused) stsIcon = `${R}⏸${_}`;
|
|
508
|
-
else if (wk.dashboardPaused) stsIcon = `${Y}⏸${_}`;
|
|
509
|
-
else if (wk.busy) stsIcon = `${G}${getSpinner('pulse').substring(0, 1)}${_}`;
|
|
510
|
-
else stsIcon = `${G}●${_}`;
|
|
511
|
-
|
|
512
|
-
const rawName = (wk.username || wk.account.label || '?').substring(0, colName);
|
|
513
|
-
const nameStr = `${wk.color}${rawName}${_}`;
|
|
514
|
-
const balStr = wk.stats.balance > 0 ? `${Au}⏣${_}${W}${formatCoins(wk.stats.balance)}${_}` : `${D}⏣-${_}`;
|
|
515
|
-
const lvl = wk._level || 0;
|
|
516
|
-
const lvlStr = lvl > 0 ? `${Cy}L${lvl}${_}` : `${D}L???${_}`;
|
|
517
|
-
const ls = wk._lifesavers;
|
|
518
|
-
let lsStr;
|
|
519
|
-
if (ls === 0) lsStr = `${R}♥${ls}${_}`;
|
|
520
|
-
else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${_}`;
|
|
521
|
-
else if (ls != null) lsStr = `${G}♥${ls}${_}`;
|
|
522
|
-
else {
|
|
523
|
-
const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
|
|
524
|
-
lsStr = `${D}${pulse}♥?${_}`;
|
|
525
|
-
}
|
|
526
|
-
const earnNum = wk.stats.coins || 0;
|
|
527
|
-
const earnStr = earnNum > 0 ? `${G}+${formatCoins(earnNum)}${_}` : `${D}────${_}`;
|
|
528
|
-
const rawAct = (wk.lastStatus || 'ready').replace(RE, '').substring(0, colAct);
|
|
529
|
-
const actStr = `${D}${pad(rawAct, colAct)}${_}`;
|
|
530
|
-
const numStr = `${D}${pad(String(wk.idx + 1), colNum)}${_}`;
|
|
531
|
-
|
|
532
|
-
rows.push(`${A}│${_} ${numStr}${colGap}${pad(stsIcon, colSts)}${colGap}${pad(nameStr, colName)}${colGap}${pad(balStr, colBal)}${colGap}${pad(lvlStr, colLvl)}${colGap}${pad(lsStr, colLs)}${colGap}${pad(earnStr, colEarn)}${colGap}${actStr} ${A}│${_}`);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
if (sortedWorkers.length > maxRows) {
|
|
536
|
-
const rest = sortedWorkers.length - maxRows;
|
|
537
|
-
rows.push(`${A}│${_} ${D}+${rest} more accounts${_}${' '.repeat(Math.max(0, tw - 22 - rest.toString().length))}${A}│${_}`);
|
|
538
|
-
}
|
|
539
|
-
rows.push(`${A}└${'─'.repeat(tw - 2)}┘${_}`);
|
|
540
|
-
|
|
541
|
-
// ── LIVE FEED ─────────────────────────────────────────────────
|
|
542
|
-
const logEntries = recentLogs.toArray();
|
|
543
|
-
if (logEntries.length > 0) {
|
|
544
|
-
rows.push(`${A}┌${'─'.repeat(tw - 2)}┐${_}`);
|
|
545
|
-
rows.push(`${A}│${_} ${gradientText(' LIVE FEED ', [139, 92, 246], [52, 211, 153])}${_} ${G}${getSpinner('pulse')}${_}${' '.repeat(Math.max(0, tw - 22))}${A}│${_}`);
|
|
546
|
-
rows.push(`${A}├${'─'.repeat(tw - 2)}┤${_}`);
|
|
547
|
-
for (const entry of logEntries) {
|
|
548
|
-
let lineText;
|
|
549
|
-
if (typeof entry === 'string') {
|
|
550
|
-
lineText = entry;
|
|
551
|
-
} else if (entry && typeof entry === 'object') {
|
|
552
|
-
const ts = entry.ts ? new Date(entry.ts).toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }) : '';
|
|
553
|
-
const user = entry.username ? String(entry.username) : '';
|
|
554
|
-
const cmd = entry.command ? `[${entry.command}]` : '';
|
|
555
|
-
const resp = entry.response || '';
|
|
556
|
-
lineText = `${D}${ts}${_} ${entry.color || D}${user}${_} ${D}${cmd}${_} ${resp}`;
|
|
557
|
-
} else {
|
|
558
|
-
lineText = String(entry);
|
|
559
|
-
}
|
|
560
|
-
rows.push(`${A}│${_} ${D}${pad(lineText, tw - 4)}${_} ${A}│${_}`);
|
|
561
|
-
}
|
|
562
|
-
rows.push(`${A}└${'─'.repeat(tw - 2)}┘${_}`);
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// ── FOOTER ────────────────────────────────────────────────────
|
|
566
|
-
rows.push(`${A}┌${'─'.repeat(tw - 2)}┐${_}`);
|
|
567
|
-
rows.push(`${A}│${_} ${modeLabel} ${D}P=pause R=resume S=status Q=quit${_}${' '.repeat(Math.max(0, tw - 48))}${A}│${_}`);
|
|
568
|
-
rows.push(`${A}└${'─'.repeat(tw - 2)}┘${_}`);
|
|
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
|
+
});
|
|
569
411
|
|
|
570
|
-
|
|
571
|
-
process.stdout.write('\x1b[H');
|
|
572
|
-
for (const row of rows) {
|
|
573
|
-
process.stdout.write(c.clearLine + '\r' + row + '\n');
|
|
574
|
-
}
|
|
575
|
-
// Erase any leftover lines from previous render
|
|
576
|
-
const clearDown = Math.max(0, dashboardLines - rows.length);
|
|
577
|
-
if (clearDown > 0) {
|
|
578
|
-
process.stdout.write(`\x1b[${clearDown}B\x1b[2K`);
|
|
579
|
-
}
|
|
580
|
-
dashboardLines = rows.length;
|
|
412
|
+
if (newLines != null) dashboardLines = Math.max(dashboardLines, newLines);
|
|
581
413
|
dashboardRendering = false;
|
|
582
414
|
}
|
|
583
415
|
|