dankgrinder 7.82.0 → 8.1.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 CHANGED
@@ -1,6 +1,271 @@
1
1
  /**
2
- * CLI Dashboard — REMOVED
3
- * Dashboard functionality has been removed from the CLI.
4
- * All startup output goes through simple console.log.
2
+ * DankGrinder CLI Dashboard
3
+ *
4
+ * Fresh rewrite. Simple line-by-line overwrite no full clear, no cursor tricks.
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
5
14
  */
6
- module.exports = {};
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 };