dankgrinder 8.73.0 → 8.74.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/ui.js +241 -196
- package/package.json +1 -1
package/lib/ui.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI Live Dashboard — cursor-positioned
|
|
3
|
-
*
|
|
2
|
+
* CLI Live Dashboard — cursor-positioned, fixed-width columns.
|
|
3
|
+
* Design: Option 1 box + animated dot status + current logs + events box below.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
let _startTime = Date.now();
|
|
@@ -10,97 +10,79 @@ let _live = false;
|
|
|
10
10
|
let _phase = 'init';
|
|
11
11
|
|
|
12
12
|
// Terminal dimensions
|
|
13
|
-
let _W =
|
|
14
|
-
let _inner =
|
|
13
|
+
let _W = 110;
|
|
14
|
+
let _inner = 108;
|
|
15
15
|
let _maxAccounts = 4;
|
|
16
16
|
|
|
17
|
-
// Row map
|
|
18
|
-
let _accountRows = []; // _accountRows[accountIdx] = row
|
|
19
|
-
let _totalsRow = 0;
|
|
17
|
+
// Row map
|
|
18
|
+
let _accountRows = []; // _accountRows[accountIdx] = starting row
|
|
20
19
|
let _bottomRow = 0;
|
|
21
20
|
|
|
22
|
-
// ──
|
|
21
|
+
// ── Fixed column widths ─────────────────────────────────────────
|
|
22
|
+
// # | ACCOUNT | BAL | LS | LV | LOGS
|
|
23
|
+
// 3 19 12 3 4 rest
|
|
24
|
+
const NUM_W = 3; // " # "
|
|
25
|
+
const ACC_W = 19; // dot + name (padded to 18, dot takes 1)
|
|
26
|
+
const BAL_W = 12; // "⏣95,230" padded
|
|
27
|
+
const LS_W = 3; // lifesavers
|
|
28
|
+
const LV_W = 4; // level
|
|
29
|
+
// ROW = '│ ' + # + ' ' + dot + name + ' ' + bal + ' ' + ls + ' ' + lv + ' ' + logs + ' │'
|
|
30
|
+
// = 2 + 2 + 1 + 1 + 18 + 1 + 12 + 1 + 3 + 1 + 4 + 1 + N + 2 = 50 + N
|
|
31
|
+
// LOGS_W computed dynamically
|
|
32
|
+
|
|
33
|
+
// ── Spinner frames ──────────────────────────────────────────────
|
|
23
34
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
24
35
|
function spinnerFrame() { return SPINNER[Math.floor(Date.now() / 150) % SPINNER.length]; }
|
|
25
36
|
|
|
26
|
-
// ──
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
'
|
|
30
|
-
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
// ── ANSI helpers ──────────────────────────────────────────────
|
|
37
|
+
// ── Status dot + color ───────────────────────────────────────────
|
|
38
|
+
const STATUS_DOT = {
|
|
39
|
+
online: { dot: '●', color: '\x1b[38;2;80;255;120m' }, // green
|
|
40
|
+
busy: { dot: '◐', color: '\x1b[38;2;255;220;80m' }, // yellow
|
|
41
|
+
paused: { dot: '○', color: '\x1b[38;2;180;180;180m' }, // dim gray
|
|
42
|
+
dead: { dot: '✗', color: '\x1b[38;2;255;80;100m' }, // red
|
|
43
|
+
connect: { dot: '◯', color: '\x1b[38;2;255;180;80m' }, // orange
|
|
44
|
+
};
|
|
45
|
+
function getDot(w) {
|
|
46
|
+
if (!w.channel) return STATUS_DOT.connect;
|
|
47
|
+
if (w.paused || w.dashboardPaused) return STATUS_DOT.paused;
|
|
48
|
+
if (w._alert?.type === 'death') return STATUS_DOT.dead;
|
|
49
|
+
if (w.busy || w._invRunning || w._sellRunning) return STATUS_DOT.busy;
|
|
50
|
+
if (w.globalCooldownUntil && Date.now() < w.globalCooldownUntil) return STATUS_DOT.busy;
|
|
51
|
+
return STATUS_DOT.online;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── ANSI helpers ─────────────────────────────────────────────────
|
|
46
55
|
const c = {
|
|
47
56
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
48
57
|
green: '\x1b[38;2;80;255;120m',
|
|
49
58
|
red: '\x1b[38;2;255;80;100m',
|
|
50
59
|
yellow: '\x1b[38;2;255;220;80m',
|
|
51
60
|
cyan: '\x1b[38;2;80;220;255m',
|
|
52
|
-
white: '\x1b[37m',
|
|
53
61
|
};
|
|
54
62
|
const DIM = c.dim;
|
|
55
|
-
const B = '\x1b[38;2;77;212;238m'; // box border
|
|
63
|
+
const B = '\x1b[38;2;77;212;238m'; // box border blue
|
|
56
64
|
|
|
57
65
|
function row(n) { process.stdout.write(`\x1b[${n};1H`); }
|
|
58
66
|
function clrLine() { process.stdout.write('\x1b[2K'); }
|
|
67
|
+
function ln() { process.stdout.write('\n'); }
|
|
59
68
|
function trunc(s, n) { s = String(s || ''); return s.length <= n ? s : s.slice(0, n - 1) + '…'; }
|
|
60
69
|
function padR(s, n) { return trunc(s, n).padEnd(n); }
|
|
61
70
|
function padL(s, n, char) { return String(s).padStart(n, char || ' '); }
|
|
62
|
-
function stripAnsi(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, ''); }
|
|
63
|
-
function ln() { process.stdout.write('\n'); }
|
|
71
|
+
function stripAnsi(s) { return String(s || '').replace(/\x1b\[[0-9;]*m/g, ''); }
|
|
64
72
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
return
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ── Status helpers ─────────────────────────────────────────────
|
|
75
|
-
function statusColor(w) {
|
|
76
|
-
if (!w.channel) return c.red;
|
|
77
|
-
if (w.paused || w.dashboardPaused) return c.yellow;
|
|
78
|
-
if (w.busy || w._invRunning || w._sellRunning) return c.yellow;
|
|
79
|
-
if (Date.now() < w.globalCooldownUntil) return c.cyan;
|
|
80
|
-
return c.green;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function statusText(w) {
|
|
84
|
-
if (!w.channel) return spinnerFrame() + ' CONN';
|
|
85
|
-
if (w.paused) return 'PAUSED';
|
|
86
|
-
if (w.dashboardPaused) return 'HELD';
|
|
87
|
-
if (w._alert?.type === 'death') return 'DEAD';
|
|
88
|
-
if (w._alert?.type === 'lowls') return 'LOW LS';
|
|
89
|
-
if (w.busy || w._invRunning || w._sellRunning) return spinnerFrame() + ' WORK';
|
|
90
|
-
if (Date.now() < w.globalCooldownUntil) {
|
|
91
|
-
const wait = Math.ceil((w.globalCooldownUntil - Date.now()) / 1000);
|
|
92
|
-
return '⏳' + padL(wait, 3) + 's';
|
|
93
|
-
}
|
|
94
|
-
return '● READY';
|
|
73
|
+
// ── Format helpers ───────────────────────────────────────────────
|
|
74
|
+
function fmtCoins(n) {
|
|
75
|
+
if (!n && n !== 0) return '—';
|
|
76
|
+
if (n >= 1_000_000) return '+' + (n / 1_000_000).toFixed(1) + 'M';
|
|
77
|
+
if (n >= 1_000) return '+' + (n / 1_000).toFixed(1) + 'k';
|
|
78
|
+
if (n > 0) return '+' + n;
|
|
79
|
+
return '—';
|
|
95
80
|
}
|
|
96
81
|
|
|
97
|
-
|
|
98
|
-
function fmtCoins(n) {
|
|
82
|
+
function fmtBal(n) {
|
|
99
83
|
if (!n && n !== 0) return DIM + '—' + c.reset;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (n > 0) return c.green + '+' + n + c.reset;
|
|
103
|
-
return DIM + '—' + c.reset;
|
|
84
|
+
const s = n >= 1_000 ? n.toLocaleString() : String(n);
|
|
85
|
+
return c.green + '⏣' + s + c.reset;
|
|
104
86
|
}
|
|
105
87
|
|
|
106
88
|
function fmtLifesavers(w) {
|
|
@@ -114,130 +96,133 @@ function fmtLifesavers(w) {
|
|
|
114
96
|
function fmtLevel(w) {
|
|
115
97
|
const lv = w._level;
|
|
116
98
|
if (!lv) return DIM + '—' + c.reset;
|
|
117
|
-
return c.cyan + lv + c.reset;
|
|
99
|
+
return c.cyan + padL(lv, 4) + c.reset;
|
|
118
100
|
}
|
|
119
101
|
|
|
120
|
-
// ── Layout
|
|
102
|
+
// ── Layout ─────────────────────────────────────────────────────
|
|
121
103
|
function layout() {
|
|
122
|
-
_W = Math.min(process.stdout.columns ||
|
|
104
|
+
_W = Math.min(process.stdout.columns || 110, 130);
|
|
123
105
|
_inner = _W - 2;
|
|
124
106
|
const rows = process.stdout.rows || 40;
|
|
107
|
+
// Main box: banner(6) + status(1) + divider(1) + header(1) + hr(1) + accounts + totals(2) + bottom(1) = 13 + accounts
|
|
125
108
|
_maxAccounts = Math.min(_workers.length, Math.max(3, rows - 16));
|
|
126
109
|
}
|
|
127
110
|
|
|
128
|
-
// ──
|
|
129
|
-
function
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
col.name += nameExtra;
|
|
135
|
-
return col;
|
|
111
|
+
// ── Get LOGS column width ─────────────────────────────────────────
|
|
112
|
+
function getLogsW() {
|
|
113
|
+
// ROW = │ + # + ' ' + dot+name + ' ' + bal + ' ' + ls + ' ' + lv + ' ' + logs + ' │ + \n
|
|
114
|
+
// 1 2 1 19 1 12 1 3 1 4 1 N 1 1 = 48+N
|
|
115
|
+
const FIXED = NUM_W + 1 + ACC_W + 1 + BAL_W + 1 + LS_W + 1 + LV_W + 1 + 2;
|
|
116
|
+
return Math.max(20, _inner - FIXED);
|
|
136
117
|
}
|
|
137
118
|
|
|
138
|
-
// ── Build account row
|
|
139
|
-
function accountRow(w, wi
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
const
|
|
144
|
-
const bal = w.stats.balance !== undefined ? fmtCoins(w.stats.balance) : DIM + '?' + c.reset;
|
|
119
|
+
// ── Build one account row ─────────────────────────────────────────
|
|
120
|
+
function accountRow(w, wi) {
|
|
121
|
+
const LOG_W = getLogsW();
|
|
122
|
+
const dot = getDot(w);
|
|
123
|
+
const name = padR(trunc(w.username || '?', ACC_W - 1), ACC_W - 1); // -1 for dot
|
|
124
|
+
const bal = w.stats.balance !== undefined ? w.stats.balance : null;
|
|
145
125
|
const ls = fmtLifesavers(w);
|
|
146
126
|
const lv = fmtLevel(w);
|
|
147
|
-
const name = padR(trunc(w.username || '?', col.name), col.name);
|
|
148
|
-
const doing = padR(w.lastStatus || 'idle', col.cmd);
|
|
149
|
-
const earnedStr = stripAnsi(earned);
|
|
150
|
-
const balStr = stripAnsi(bal);
|
|
151
127
|
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
128
|
+
// Current log: lastStatus or cooldown
|
|
129
|
+
let logText = w.lastStatus || 'idle';
|
|
130
|
+
if (w.globalCooldownUntil && Date.now() < w.globalCooldownUntil) {
|
|
131
|
+
const s = Math.ceil((w.globalCooldownUntil - Date.now()) / 1000);
|
|
132
|
+
logText = s > 60 ? `cooldown ${Math.ceil(s/60)}m` : `cooldown ${s}s`;
|
|
133
|
+
}
|
|
134
|
+
if (w.paused || w.dashboardPaused) logText = 'paused';
|
|
135
|
+
if (w._alert?.type === 'death') logText = 'DEAD — lifesavers?';
|
|
136
|
+
const logPadded = padR(logText, LOG_W);
|
|
137
|
+
|
|
138
|
+
const numStr = padL(String(wi + 1), NUM_W - 1); // " 1" or "10"
|
|
155
139
|
|
|
156
140
|
return (
|
|
157
|
-
`${B}│\x1b[0m
|
|
158
|
-
`${DIM}${
|
|
159
|
-
`${
|
|
160
|
-
`${
|
|
161
|
-
`${
|
|
162
|
-
`${
|
|
163
|
-
`${
|
|
164
|
-
`${padL(lv, col.lv)} ` +
|
|
165
|
-
`${earnedPadded} ` +
|
|
141
|
+
`${B}│\x1b[0m` +
|
|
142
|
+
`${DIM}${numStr}${c.reset} ` +
|
|
143
|
+
`${dot.dot}${dot.color}${name}${c.reset} ` +
|
|
144
|
+
`${fmtBal(bal)} ` +
|
|
145
|
+
`${ls} ` +
|
|
146
|
+
`${lv} ` +
|
|
147
|
+
`${DIM}${logPadded}${c.reset} ` +
|
|
166
148
|
`${B}│\x1b[0m`
|
|
167
149
|
);
|
|
168
150
|
}
|
|
169
151
|
|
|
170
|
-
// ──
|
|
152
|
+
// ── Top border helper ─────────────────────────────────────────────
|
|
153
|
+
function hRule(char) {
|
|
154
|
+
process.stdout.write(`${B}│\x1b[0m${char.repeat(_inner)}${B}│`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Header row ───────────────────────────────────────────────────
|
|
158
|
+
function headerRow(LOG_W) {
|
|
159
|
+
return (
|
|
160
|
+
`${B}│\x1b[0m` +
|
|
161
|
+
`${c.bold}${padL('#', NUM_W - 1)}${c.reset} ` +
|
|
162
|
+
`${c.bold}${padR('ACCOUNT', ACC_W)}${c.reset} ` +
|
|
163
|
+
`${c.bold}${padL('BAL', BAL_W)}${c.reset} ` +
|
|
164
|
+
`${c.bold}${padL('LS', LS_W)}${c.reset} ` +
|
|
165
|
+
`${c.bold}${padL('LV', LV_W)}${c.reset} ` +
|
|
166
|
+
`${c.bold}${padR('LOGS', LOG_W)}${c.reset} ` +
|
|
167
|
+
`${B}│\x1b[0m`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Draw FULL box (called once on startup) ─────────────────────
|
|
171
172
|
function draw() {
|
|
172
173
|
layout();
|
|
173
|
-
const
|
|
174
|
-
const bannerH = BANNER_LINES.length;
|
|
174
|
+
const LOG_W = getLogsW();
|
|
175
175
|
|
|
176
|
-
// Clear
|
|
176
|
+
// Clear screen + home
|
|
177
177
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
178
178
|
|
|
179
179
|
let r = 1;
|
|
180
|
+
const top = _inner;
|
|
180
181
|
|
|
181
182
|
// ── Top border ──
|
|
182
183
|
row(r++); clrLine();
|
|
183
|
-
process.stdout.write(`${B}┌${'─'.repeat(
|
|
184
|
+
process.stdout.write(`${B}┌${'─'.repeat(top)}┐`);
|
|
184
185
|
ln();
|
|
185
186
|
|
|
186
|
-
// ──
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const ansiLen = stripAnsi(gradLine).length;
|
|
193
|
-
const totalContent = 2 + ansiLen; // left spaces + content
|
|
194
|
-
process.stdout.write(`${B}│\x1b[0m ${gradLine}${' '.repeat(Math.max(0, _inner - totalContent - 1))}${B}│`);
|
|
195
|
-
} else {
|
|
196
|
-
const ansiLen = stripAnsi(line).length;
|
|
197
|
-
const totalContent = 2 + ansiLen;
|
|
198
|
-
process.stdout.write(`${B}│\x1b[0m ${DIM}${line}${c.reset}${' '.repeat(Math.max(0, _inner - totalContent - 1))}${B}│`);
|
|
199
|
-
}
|
|
200
|
-
ln();
|
|
201
|
-
}
|
|
187
|
+
// ── Title bar ──
|
|
188
|
+
const up = fmtUptime();
|
|
189
|
+
let totalCoins = 0, totalBal = 0;
|
|
190
|
+
for (const w of _workers) { totalCoins += w.stats.coins || 0; totalBal += w.stats.balance || 0; }
|
|
191
|
+
const totalBalStr = totalBal > 0 ? c.green + '⏣' + totalBal.toLocaleString() + c.reset : DIM + '—' + c.reset;
|
|
192
|
+
const online = _workers.filter(w => w.channel && !w.paused && !w.dashboardPaused).length;
|
|
202
193
|
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
const
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
`${c.green}●${c.reset} ${running} online`,
|
|
210
|
-
paused > 0 ? `${c.yellow}~${c.reset} ${paused} paused` : null,
|
|
211
|
-
errors > 0 ? `${c.red}E${c.reset} ${errors} error` : null,
|
|
212
|
-
`${DIM}Ctrl+C${c.reset}`,
|
|
213
|
-
].filter(Boolean);
|
|
214
|
-
const statusStr = statusParts.join(' ') + phaseLabel;
|
|
194
|
+
const titleLeft = `${c.bold}DANKGRINDER${c.reset} ${c.dim}v${_version}${c.reset}`;
|
|
195
|
+
const titleRight = `${c.green}●${c.reset} ${online} online ${totalBalStr} uptime ${up}`;
|
|
196
|
+
const totalW = stripAnsi(titleLeft).length + stripAnsi(titleRight).length;
|
|
197
|
+
const padding = Math.max(0, top - totalW);
|
|
198
|
+
const leftPad = Math.floor(padding / 2);
|
|
199
|
+
const rightPad = padding - leftPad;
|
|
215
200
|
|
|
216
201
|
row(r++); clrLine();
|
|
217
|
-
process.stdout.write(`${B}│\x1b[0m ${
|
|
202
|
+
process.stdout.write(`${B}│\x1b[0m${' '.repeat(leftPad)}${titleLeft}${' '.repeat(rightPad)}${titleRight}${B}│`);
|
|
218
203
|
ln();
|
|
219
204
|
|
|
220
205
|
// ── Divider ──
|
|
221
206
|
row(r++); clrLine();
|
|
222
|
-
process.stdout.write(`${B}├${'─'.repeat(
|
|
207
|
+
process.stdout.write(`${B}├${'─'.repeat(top)}┤`);
|
|
223
208
|
ln();
|
|
224
209
|
|
|
225
|
-
// ──
|
|
210
|
+
// ── Column header ──
|
|
226
211
|
row(r++); clrLine();
|
|
227
|
-
process.stdout.write(
|
|
212
|
+
process.stdout.write(headerRow(LOG_W));
|
|
228
213
|
ln();
|
|
229
214
|
|
|
230
215
|
// ── HR ──
|
|
231
216
|
row(r++); clrLine();
|
|
232
|
-
process.stdout.write(`${B}│\x1b[0m
|
|
217
|
+
process.stdout.write(`${B}│\x1b[0m${'─'.repeat(top)}${B}│`);
|
|
233
218
|
ln();
|
|
234
219
|
|
|
235
220
|
// ── Account rows ──
|
|
236
221
|
const sorted = [..._workers].sort((a, b) => {
|
|
237
|
-
if (!a.channel !== !b.channel) return
|
|
222
|
+
if (!a.channel !== !b.channel) return a.channel ? -1 : 1;
|
|
238
223
|
const aA = a.channel && !a.paused && !a.dashboardPaused;
|
|
239
|
-
const
|
|
240
|
-
if (aA !==
|
|
224
|
+
const bB = b.channel && !b.paused && !b.dashboardPaused;
|
|
225
|
+
if (aA !== bB) return aA ? -1 : 1;
|
|
241
226
|
return (b.stats.commands || 0) - (a.stats.commands || 0);
|
|
242
227
|
});
|
|
243
228
|
const shown = sorted.slice(0, _maxAccounts);
|
|
@@ -248,55 +233,105 @@ function draw() {
|
|
|
248
233
|
const wi = _workers.indexOf(w);
|
|
249
234
|
_accountRows[wi] = r;
|
|
250
235
|
row(r++); clrLine();
|
|
251
|
-
process.stdout.write(accountRow(w, wi
|
|
236
|
+
process.stdout.write(accountRow(w, wi));
|
|
252
237
|
ln();
|
|
253
238
|
}
|
|
254
239
|
|
|
255
240
|
// ── Totals divider ──
|
|
256
|
-
_totalsRow = r;
|
|
257
241
|
row(r++); clrLine();
|
|
258
|
-
process.stdout.write(`${B}│\x1b[0m
|
|
242
|
+
process.stdout.write(`${B}│\x1b[0m${'─'.repeat(top)}${B}│`);
|
|
259
243
|
ln();
|
|
260
244
|
|
|
261
|
-
// ── Totals ──
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
const balStr = totalBal > 0 ? '⏣' + totalBal.toLocaleString() : '—';
|
|
269
|
-
const balPadded = balStr.padEnd(col.bal);
|
|
270
|
-
const earnedStr = stripAnsi(fmtCoins(totalCoins));
|
|
245
|
+
// ── Totals row ──
|
|
246
|
+
const coinsStr = fmtCoins(totalCoins);
|
|
247
|
+
const coinsLabel = `${c.bold}Σ${c.reset} ${_workers.length} accounts`;
|
|
248
|
+
const coinsVal = `${c.green}${coinsStr}${c.reset} coins`;
|
|
249
|
+
const valLen = stripAnsi(coinsVal);
|
|
250
|
+
const labelLen = stripAnsi(coinsLabel);
|
|
251
|
+
const gap = Math.max(0, top - valLen - labelLen - 4);
|
|
271
252
|
|
|
272
253
|
row(r++); clrLine();
|
|
273
|
-
process.stdout.write(
|
|
274
|
-
`${B}│\x1b[0m ${c.bold}Σ${c.reset} ${DIM}${padL(_workers.length, 2)} acc${c.reset} ${' '.repeat(col.name + col.cmd)} ${balPadded} ${' '.repeat(col.ls + col.lv)} ${fmtCoins(totalCoins)}${' '.repeat(_inner - 2 - stripAnsi(` ${c.bold}Σ${c.reset} ${DIM}${padL(_workers.length, 2)} acc${c.reset} ${' '.repeat(col.name + col.cmd)} ${balPadded} ${' '.repeat(col.ls + col.lv)} ${fmtCoins(totalCoins)}`))}${B}│`
|
|
275
|
-
);
|
|
254
|
+
process.stdout.write(`${B}│\x1b[0m${' '.repeat(2)}${coinsLabel}${' '.repeat(gap)}${coinsVal}${' '.repeat(2)}${B}│`);
|
|
276
255
|
ln();
|
|
277
256
|
|
|
278
|
-
// ── Bottom ──
|
|
257
|
+
// ── Bottom border ──
|
|
279
258
|
_bottomRow = r;
|
|
280
259
|
row(r++); clrLine();
|
|
281
|
-
process.stdout.write(`${B}└${'─'.repeat(
|
|
260
|
+
process.stdout.write(`${B}└${'─'.repeat(top)}┘`);
|
|
261
|
+
ln();
|
|
262
|
+
|
|
263
|
+
// ── Events box header ──
|
|
264
|
+
r++; // blank row
|
|
265
|
+
row(r++); clrLine();
|
|
266
|
+
process.stdout.write(`${B}┌${'─'.repeat(top)}┐`);
|
|
267
|
+
ln();
|
|
268
|
+
row(r++); clrLine();
|
|
269
|
+
process.stdout.write(`${B}│\x1b[0m ${c.bold}EVENTS${c.reset}${' '.repeat(top - 10)}${B}│`);
|
|
270
|
+
ln();
|
|
271
|
+
row(r++); clrLine();
|
|
272
|
+
process.stdout.write(`${B}├${'─'.repeat(top)}┤`);
|
|
282
273
|
ln();
|
|
283
274
|
}
|
|
284
275
|
|
|
285
|
-
// ── Update ONE account row in-place
|
|
276
|
+
// ── Update ONE account row in-place ───────────────────────────────
|
|
286
277
|
function updateAccountRow(accountIdx) {
|
|
287
278
|
if (!_live) return;
|
|
288
279
|
const rowNum = _accountRows[accountIdx];
|
|
289
|
-
if (!rowNum) return;
|
|
280
|
+
if (!rowNum) return;
|
|
290
281
|
const w = _workers[accountIdx];
|
|
291
282
|
if (!w) return;
|
|
292
|
-
const col = getCol();
|
|
293
|
-
|
|
294
283
|
row(rowNum); clrLine();
|
|
295
|
-
process.stdout.write(accountRow(w, accountIdx
|
|
284
|
+
process.stdout.write(accountRow(w, accountIdx));
|
|
285
|
+
ln();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Event log tracking ─────────────────────────────────────────────
|
|
289
|
+
const MAX_EVENTS = 8;
|
|
290
|
+
let _events = [];
|
|
291
|
+
|
|
292
|
+
function addEvent(type, msg) {
|
|
293
|
+
const now = new Date();
|
|
294
|
+
const ts = `${padL(now.getHours(), 2, '0')}:${padL(now.getMinutes(), 2, '0')}:${padL(now.getSeconds(), 2, '0')}`;
|
|
295
|
+
const icons = { info: 'ℹ', warn: '⚠', error: '✗', success: '✔', debug: '⌘' };
|
|
296
|
+
const colors = { info: c.cyan, warn: c.yellow, error: c.red, success: c.green, debug: DIM };
|
|
297
|
+
const icon = icons[type] || 'ℹ';
|
|
298
|
+
const col = colors[type] || c.cyan;
|
|
299
|
+
|
|
300
|
+
_events.unshift({ icon, col, msg, ts });
|
|
301
|
+
if (_events.length > MAX_EVENTS) _events.pop();
|
|
302
|
+
if (_live) drawEvents();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── Draw events box ───────────────────────────────────────────────
|
|
306
|
+
function drawEvents() {
|
|
307
|
+
if (!_live) return;
|
|
308
|
+
const LOG_W = getLogsW();
|
|
309
|
+
const inner = _inner;
|
|
310
|
+
|
|
311
|
+
// Draw from _bottomRow + 2 (after the events box header rows)
|
|
312
|
+
let r = _bottomRow + 2;
|
|
313
|
+
|
|
314
|
+
for (let i = 0; i < MAX_EVENTS; i++) {
|
|
315
|
+
row(r); clrLine();
|
|
316
|
+
if (i < _events.length) {
|
|
317
|
+
const ev = _events[i];
|
|
318
|
+
const msgPadded = padR(`${ev.icon} ${ev.msg}`, inner - 30);
|
|
319
|
+
const tsPadded = padL(`[${ev.ts}]`, 20);
|
|
320
|
+
process.stdout.write(`${B}│\x1b[0m ${ev.col}${msgPadded}${c.reset}${tsPadded} ${B}│`);
|
|
321
|
+
} else {
|
|
322
|
+
process.stdout.write(`${B}│\x1b[0m${' '.repeat(inner)}${B}│`);
|
|
323
|
+
}
|
|
324
|
+
ln();
|
|
325
|
+
r++;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Bottom border of events box
|
|
329
|
+
row(r); clrLine();
|
|
330
|
+
process.stdout.write(`${B}└${'─'.repeat(inner)}┘`);
|
|
296
331
|
ln();
|
|
297
332
|
}
|
|
298
333
|
|
|
299
|
-
// ── Gradient line
|
|
334
|
+
// ── Gradient line (unused in new design but kept) ─────────────────
|
|
300
335
|
function gradientLine(text, r1, g1, b1, r2, g2, b2) {
|
|
301
336
|
let out = '';
|
|
302
337
|
for (let i = 0; i < text.length; i++) {
|
|
@@ -306,17 +341,21 @@ function gradientLine(text, r1, g1, b1, r2, g2, b2) {
|
|
|
306
341
|
return out + '\x1b[0m';
|
|
307
342
|
}
|
|
308
343
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
344
|
+
function fmtUptime() {
|
|
345
|
+
const s = Math.floor((Date.now() - _startTime) / 1000);
|
|
346
|
+
if (s < 60) return `${s}s`;
|
|
347
|
+
const m = Math.floor(s / 60);
|
|
348
|
+
const h = Math.floor(m / 60);
|
|
349
|
+
if (h > 0) return `${h}h${m % 60}m`;
|
|
350
|
+
return `${m}m`;
|
|
351
|
+
}
|
|
314
352
|
|
|
315
|
-
|
|
353
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
354
|
+
function init({ workers }) {
|
|
316
355
|
_startTime = Date.now();
|
|
317
356
|
_workers = workers;
|
|
318
357
|
_version = '0.0.0';
|
|
319
|
-
|
|
358
|
+
_events = [];
|
|
320
359
|
_live = false;
|
|
321
360
|
_phase = 'init';
|
|
322
361
|
_accountRows = [];
|
|
@@ -324,47 +363,53 @@ function init({ workers, isShuttingDown }) {
|
|
|
324
363
|
|
|
325
364
|
function drawBanner(version) { _version = version || '0.0.0'; }
|
|
326
365
|
function start() {}
|
|
327
|
-
|
|
328
366
|
function stop() {
|
|
329
367
|
_live = false;
|
|
330
|
-
_phase = 'init';
|
|
331
368
|
process.stdout.write('\x1b[2J\x1b[H' + c.reset + '\n');
|
|
332
369
|
}
|
|
333
|
-
|
|
334
370
|
function setLive(val) { _live = val; }
|
|
335
371
|
function setPhase(phase) { _phase = phase; }
|
|
336
372
|
|
|
337
373
|
function log(accountIdx, msg) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
if (accountIdx >= 0) {
|
|
342
|
-
if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
|
|
343
|
-
_eventLines[accountIdx].push({ text: msg, ts });
|
|
344
|
-
if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
|
|
345
|
-
if (_live) updateAccountRow(accountIdx);
|
|
346
|
-
}
|
|
374
|
+
if (accountIdx >= 0 && _live) updateAccountRow(accountIdx);
|
|
375
|
+
}
|
|
347
376
|
|
|
377
|
+
function logGlobal(msg) {
|
|
348
378
|
if (!_live) return;
|
|
349
|
-
|
|
350
|
-
const col2 = accountIdx >= 0 ? wc(accountIdx) : c.cyan;
|
|
351
|
-
const name = accountIdx >= 0 ? trunc(_workers[accountIdx]?.username || '?', 14) : 'GLOBAL';
|
|
352
|
-
process.stdout.write(`${col2}${name}${c.reset} ${DIM}[${ts}]${c.reset} ${msg}\n`);
|
|
379
|
+
addEvent('info', msg);
|
|
353
380
|
}
|
|
354
381
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
// ── Refresh ──────────────────────────────────────────────────
|
|
382
|
+
// ── Refresh timer ─────────────────────────────────────────────────
|
|
358
383
|
let _refreshTimer = null;
|
|
359
384
|
function startRefresh() {
|
|
360
385
|
if (_refreshTimer) return;
|
|
361
386
|
_refreshTimer = setInterval(() => {
|
|
362
387
|
if (!_live) return;
|
|
363
|
-
|
|
364
|
-
|
|
388
|
+
// Update all visible account rows
|
|
389
|
+
for (const idx of Object.keys(_accountRows)) {
|
|
390
|
+
updateAccountRow(parseInt(idx));
|
|
391
|
+
}
|
|
392
|
+
// Refresh uptime bar
|
|
393
|
+
const online = _workers.filter(w => w.channel && !w.paused && !w.dashboardPaused).length;
|
|
394
|
+
const up = fmtUptime();
|
|
395
|
+
let totalBal = 0;
|
|
396
|
+
for (const w of _workers) totalBal += w.stats.balance || 0;
|
|
397
|
+
const totalBalStr = totalBal > 0 ? c.green + '⏣' + totalBal.toLocaleString() + c.reset : DIM + '—' + c.reset;
|
|
398
|
+
// Update title bar on row 2 (row 1 = top border)
|
|
399
|
+
const titleLeft = `${c.bold}DANKGRINDER${c.reset} ${c.dim}v${_version}${c.reset}`;
|
|
400
|
+
const titleRight = `${c.green}●${c.reset} ${online} online ${totalBalStr} uptime ${up}`;
|
|
401
|
+
const totalW = stripAnsi(titleLeft) + stripAnsi(titleRight);
|
|
402
|
+
const padding = Math.max(0, _inner - totalW);
|
|
403
|
+
const leftPad = Math.floor(padding / 2);
|
|
404
|
+
const rightPad = padding - leftPad;
|
|
405
|
+
row(2); clrLine();
|
|
406
|
+
process.stdout.write(`${B}│\x1b[0m${' '.repeat(leftPad)}${titleLeft}${' '.repeat(rightPad)}${titleRight}${B}│`);
|
|
407
|
+
ln();
|
|
408
|
+
}, 1000);
|
|
365
409
|
}
|
|
410
|
+
|
|
366
411
|
function stopRefresh() {
|
|
367
412
|
if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
|
|
368
413
|
}
|
|
369
414
|
|
|
370
|
-
module.exports = { init, drawBanner, start, draw, log, logGlobal,
|
|
415
|
+
module.exports = { init, drawBanner, start, draw, log, logGlobal, stop, setLive, setPhase, updateAccountRow, startRefresh, stopRefresh, addEvent };
|