dankgrinder 7.62.0 → 7.64.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 +284 -0
- package/lib/grinder.js +17 -348
- package/package.json +1 -1
package/lib/dashboard.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
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) 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(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
|
+
process.stdout.write('\x1b[H');
|
|
273
|
+
for (const row of rows) {
|
|
274
|
+
process.stdout.write(`${_c.clearLine}\r${row}\n`);
|
|
275
|
+
}
|
|
276
|
+
const leftover = ctx.dashboardLines - rows.length;
|
|
277
|
+
if (leftover > 0) {
|
|
278
|
+
process.stdout.write(`\x1b[${leftover}B\x1b[2K`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return rows.length;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
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,372 +376,40 @@ function scheduleRender() {
|
|
|
375
376
|
}
|
|
376
377
|
}
|
|
377
378
|
|
|
379
|
+
// ── Dashboard ──────────────────────────────────────────────────────────────────
|
|
380
|
+
// Thin wrapper: aggregates stats then delegates to ./dashboard.js
|
|
378
381
|
function renderDashboard() {
|
|
379
382
|
if (!dashboardStarted || workers.length === 0 || dashboardRendering || shutdownCalled) return;
|
|
380
383
|
dashboardRendering = true;
|
|
381
384
|
lastRenderTime = Date.now();
|
|
382
385
|
|
|
386
|
+
// Aggregate session totals
|
|
383
387
|
totalBalance = 0; totalCoins = 0; totalCommands = 0;
|
|
384
388
|
let totalErrors = 0;
|
|
385
|
-
let totalLosses = 0;
|
|
386
389
|
for (const w of workers) {
|
|
387
390
|
totalBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
|
|
388
|
-
totalCoins
|
|
391
|
+
totalCoins += w.stats.coins || 0;
|
|
389
392
|
totalCommands += w.stats.commands || 0;
|
|
390
|
-
totalErrors
|
|
391
|
-
totalLosses += w.stats.losses || 0;
|
|
393
|
+
totalErrors += w.stats.errors || 0;
|
|
392
394
|
}
|
|
393
|
-
const successRate = totalCommands > 0 ? Math.round(((totalCommands - totalErrors) / totalCommands) * 100) : 100;
|
|
394
395
|
if (totalCoins > sessionPeakCoins) {
|
|
395
396
|
sessionPeakCoins = totalCoins;
|
|
396
397
|
isNewHigh = true;
|
|
397
398
|
setTimeout(() => { isNewHigh = false; }, 3000);
|
|
398
399
|
}
|
|
399
400
|
|
|
400
|
-
//
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const Au = rgb(255, 215, 0); // gold
|
|
411
|
-
const O = rgb(251, 146, 60); // orange
|
|
412
|
-
const Cy = rgb(34, 211, 238); // cyan
|
|
413
|
-
const R = rgb(239, 68, 68); // red
|
|
414
|
-
const Y = rgb(251, 191, 36); // yellow
|
|
415
|
-
const W = c.white;
|
|
416
|
-
const D = c.dim;
|
|
417
|
-
const RE = /\x1b\[[0-9;]*m/g;
|
|
418
|
-
|
|
419
|
-
// ── Box drawing (scales to tw) ──
|
|
420
|
-
const bTop = A + '╔' + '═'.repeat(tw - 2) + '╗' + c.reset;
|
|
421
|
-
const bMid = A + '╟' + '─'.repeat(tw - 2) + '╢' + c.reset;
|
|
422
|
-
const bSep = A + '╠' + '═'.repeat(tw - 2) + '╣' + c.reset;
|
|
423
|
-
const bBot = A + '╚' + '═'.repeat(tw - 2) + '╝' + c.reset;
|
|
424
|
-
const bRow = (content) => {
|
|
425
|
-
const vis = content.replace(RE, '').length;
|
|
426
|
-
const pad = Math.max(0, iw - vis);
|
|
427
|
-
return A + '║' + c.reset + ' ' + content + ' '.repeat(pad) + ' ' + A + '║' + c.reset;
|
|
428
|
-
};
|
|
429
|
-
const bEmpty = bRow('');
|
|
430
|
-
|
|
431
|
-
// ═══════════════════════════════════════════════════════════════
|
|
432
|
-
// HEADER
|
|
433
|
-
// ═══════════════════════════════════════════════════════════════
|
|
434
|
-
lines.push(bTop);
|
|
435
|
-
lines.push(bEmpty);
|
|
436
|
-
|
|
437
|
-
const spin = getSpinner('braille');
|
|
438
|
-
|
|
439
|
-
// Title — big gradient banner
|
|
440
|
-
const titleLines = [
|
|
441
|
-
'██████╗ █████╗ ███╗ ██╗██╗ ██╗ ██████╗ ██████╗ ██╗███╗ ██╗██████╗ ███████╗██████╗',
|
|
442
|
-
'██╔══██╗██╔══██╗████╗ ██║██║ ██╔╝ ██╔════╝ ██╔══██╗██║████╗ ██║██╔══██╗██╔════╝██╔══██╗',
|
|
443
|
-
'██║ ██║███████║██╔██╗ ██║█████╔╝ ██║ ███╗██████╔╝██║██╔██╗ ██║██║ ██║█████╗ ██████╔╝',
|
|
444
|
-
'██║ ██║██╔══██║██║╚██╗██║██╔═██╗ ██║ ██║██╔══██╗██║██║╚██╗██║██║ ██║██╔══╝ ██╔══██╗',
|
|
445
|
-
'██████╔╝██║ ██║██║ ╚████║██║ ██╗ ╚██████╔╝██║ ██║██║██║ ╚████║██████╔╝███████╗██║ ██║',
|
|
446
|
-
'╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝',
|
|
447
|
-
];
|
|
448
|
-
// Check terminal width — fall back to compact title if too narrow
|
|
449
|
-
const termW = (process.stdout.columns || 120) - 6; // account for box borders
|
|
450
|
-
const useBigTitle = termW >= 92;
|
|
451
|
-
if (useBigTitle) {
|
|
452
|
-
for (let i = 0; i < titleLines.length; i++) {
|
|
453
|
-
const t = i / (titleLines.length - 1);
|
|
454
|
-
const from = t < 0.5
|
|
455
|
-
? [lerp(192, 139, t * 2), lerp(132, 92, t * 2), lerp(252, 246, t * 2)]
|
|
456
|
-
: [lerp(139, 34, (t - 0.5) * 2), lerp(92, 211, (t - 0.5) * 2), lerp(246, 238, (t - 0.5) * 2)];
|
|
457
|
-
lines.push(bRow(` ${c.bold}${gradientLine(titleLines[i], from, [52, 211, 153])}${c.reset}`));
|
|
458
|
-
}
|
|
459
|
-
} else {
|
|
460
|
-
const titleGrad = gradientText(' D A N K G R I N D E R ', [192, 132, 252], [52, 211, 153]);
|
|
461
|
-
lines.push(bRow(` ${c.bold}${titleGrad}${c.reset} ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset}`));
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
lines.push(bRow(` ${D}v${PKG_VERSION}${c.reset} ${G}${spin}${c.reset} ${Y}◷${c.reset} ${D}UP${c.reset} ${c.bold}${Y}${formatUptime()}${c.reset}`));
|
|
465
|
-
|
|
466
|
-
// Subtitle info
|
|
467
|
-
const activeCount = workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
|
|
468
|
-
const invalidCount = workers.filter(w => w._tokenInvalid).length;
|
|
469
|
-
const pausedCount = workers.filter(w => w.paused || w.dashboardPaused).length;
|
|
470
|
-
const recovCount = workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
|
|
471
|
-
const mode = CLOUD_MODE ? `${Cy}CLOUD${c.reset}` : (CLUSTER_ENABLED ? `${Cy}CLUSTER${c.reset}` : `${D}Standalone${c.reset}`);
|
|
472
|
-
const cmdCount = AccountWorker.COMMAND_MAP.length;
|
|
473
|
-
|
|
474
|
-
const netQ = workers.length > 0
|
|
475
|
-
? workers.reduce((s, w) => s + (w._lastPing < 200 ? 1 : w._lastPing < 500 ? 0.5 : 0), 0) / workers.length : 1;
|
|
476
|
-
const netDot = netQ > 0.8 ? `${G}●${c.reset}` : netQ > 0.5 ? `${Y}●${c.reset}` : `${R}●${c.reset}`;
|
|
477
|
-
const netLbl = netQ > 0.8 ? `${G}Good${c.reset}` : netQ > 0.5 ? `${Y}Fair${c.reset}` : `${R}Poor${c.reset}`;
|
|
478
|
-
|
|
479
|
-
const infoParts = [
|
|
480
|
-
mode,
|
|
481
|
-
`${netDot} ${netLbl}`,
|
|
482
|
-
`${B}${cmdCount}${c.reset} ${D}commands${c.reset}`,
|
|
483
|
-
`${G}${activeCount}${c.reset}${D}/${c.reset}${W}${workers.length}${c.reset} ${D}active${c.reset}`,
|
|
484
|
-
];
|
|
485
|
-
if (invalidCount > 0) infoParts.push(`${R}${invalidCount} invalid${c.reset}`);
|
|
486
|
-
if (pausedCount > 0) infoParts.push(`${Y}${pausedCount} paused${c.reset}`);
|
|
487
|
-
if (recovCount > 0) infoParts.push(`${O}${recovCount} recovering${c.reset}`);
|
|
488
|
-
lines.push(bRow(` ${infoParts.join(` ${D}·${c.reset} `)}`));
|
|
489
|
-
|
|
490
|
-
lines.push(bEmpty);
|
|
491
|
-
|
|
492
|
-
// ═══════════════════════════════════════════════════════════════
|
|
493
|
-
// STATS PANEL — left: all metrics | right: big trend + rate
|
|
494
|
-
// ═══════════════════════════════════════════════════════════════
|
|
495
|
-
lines.push(bSep);
|
|
496
|
-
lines.push(bEmpty);
|
|
497
|
-
|
|
498
|
-
const now = Date.now();
|
|
499
|
-
if (now - lastEarningsSample > 8000) { earningsHistory.push(totalCoins); lastEarningsSample = now; }
|
|
500
|
-
const elapsedHrs = (Date.now() - startTime) / 3_600_000;
|
|
501
|
-
const perHr = elapsedHrs > 0.01 ? Math.round(totalCoins / elapsedHrs) : 0;
|
|
502
|
-
|
|
503
|
-
// ── Compute metric values ─────────────────────────────────────
|
|
504
|
-
const cpmVal = globalCmdRate.getRate().toFixed(1);
|
|
505
|
-
const srColor = successRate >= 95 ? G : successRate >= 80 ? Y : R;
|
|
506
|
-
const srBar = progressBar(successRate, 100, 10, successRate >= 95 ? [52, 211, 153] : successRate >= 80 ? [251, 191, 36] : [239, 68, 68]);
|
|
507
|
-
const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
|
|
508
|
-
const memCol = memMB > 900 ? [239, 68, 68] : memMB > 600 ? [251, 191, 36] : [52, 211, 153];
|
|
509
|
-
const memBar = progressBar(memMB, 1024, 10, memCol, [40, 40, 55]);
|
|
510
|
-
const perHrColor = perHr >= 0 ? G : R;
|
|
511
|
-
const perHrSign = perHr >= 0 ? '+' : '';
|
|
512
|
-
const newHighFlag = isNewHigh ? ` ${R}${c.bold}★ NEW HIGH ★${c.reset}` : '';
|
|
513
|
-
|
|
514
|
-
// ── Big trend sparkline (takes ~40% of inner width) ─────────
|
|
515
|
-
const sparkW = Math.max(28, Math.floor(iw * 0.4));
|
|
516
|
-
const spark = drawSparkline(earningsHistory.toArray(), sparkW);
|
|
517
|
-
const sparkLabel = `${A}~${c.reset} ${D}TREND${c.reset}`;
|
|
518
|
-
|
|
519
|
-
// ── Left metric rows (each left-aligned, ANSI-aware padding) ─
|
|
520
|
-
// Helper: ANSI-strip-aware pad — strip ANSI then pad the visible content
|
|
521
|
-
const padRow = (content, totalVis) => {
|
|
522
|
-
const vis = content.replace(RE, '').length;
|
|
523
|
-
return content + ' '.repeat(Math.max(0, totalVis - vis));
|
|
524
|
-
};
|
|
525
|
-
|
|
526
|
-
const leftTotal = iw - sparkW - 10; // reserve space for spark + gap
|
|
527
|
-
const lRow1 = `${Au}⟐${c.reset} ${D}BALANCE${c.reset} ${c.bold}${Au}⏣${c.reset} ${formatCoins(totalBalance)}`;
|
|
528
|
-
const lRow2 = `${G}▲${c.reset} ${D}EARNED${c.reset} ${c.bold}${G}${perHrSign}⏣${c.reset} ${formatCoins(totalCoins)}${newHighFlag}`;
|
|
529
|
-
const lRow3 = `${O}★${c.reset} ${D}PEAK${c.reset} ${c.bold}${O}⏣${c.reset} ${formatCoins(sessionPeakCoins)}`;
|
|
530
|
-
const lRow4 = `${B}◆${c.reset} ${D}CMDS${c.reset} ${c.bold}${B}${totalCommands}${c.reset} ${srColor}${successRate}%${c.reset} ${srBar} ${Cy}${cpmVal}${c.reset}${D}/min${c.reset}`;
|
|
531
|
-
const lRow5 = `${D}≡${c.reset} ${D}MEM${c.reset} ${rgb(memCol[0], memCol[1], memCol[2])}${c.bold}${memMB}MB${c.reset} ${memBar}`;
|
|
532
|
-
|
|
533
|
-
// Build right column label
|
|
534
|
-
const rRate = `${perHrColor}${perHrSign}⏣${c.reset} ${formatCoins(Math.abs(perHr))}/h`;
|
|
535
|
-
|
|
536
|
-
lines.push(bRow(` ${padRow(lRow1, leftTotal)} ${sparkLabel} ${spark}`));
|
|
537
|
-
lines.push(bRow(` ${padRow(lRow2, leftTotal)} ${D}────────${c.reset} ${c.dim}earned${c.reset}`));
|
|
538
|
-
lines.push(bRow(` ${padRow(lRow3, leftTotal)} ${D} ${c.reset} ${rRate}`));
|
|
539
|
-
lines.push(bRow(` ${padRow(lRow4, leftTotal)} ${D} ${c.reset}`));
|
|
540
|
-
lines.push(bRow(` ${padRow(lRow5, leftTotal)} ${D} ${c.reset}`));
|
|
541
|
-
|
|
542
|
-
lines.push(bEmpty);
|
|
543
|
-
|
|
544
|
-
// ═══════════════════════════════════════════════════════════════
|
|
545
|
-
// ACCOUNTS TABLE (sorted by original index, proper alignment)
|
|
546
|
-
// ═══════════════════════════════════════════════════════════════
|
|
547
|
-
lines.push(bSep);
|
|
548
|
-
|
|
549
|
-
// Column widths scale with terminal
|
|
550
|
-
const colNum = 4; // " 1 "
|
|
551
|
-
const colSts = 3; // "● "
|
|
552
|
-
const colMedal = 4; // " 1st" or " "
|
|
553
|
-
const colBal = 12; // "⏣ 999.9M "
|
|
554
|
-
const colEarn = 10; // "+999.9K "
|
|
555
|
-
const colBar = 8; // "████░░░░"
|
|
556
|
-
const fixedW = colNum + colSts + colMedal + colBal + colEarn + colBar + 16; // 16 for spacing
|
|
557
|
-
const colName = Math.max(10, Math.min(22, Math.floor((iw - fixedW) * 0.45)));
|
|
558
|
-
const colActivity = Math.max(10, iw - fixedW - colName);
|
|
559
|
-
|
|
560
|
-
// Header
|
|
561
|
-
const hNum = `${D}${'#'.padStart(colNum)}${c.reset}`;
|
|
562
|
-
const hSts = `${D}${'ST'.padEnd(colSts)}${c.reset}`;
|
|
563
|
-
const hMedal = `${D}${'RNK'.padEnd(colMedal)}${c.reset}`;
|
|
564
|
-
const hName = `${gradientText('Account', [139, 92, 246], [96, 165, 250])}${''.padEnd(Math.max(0, colName - 7))}`;
|
|
565
|
-
const hBal = `${D}${'Balance'.padEnd(colBal)}${c.reset}`;
|
|
566
|
-
const hEarn = `${D}${'Earned'.padEnd(colEarn)}${c.reset}`;
|
|
567
|
-
const hBar = `${D}${''.padEnd(colBar)}${c.reset}`;
|
|
568
|
-
const hAct = `${D}Activity${c.reset}`;
|
|
569
|
-
|
|
570
|
-
lines.push(bRow(` ${hNum} ${hSts} ${hMedal} ${hName} ${hBal} ${hEarn} ${hBar} ${hAct}`));
|
|
571
|
-
lines.push(bRow(` ${D}${'─'.repeat(iw - 2)}${c.reset}`));
|
|
572
|
-
|
|
573
|
-
// Sort workers by original index for consistent display
|
|
574
|
-
const sortedWorkers = [...workers].sort((a, b) => a.idx - b.idx);
|
|
575
|
-
|
|
576
|
-
// Top 3 earners
|
|
577
|
-
const topEarners = [...workers]
|
|
578
|
-
.filter(w => (w.stats.coins || 0) > 0)
|
|
579
|
-
.sort((a, b) => (b.stats.coins || 0) - (a.stats.coins || 0))
|
|
580
|
-
.slice(0, 3);
|
|
581
|
-
const topIds = new Map();
|
|
582
|
-
topEarners.forEach((w, i) => topIds.set(w.account.id, i));
|
|
583
|
-
|
|
584
|
-
const MAX_VIS = 30;
|
|
585
|
-
const visibleWorkers = sortedWorkers.slice(0, MAX_VIS);
|
|
586
|
-
|
|
587
|
-
for (const wk of visibleWorkers) {
|
|
588
|
-
const origNum = (wk.idx + 1).toString().padStart(colNum);
|
|
589
|
-
const rawStat = (wk.lastStatus || 'ready').replace(RE, '');
|
|
590
|
-
const activityText = rawStat.substring(0, colActivity);
|
|
591
|
-
|
|
592
|
-
// ── Status icon ──
|
|
593
|
-
let stsIcon, actLabel;
|
|
594
|
-
const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
|
|
595
|
-
if (wk._tokenInvalid) {
|
|
596
|
-
stsIcon = `${R}✗${c.reset}`;
|
|
597
|
-
actLabel = `${R}TOKEN INVALID${c.reset}`;
|
|
598
|
-
} else if (!wk.running) {
|
|
599
|
-
stsIcon = `${D}○${c.reset}`;
|
|
600
|
-
actLabel = `${D}offline${c.reset}`;
|
|
601
|
-
} else if (isRecov) {
|
|
602
|
-
const sL = Math.ceil((wk._errorCooldownUntil - Date.now()) / 1000);
|
|
603
|
-
stsIcon = `${O}${getSpinner('braille')}${c.reset}`;
|
|
604
|
-
actLabel = `${O}recovering (${sL}s)${c.reset}`;
|
|
605
|
-
} else if (wk.paused) {
|
|
606
|
-
stsIcon = `${R}⏸${c.reset}`;
|
|
607
|
-
actLabel = `${R}PAUSED${c.reset}`;
|
|
608
|
-
} else if (wk.dashboardPaused) {
|
|
609
|
-
stsIcon = `${Y}⏸${c.reset}`;
|
|
610
|
-
actLabel = `${Y}paused${c.reset}`;
|
|
611
|
-
} else if (wk.busy) {
|
|
612
|
-
stsIcon = `${G}${getSpinner('pulse')}${c.reset}`;
|
|
613
|
-
actLabel = `${D}${activityText}${c.reset}`;
|
|
614
|
-
} else {
|
|
615
|
-
stsIcon = `${G}●${c.reset}`;
|
|
616
|
-
actLabel = `${D}${activityText}${c.reset}`;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// ── Medal (fixed 3-char visible width + 1 space) ──
|
|
620
|
-
let medalStr;
|
|
621
|
-
if (topIds.has(wk.account.id)) {
|
|
622
|
-
const rank = topIds.get(wk.account.id);
|
|
623
|
-
if (rank === 0) medalStr = `${Au}1st${c.reset} `;
|
|
624
|
-
else if (rank === 1) medalStr = `${rgb(192, 192, 192)}2nd${c.reset} `;
|
|
625
|
-
else medalStr = `${rgb(205, 127, 50)}3rd${c.reset} `;
|
|
626
|
-
} else {
|
|
627
|
-
medalStr = ' '; // 4 chars: 3 empty + 1 space
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// ── Name (fixed visible width, padded) ──
|
|
631
|
-
const rawName = (wk.username || wk.account.label || '?').substring(0, colName);
|
|
632
|
-
const nameStr = `${wk.color}${c.bold}${rawName.padEnd(colName)}${c.reset}`;
|
|
633
|
-
|
|
634
|
-
// ── Balance (fixed visible width) ──
|
|
635
|
-
let balStr;
|
|
636
|
-
if (wk.stats.balance > 0) {
|
|
637
|
-
balStr = `${Au}⏣${c.reset}${W}${formatCoins(wk.stats.balance).padStart(colBal - 2)}${c.reset}`;
|
|
638
|
-
} else {
|
|
639
|
-
balStr = `${D}⏣${' '.repeat(colBal - 3)}-${c.reset}`;
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// ── Lifesaver indicator ──
|
|
643
|
-
const ls = wk._lifesavers;
|
|
644
|
-
let lsStr;
|
|
645
|
-
if (ls === 0) lsStr = `${R}♥0${c.reset}`;
|
|
646
|
-
else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${c.reset}`;
|
|
647
|
-
else if (ls != null) lsStr = `${G}♥${ls}${c.reset}`;
|
|
648
|
-
else {
|
|
649
|
-
// Unknown — pulse to show it's still being determined
|
|
650
|
-
const pulse = PULSE_CHARS[Math.floor(Date.now() / 400) % PULSE_CHARS.length];
|
|
651
|
-
lsStr = `${D}${pulse}♥?${c.reset}`;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
// ── Level indicator (fixed width so value changes don't jitter) ──
|
|
655
|
-
const lvl = wk._level || 0;
|
|
656
|
-
const lvlStr = lvl > 0 ? `${Cy}L${String(lvl).padStart(3)}${c.reset}` : `${D}L???${c.reset}`;
|
|
657
|
-
|
|
658
|
-
// ── Earned (fixed visible width) ──
|
|
659
|
-
const earnNum = wk.stats.coins || 0;
|
|
660
|
-
let earnStr;
|
|
661
|
-
if (earnNum > 0) {
|
|
662
|
-
earnStr = `${G}+${formatCoins(earnNum).padEnd(colEarn - 1)}${c.reset}`;
|
|
663
|
-
} else if (earnNum < 0) {
|
|
664
|
-
earnStr = `${R}${formatCoins(earnNum).padEnd(colEarn)}${c.reset}`;
|
|
665
|
-
} else {
|
|
666
|
-
earnStr = `${D}${'-'.padEnd(colEarn)}${c.reset}`;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// ── Progress bar (fixed width) ──
|
|
670
|
-
const earnBarFill = earnNum > 0 ? Math.min(colBar, Math.max(1, Math.floor(Math.log10(earnNum + 1)))) : 0;
|
|
671
|
-
const earnBar = progressBar(earnBarFill, colBar, colBar, [52, 211, 153], [40, 40, 55]);
|
|
672
|
-
|
|
673
|
-
lines.push(bRow(` ${D}${origNum}${c.reset} ${stsIcon} ${medalStr}${nameStr} ${balStr} ${lvlStr} ${lsStr} ${earnStr} ${earnBar} ${actLabel}`));
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// Overflow summary
|
|
677
|
-
if (sortedWorkers.length > MAX_VIS) {
|
|
678
|
-
const rest = sortedWorkers.length - MAX_VIS;
|
|
679
|
-
let ha = 0, hp = 0, hr = 0, ho = 0, hi = 0;
|
|
680
|
-
for (let i = MAX_VIS; i < sortedWorkers.length; i++) {
|
|
681
|
-
const w = sortedWorkers[i];
|
|
682
|
-
if (w._tokenInvalid) hi++;
|
|
683
|
-
else if (!w.running) ho++;
|
|
684
|
-
else if (w.paused || w.dashboardPaused) hp++;
|
|
685
|
-
else if (w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()) hr++;
|
|
686
|
-
else ha++;
|
|
687
|
-
}
|
|
688
|
-
const parts = [`${D}+${rest} more${c.reset}`];
|
|
689
|
-
if (ha > 0) parts.push(`${G}${ha} active${c.reset}`);
|
|
690
|
-
if (hp > 0) parts.push(`${Y}${hp} paused${c.reset}`);
|
|
691
|
-
if (hr > 0) parts.push(`${O}${hr} recovering${c.reset}`);
|
|
692
|
-
if (hi > 0) parts.push(`${R}${hi} invalid${c.reset}`);
|
|
693
|
-
if (ho > 0) parts.push(`${D}${ho} offline${c.reset}`);
|
|
694
|
-
lines.push(bRow(` ${parts.join(` ${D}·${c.reset} `)}`));
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
// ═══════════════════════════════════════════════════════════════
|
|
698
|
-
// HEALTH BAR
|
|
699
|
-
// ═══════════════════════════════════════════════════════════════
|
|
700
|
-
const totalRecoveries = workers.reduce((s, w) => s + (w._totalRecoveries || 0), 0);
|
|
701
|
-
const totalDisconnects = workers.reduce((s, w) => s + (w._disconnectCount || 0), 0);
|
|
702
|
-
if (recovCount > 0 || pausedCount > 0 || invalidCount > 0 || totalRecoveries > 0 || totalDisconnects > 0) {
|
|
703
|
-
lines.push(bMid);
|
|
704
|
-
const hParts = [];
|
|
705
|
-
if (invalidCount > 0) hParts.push(`${R}✗ ${invalidCount} invalid${c.reset}`);
|
|
706
|
-
if (recovCount > 0) hParts.push(`${O}${getSpinner('braille')} ${recovCount} recovering${c.reset}`);
|
|
707
|
-
if (pausedCount > 0) hParts.push(`${Y}⏸ ${pausedCount} paused${c.reset}`);
|
|
708
|
-
if (totalRecoveries > 0) hParts.push(`${D}${totalRecoveries} auto-healed${c.reset}`);
|
|
709
|
-
if (totalDisconnects > 0) hParts.push(`${D}${totalDisconnects} reconnects${c.reset}`);
|
|
710
|
-
lines.push(bRow(` ${D}HEALTH${c.reset} ${hParts.join(` ${D}·${c.reset} `)}`));
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
// Cluster
|
|
714
|
-
if (CLUSTER_ENABLED) {
|
|
715
|
-
const nodeShort = NODE_ID.substring(0, 12);
|
|
716
|
-
const claimedCount = workers.filter(w => w.running).length;
|
|
717
|
-
lines.push(bRow(` ${Cy}CLUSTER${c.reset} ${D}node:${c.reset} ${Cy}${nodeShort}${c.reset} ${D}claimed:${c.reset} ${W}${claimedCount}${c.reset}`));
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
// ═══════════════════════════════════════════════════════════════
|
|
721
|
-
// LIVE LOG FEED
|
|
722
|
-
// ═══════════════════════════════════════════════════════════════
|
|
723
|
-
const logEntries = recentLogs.toArray();
|
|
724
|
-
if (logEntries.length > 0) {
|
|
725
|
-
lines.push(bSep);
|
|
726
|
-
lines.push(bRow(` ${gradientText('LIVE FEED', [139, 92, 246], [52, 211, 153])} ${G}${getSpinner('pulse')}${c.reset}`));
|
|
727
|
-
lines.push(bMid);
|
|
728
|
-
for (const entry of logEntries) {
|
|
729
|
-
lines.push(bRow(` ${D}${entry}${c.reset}`));
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// ═══════════════════════════════════════════════════════════════
|
|
734
|
-
lines.push(bEmpty);
|
|
735
|
-
lines.push(bBot);
|
|
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
|
+
});
|
|
736
411
|
|
|
737
|
-
|
|
738
|
-
process.stdout.write('\x1b[H');
|
|
739
|
-
for (const line of lines) {
|
|
740
|
-
process.stdout.write(c.clearLine + '\r' + line + '\n');
|
|
741
|
-
}
|
|
742
|
-
process.stdout.write('\x1b[J');
|
|
743
|
-
dashboardLines = lines.length;
|
|
412
|
+
if (newLines != null) dashboardLines = newLines;
|
|
744
413
|
dashboardRendering = false;
|
|
745
414
|
}
|
|
746
415
|
|