dankgrinder 8.72.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 -190
- 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,124 +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
|
-
|
|
133
|
-
return
|
|
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);
|
|
134
117
|
}
|
|
135
118
|
|
|
136
|
-
// ── Build account row
|
|
137
|
-
function accountRow(w, wi
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
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;
|
|
143
125
|
const ls = fmtLifesavers(w);
|
|
144
126
|
const lv = fmtLevel(w);
|
|
145
|
-
const name = padR(trunc(w.username || '?', col.name), col.name);
|
|
146
|
-
const doing = padR(w.lastStatus || 'idle', col.cmd);
|
|
147
|
-
const earnedStr = stripAnsi(earned);
|
|
148
|
-
const balStr = stripAnsi(bal);
|
|
149
127
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
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"
|
|
153
139
|
|
|
154
140
|
return (
|
|
155
|
-
`${B}│\x1b[0m
|
|
156
|
-
`${DIM}${
|
|
157
|
-
`${
|
|
158
|
-
`${
|
|
159
|
-
`${
|
|
160
|
-
`${
|
|
161
|
-
`${
|
|
162
|
-
`${padL(lv, col.lv)} ` +
|
|
163
|
-
`${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} ` +
|
|
164
148
|
`${B}│\x1b[0m`
|
|
165
149
|
);
|
|
166
150
|
}
|
|
167
151
|
|
|
168
|
-
// ──
|
|
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) ─────────────────────
|
|
169
172
|
function draw() {
|
|
170
173
|
layout();
|
|
171
|
-
const
|
|
172
|
-
const bannerH = BANNER_LINES.length;
|
|
174
|
+
const LOG_W = getLogsW();
|
|
173
175
|
|
|
174
|
-
// Clear
|
|
176
|
+
// Clear screen + home
|
|
175
177
|
process.stdout.write('\x1b[2J\x1b[H');
|
|
176
178
|
|
|
177
179
|
let r = 1;
|
|
180
|
+
const top = _inner;
|
|
178
181
|
|
|
179
182
|
// ── Top border ──
|
|
180
183
|
row(r++); clrLine();
|
|
181
|
-
process.stdout.write(`${B}┌${'─'.repeat(
|
|
184
|
+
process.stdout.write(`${B}┌${'─'.repeat(top)}┐`);
|
|
182
185
|
ln();
|
|
183
186
|
|
|
184
|
-
// ──
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
process.stdout.write(`${B}│\x1b[0m ${gradLine}${' '.repeat(_inner - 2 - stripAnsi(gradLine) - 2)}${B}│`);
|
|
191
|
-
} else {
|
|
192
|
-
process.stdout.write(`${B}│\x1b[0m ${DIM}${line}${c.reset}${' '.repeat(_inner - 2 - stripAnsi(line) - 2)}${B}│`);
|
|
193
|
-
}
|
|
194
|
-
ln();
|
|
195
|
-
}
|
|
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;
|
|
196
193
|
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
`${c.green}●${c.reset} ${running} online`,
|
|
204
|
-
paused > 0 ? `${c.yellow}~${c.reset} ${paused} paused` : null,
|
|
205
|
-
errors > 0 ? `${c.red}E${c.reset} ${errors} error` : null,
|
|
206
|
-
`${DIM}Ctrl+C${c.reset}`,
|
|
207
|
-
].filter(Boolean);
|
|
208
|
-
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;
|
|
209
200
|
|
|
210
201
|
row(r++); clrLine();
|
|
211
|
-
process.stdout.write(`${B}│\x1b[0m ${
|
|
202
|
+
process.stdout.write(`${B}│\x1b[0m${' '.repeat(leftPad)}${titleLeft}${' '.repeat(rightPad)}${titleRight}${B}│`);
|
|
212
203
|
ln();
|
|
213
204
|
|
|
214
205
|
// ── Divider ──
|
|
215
206
|
row(r++); clrLine();
|
|
216
|
-
process.stdout.write(`${B}├${'─'.repeat(
|
|
207
|
+
process.stdout.write(`${B}├${'─'.repeat(top)}┤`);
|
|
217
208
|
ln();
|
|
218
209
|
|
|
219
|
-
// ──
|
|
210
|
+
// ── Column header ──
|
|
220
211
|
row(r++); clrLine();
|
|
221
|
-
process.stdout.write(
|
|
212
|
+
process.stdout.write(headerRow(LOG_W));
|
|
222
213
|
ln();
|
|
223
214
|
|
|
224
215
|
// ── HR ──
|
|
225
216
|
row(r++); clrLine();
|
|
226
|
-
process.stdout.write(`${B}│\x1b[0m
|
|
217
|
+
process.stdout.write(`${B}│\x1b[0m${'─'.repeat(top)}${B}│`);
|
|
227
218
|
ln();
|
|
228
219
|
|
|
229
220
|
// ── Account rows ──
|
|
230
221
|
const sorted = [..._workers].sort((a, b) => {
|
|
231
|
-
if (!a.channel !== !b.channel) return
|
|
222
|
+
if (!a.channel !== !b.channel) return a.channel ? -1 : 1;
|
|
232
223
|
const aA = a.channel && !a.paused && !a.dashboardPaused;
|
|
233
|
-
const
|
|
234
|
-
if (aA !==
|
|
224
|
+
const bB = b.channel && !b.paused && !b.dashboardPaused;
|
|
225
|
+
if (aA !== bB) return aA ? -1 : 1;
|
|
235
226
|
return (b.stats.commands || 0) - (a.stats.commands || 0);
|
|
236
227
|
});
|
|
237
228
|
const shown = sorted.slice(0, _maxAccounts);
|
|
@@ -242,55 +233,105 @@ function draw() {
|
|
|
242
233
|
const wi = _workers.indexOf(w);
|
|
243
234
|
_accountRows[wi] = r;
|
|
244
235
|
row(r++); clrLine();
|
|
245
|
-
process.stdout.write(accountRow(w, wi
|
|
236
|
+
process.stdout.write(accountRow(w, wi));
|
|
246
237
|
ln();
|
|
247
238
|
}
|
|
248
239
|
|
|
249
240
|
// ── Totals divider ──
|
|
250
|
-
_totalsRow = r;
|
|
251
241
|
row(r++); clrLine();
|
|
252
|
-
process.stdout.write(`${B}│\x1b[0m
|
|
242
|
+
process.stdout.write(`${B}│\x1b[0m${'─'.repeat(top)}${B}│`);
|
|
253
243
|
ln();
|
|
254
244
|
|
|
255
|
-
// ── Totals ──
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
const balStr = totalBal > 0 ? '⏣' + totalBal.toLocaleString() : '—';
|
|
263
|
-
const balPadded = balStr.padEnd(col.bal);
|
|
264
|
-
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);
|
|
265
252
|
|
|
266
253
|
row(r++); clrLine();
|
|
267
|
-
process.stdout.write(
|
|
268
|
-
`${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}│`
|
|
269
|
-
);
|
|
254
|
+
process.stdout.write(`${B}│\x1b[0m${' '.repeat(2)}${coinsLabel}${' '.repeat(gap)}${coinsVal}${' '.repeat(2)}${B}│`);
|
|
270
255
|
ln();
|
|
271
256
|
|
|
272
|
-
// ── Bottom ──
|
|
257
|
+
// ── Bottom border ──
|
|
273
258
|
_bottomRow = r;
|
|
274
259
|
row(r++); clrLine();
|
|
275
|
-
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)}┤`);
|
|
276
273
|
ln();
|
|
277
274
|
}
|
|
278
275
|
|
|
279
|
-
// ── Update ONE account row in-place
|
|
276
|
+
// ── Update ONE account row in-place ───────────────────────────────
|
|
280
277
|
function updateAccountRow(accountIdx) {
|
|
281
278
|
if (!_live) return;
|
|
282
279
|
const rowNum = _accountRows[accountIdx];
|
|
283
|
-
if (!rowNum) return;
|
|
280
|
+
if (!rowNum) return;
|
|
284
281
|
const w = _workers[accountIdx];
|
|
285
282
|
if (!w) return;
|
|
286
|
-
const col = getCol();
|
|
287
|
-
|
|
288
283
|
row(rowNum); clrLine();
|
|
289
|
-
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)}┘`);
|
|
290
331
|
ln();
|
|
291
332
|
}
|
|
292
333
|
|
|
293
|
-
// ── Gradient line
|
|
334
|
+
// ── Gradient line (unused in new design but kept) ─────────────────
|
|
294
335
|
function gradientLine(text, r1, g1, b1, r2, g2, b2) {
|
|
295
336
|
let out = '';
|
|
296
337
|
for (let i = 0; i < text.length; i++) {
|
|
@@ -300,17 +341,21 @@ function gradientLine(text, r1, g1, b1, r2, g2, b2) {
|
|
|
300
341
|
return out + '\x1b[0m';
|
|
301
342
|
}
|
|
302
343
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
+
}
|
|
308
352
|
|
|
309
|
-
|
|
353
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
354
|
+
function init({ workers }) {
|
|
310
355
|
_startTime = Date.now();
|
|
311
356
|
_workers = workers;
|
|
312
357
|
_version = '0.0.0';
|
|
313
|
-
|
|
358
|
+
_events = [];
|
|
314
359
|
_live = false;
|
|
315
360
|
_phase = 'init';
|
|
316
361
|
_accountRows = [];
|
|
@@ -318,47 +363,53 @@ function init({ workers, isShuttingDown }) {
|
|
|
318
363
|
|
|
319
364
|
function drawBanner(version) { _version = version || '0.0.0'; }
|
|
320
365
|
function start() {}
|
|
321
|
-
|
|
322
366
|
function stop() {
|
|
323
367
|
_live = false;
|
|
324
|
-
_phase = 'init';
|
|
325
368
|
process.stdout.write('\x1b[2J\x1b[H' + c.reset + '\n');
|
|
326
369
|
}
|
|
327
|
-
|
|
328
370
|
function setLive(val) { _live = val; }
|
|
329
371
|
function setPhase(phase) { _phase = phase; }
|
|
330
372
|
|
|
331
373
|
function log(accountIdx, msg) {
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (accountIdx >= 0) {
|
|
336
|
-
if (!_eventLines[accountIdx]) _eventLines[accountIdx] = [];
|
|
337
|
-
_eventLines[accountIdx].push({ text: msg, ts });
|
|
338
|
-
if (_eventLines[accountIdx].length > MAX_EVENTS) _eventLines[accountIdx].shift();
|
|
339
|
-
if (_live) updateAccountRow(accountIdx);
|
|
340
|
-
}
|
|
374
|
+
if (accountIdx >= 0 && _live) updateAccountRow(accountIdx);
|
|
375
|
+
}
|
|
341
376
|
|
|
377
|
+
function logGlobal(msg) {
|
|
342
378
|
if (!_live) return;
|
|
343
|
-
|
|
344
|
-
const col2 = accountIdx >= 0 ? wc(accountIdx) : c.cyan;
|
|
345
|
-
const name = accountIdx >= 0 ? trunc(_workers[accountIdx]?.username || '?', 14) : 'GLOBAL';
|
|
346
|
-
process.stdout.write(`${col2}${name}${c.reset} ${DIM}[${ts}]${c.reset} ${msg}\n`);
|
|
379
|
+
addEvent('info', msg);
|
|
347
380
|
}
|
|
348
381
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
// ── Refresh ──────────────────────────────────────────────────
|
|
382
|
+
// ── Refresh timer ─────────────────────────────────────────────────
|
|
352
383
|
let _refreshTimer = null;
|
|
353
384
|
function startRefresh() {
|
|
354
385
|
if (_refreshTimer) return;
|
|
355
386
|
_refreshTimer = setInterval(() => {
|
|
356
387
|
if (!_live) return;
|
|
357
|
-
|
|
358
|
-
|
|
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);
|
|
359
409
|
}
|
|
410
|
+
|
|
360
411
|
function stopRefresh() {
|
|
361
412
|
if (_refreshTimer) { clearInterval(_refreshTimer); _refreshTimer = null; }
|
|
362
413
|
}
|
|
363
414
|
|
|
364
|
-
module.exports = { init, drawBanner, start, draw, log, logGlobal,
|
|
415
|
+
module.exports = { init, drawBanner, start, draw, log, logGlobal, stop, setLive, setPhase, updateAccountRow, startRefresh, stopRefresh, addEvent };
|