dankgrinder 7.66.0 → 7.67.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,43 +1,37 @@
1
1
  /**
2
2
  * DankGrinder CLI Dashboard
3
3
  *
4
- * Modern terminal UI with box-drawing characters, gradient accents, and real-time stats.
4
+ * Simple, stable terminal dashboard using \r overwrite + clear-line.
5
+ * No full screen clear — just overwrites rows from top to bottom.
5
6
  *
6
7
  * Usage:
7
8
  * const { renderDashboard } = require('./dashboard');
8
- * renderDashboard(context); // called from grinder.js render loop
9
+ * renderDashboard(context); // called every ~1s from grinder.js
9
10
  *
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
- * }
11
+ * Context: same as before, see docstring in file header.
20
12
  */
21
13
 
22
14
  'use strict';
23
15
 
24
- // ── Color shortcuts (resolved from context) ────────────────────
25
- let _c, _rgb, _BOX, _getSpinner, _gradientText;
16
+ // ── Local refs (set once per render via initColors) ────────────
17
+ let _c, _rgb, _getSpinner, _gradientText;
26
18
 
27
19
  function initColors(ctx) {
28
20
  _c = ctx.c;
29
21
  _rgb = ctx.rgb;
30
- _BOX = ctx.BOX;
31
22
  _getSpinner = ctx.getSpinner;
32
23
  _gradientText = ctx.gradientText;
33
24
  }
34
25
 
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));
26
+ // ── Helpers ─────────────────────────────────────────────────────
27
+
28
+ function vis(s) {
29
+ return String(s).replace(/\x1b\[[0-9;]*m/g, '').length;
38
30
  }
39
31
 
40
- // ── Formatters ─────────────────────────────────────────────────
32
+ function pad(str, width) {
33
+ return str + ' '.repeat(Math.max(0, width - vis(str)));
34
+ }
41
35
 
42
36
  function fmtCoins(n) {
43
37
  if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
@@ -59,29 +53,26 @@ function fmtUptime(startTime) {
59
53
  // ── Render ──────────────────────────────────────────────────────
60
54
 
61
55
  function renderDashboard(ctx) {
62
- if (!ctx.dashboardStarted || ctx.workers.length === 0 || ctx.dashboardRendering || ctx.shutdownCalled) return;
56
+ if (!ctx.dashboardStarted || ctx.workers.length === 0 || ctx.dashboardRendering) return;
63
57
 
64
58
  initColors(ctx);
65
59
 
60
+ const _ = _c.reset;
66
61
  const tw = Math.max(process.stdout.columns || 80, 60);
67
62
 
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
63
+ // Color palette
64
+ const A = _rgb(139, 92, 246);
65
+ const G = _rgb(52, 211, 153);
66
+ const Au = _rgb(255, 215, 0);
67
+ const O = _rgb(251, 146, 60);
68
+ const Cy = _rgb(34, 211, 238);
69
+ const R = _rgb(239, 68, 68);
70
+ const Y = _rgb(251, 191, 36);
77
71
  const W = _c.white;
78
72
  const D = _c.dim;
79
- const _ = _c.reset;
80
- const BOX = _BOX;
81
-
82
- const rows = [];
73
+ const CL = _c.clearLine;
83
74
 
84
- // ── Aggregate stats ──────────────────────────────────────────
75
+ // ── Aggregate ────────────────────────────────────────────────
85
76
  let aggBalance = 0, aggCoins = 0, aggCommands = 0, aggErrors = 0;
86
77
  for (const w of ctx.workers) {
87
78
  aggBalance += (w.stats.balance || 0) + (w.stats.bankBalance || 0);
@@ -90,121 +81,107 @@ function renderDashboard(ctx) {
90
81
  aggErrors += w.stats.errors || 0;
91
82
  }
92
83
  const successRate = aggCommands > 0 ? Math.round(((aggCommands - aggErrors) / aggCommands) * 100) : 100;
84
+ const elapsedHrs = (Date.now() - ctx.startTime) / 3_600_000;
85
+ const coinsPerHr = elapsedHrs > 0.01 ? Math.round(aggCoins / elapsedHrs) : 0;
86
+ const cpmVal = ctx.globalCmdRate.getRate().toFixed(1);
87
+
88
+ const activeCount = ctx.workers.filter(w => w.running && !w.paused && !w.dashboardPaused).length;
89
+ const invalidCount = ctx.workers.filter(w => w._tokenInvalid).length;
90
+ const pausedCount = ctx.workers.filter(w => w.paused || w.dashboardPaused).length;
91
+ const recovCount = ctx.workers.filter(w => w._recoveryAttempts > 0 && w._errorCooldownUntil > Date.now()).length;
92
+ const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
93
+ const spin = _getSpinner('braille');
94
+
95
+ // ── Line builder helper ───────────────────────────────────────
96
+ // prefix + content padded to tw + suffix
97
+ function line(prefix, content, suffix) {
98
+ const raw = content.replace(/\x1b\[[0-9;]*m/g, '');
99
+ const padLen = Math.max(0, tw - vis(prefix) - raw.length - vis(suffix));
100
+ return `${prefix}${content}${' '.repeat(padLen)}${suffix}`;
101
+ }
102
+
103
+ const dash = D + '─'.repeat(tw) + _;
104
+
105
+ // ── HEADER ────────────────────────────────────────────────────
106
+ const title = _c.bold + _gradientText(' DANK GRINDER ', [139, 92, 246], [52, 211, 153]) + _;
107
+ const modeTag = ctx.CLOUD_MODE ? `${Cy}CLOUD${_}` : (ctx.CLUSTER_ENABLED ? `${Cy}CLUSTER${_}` : `${D}local${_}`);
108
+ const activeTag = `${G}${activeCount}/${W}${ctx.workers.length}${_} active`;
109
+ const invTag = invalidCount > 0 ? ` ${R}${invalidCount} inv${_}` : '';
110
+ const pauseTag = pausedCount > 0 ? ` ${Y}${pausedCount} pause${_}` : '';
111
+ const recovTag = recovCount > 0 ? ` ${O}${recovCount} recov${_}` : '';
112
+
113
+ const h1 = `${title} ${D}v${ctx.PKG_VERSION}${_} ${G}${spin}${_} ${D}up${_} ${W}${fmtUptime(ctx.startTime)}${_} ${D}bal${_} ${Au}⏣${_}${W}${fmtCoins(aggBalance)}${_} ${D}/h${_} ${G}+⏣${fmtCoins(coinsPerHr)}${_} ${activeTag}${invTag}${pauseTag}${recovTag} ${D}${aggCommands}${_}cmds ${D}${cpmVal}${_}/min ${D}${memMB}MB ${modeTag}`;
114
+
115
+ // ── ACCOUNTS TABLE ───────────────────────────────────────────
116
+ const colNum = 4;
117
+ const colSts = 3;
118
+ const colName = Math.max(16, Math.min(22, Math.floor(tw * 0.22)));
119
+ const colBal = 10;
120
+ const colLvl = 5;
121
+ const colLs = 4;
122
+ const colEarn = 9;
123
+ const colAct = Math.max(8, tw - colNum - colSts - colName - colBal - colLvl - colLs - colEarn - 10);
124
+ const gap = ' ';
125
+
126
+ const headers = `${D}${pad('#', colNum)}${_}${gap}${D}${pad('S', colSts)}${_}${gap}${_gradientText(pad('Account', colName), [139, 92, 246], [96, 165, 250])}${_}${gap}${D}${pad('Balance', colBal)}${_}${gap}${D}${pad('Lvl', colLvl)}${_}${gap}${D}${pad('LS', colLs)}${_}${gap}${D}${pad('Earned', colEarn)}${_}${gap}${D}${pad('Activity', colAct)}${_}`;
127
+
128
+ const sorted = [...ctx.workers].sort((a, b) => a.idx - b.idx);
129
+ const maxRows = Math.max(5, Math.min(sorted.length, Math.floor((process.stdout.rows || 24) - 12)));
130
+ const visible = sorted.slice(0, maxRows);
131
+ const extraRows = sorted.length - maxRows;
132
+
133
+ // ── LIVE FEED ─────────────────────────────────────────────────
134
+ const logs = ctx.recentLogs.toArray();
93
135
 
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);
136
+ // ── FOOTER ───────────────────────────────────────────────────
137
+ const footer = `${modeTag} ${D}P=pause R=resume S=status Q=quit${_}`;
97
138
 
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;
139
+ // ── WRITE ALL ROWS (overwrite from top) ─────────────────────
140
+ // We write \r + clear line to overwrite each line in place.
141
+ // No full screen clear — just overwrite. Simple and reliable.
102
142
 
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}●${_}`;
143
+ let rowNum = 0;
106
144
 
107
- const memMB = Math.round((process.memoryUsage?.rss?.() ?? process.memoryUsage().rss) / 1048576);
108
- const spin = _getSpinner('braille');
145
+ function writeRow(content) {
146
+ process.stdout.write(`${CL}\r${content}\n`);
147
+ rowNum++;
148
+ }
109
149
 
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${_}`);
150
+ // 1. Header
151
+ writeRow(line(A + '╔', D + '═'.repeat(tw - 2), A + '╗'));
152
+ writeRow(line(`${A}║ `, h1, `${A} ║`));
153
+ writeRow(line(A + '╠', D + '═'.repeat(tw - 2), A + '╣'));
154
+ writeRow(line(`${A}║ `, `${D}session${_} ${G}${fmtCoins(aggCoins)}${_} ${D}coins${_} ${G}${successRate}%${_} ${D}success${_} ${W}${fmtCoins(ctx.sessionPeakCoins)}${_} ${D}peak${_}${ctx.isNewHigh ? ` ${Au}★ NEW HIGH${_}` : ''}`, `${A} ║`));
155
+ writeRow(line(A + '╚', D + '═'.repeat(tw - 2), A + '╝'));
114
156
 
115
- // Top border
116
- rows.push(`${A}${BOX.dtl}${BOX.dh.repeat(tw - 2)}${BOX.dtr}${_}`);
117
-
118
- // Main header line
119
- const h1 = [
120
- titleBar, versionTag, spin,
121
- `${D}up${_} ${W}${fmtUptime(ctx.startTime)}${_}`,
122
- netDot, `${D}net${_}`,
123
- `${Au}⏣${_}${W}${fmtCoins(aggBalance)}${_} ${D}bal${_}`,
124
- `${G}${coinsPerHr >= 0 ? '+' : ''}⏣${fmtCoins(coinsPerHr)}${_}/h`,
125
- `${G}${activeCount}${_}/${W}${ctx.workers.length}${_} ${D}active${_}`,
126
- invalidCount > 0 ? ` ${R}${invalidCount} inv${_}` : '',
127
- pausedCount > 0 ? ` ${Y}${pausedCount} pause${_}` : '',
128
- recovCount > 0 ? ` ${O}${recovCount} recov${_}` : '',
129
- ` ${D}${aggCommands}${_} cmds`,
130
- ` ${D}${cpmVal}${_}/min`,
131
- ` ${D}mem${_} ${memMB}MB`,
132
- modeTag,
133
- ].filter(Boolean).join(' ');
134
-
135
- rows.push(`${A}${BOX.dv}${_} ${pad(h1, tw - 4)} ${A}${BOX.dv}${_}`);
136
-
137
- // Divider
138
- rows.push(`${A}${BOX.teeD}${BOX.dh.repeat(tw - 2)}${BOX.teeU}${_}`);
139
-
140
- // Stats row
141
- const statsRow = [
142
- `${G}${fmtCoins(aggCoins)}${_} ${D}coins${_}`,
143
- `${G}${successRate}%${_} ${D}success${_}`,
144
- `${W}${fmtCoins(ctx.sessionPeakCoins)}${_} ${D}peak${_}`,
145
- ctx.isNewHigh ? `${Au}★ NEW HIGH${_}` : '',
146
- ].filter(Boolean).join(` ${D}│${_} `);
147
-
148
- const statLine = ` ${D}session${_} ${statsRow}`;
149
- rows.push(`${A}${BOX.dv}${_}${pad(statLine, tw - 4)}${A}${BOX.dv}${_}`);
150
-
151
- // Bottom header border
152
- rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
153
-
154
- // ── ACCOUNTS TABLE ──────────────────────────────────────────
155
- const colNum = 4;
156
- const colSts = 3;
157
- const colName = Math.max(14, Math.min(22, Math.floor(tw * 0.20)));
158
- const colBal = 10;
159
- const colLvl = 5;
160
- const colLs = 4;
161
- const colEarn = 9;
162
- const colAct = Math.max(6, tw - colNum - colSts - colName - colBal - colLvl - colLs - colEarn - 18);
163
- const gap = 2;
164
- const colGap = ' '.repeat(gap);
165
-
166
- // Column header
167
- const headers = [
168
- `${D}${pad('#', colNum)}${_}`,
169
- `${D}${pad('S', colSts)}${_}`,
170
- `${_gradientText(pad('Account', colName), [139, 92, 246], [96, 165, 250])}${_}`,
171
- `${D}${pad('Balance', colBal)}${_}`,
172
- `${D}${pad('Lvl', colLvl)}${_}`,
173
- `${D}${pad('LS', colLs)}${_}`,
174
- `${D}${pad('Earned', colEarn)}${_}`,
175
- `${D}${pad('Activity', colAct)}${_}`,
176
- ].join(colGap);
177
-
178
- rows.push(`${A}${BOX.dtl}${BOX.dh.repeat(tw - 2)}${BOX.dtr}${_}`);
179
- rows.push(`${A}${BOX.dv}${_} ${headers} ${A}${BOX.dv}${_}`);
180
- rows.push(`${A}${BOX.teeD}${BOX.h.repeat(tw - 2)}${BOX.teeU}${_}`);
181
-
182
- const sorted = [...ctx.workers].sort((a, b) => a.idx - b.idx);
183
- const maxRows = Math.max(4, Math.min(sorted.length, Math.floor((process.stdout.rows || 24) - 14)));
184
- const visible = sorted.slice(0, maxRows);
157
+ // 2. Table header
158
+ writeRow(line(A + '╔', D + '─'.repeat(tw - 2), A + '╗'));
159
+ writeRow(line(`${A}║ `, headers, `${A} ║`));
160
+ writeRow(line(A + '╟', D + '─'.repeat(tw - 2), A + '╢'));
185
161
 
162
+ // 3. Account rows
186
163
  for (const wk of visible) {
187
164
  const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
188
165
 
189
166
  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);
167
+ if (wk._tokenInvalid) stsIcon = `${R}✗${_}`;
168
+ else if (!wk.running) stsIcon = `${D}○${_}`;
169
+ else if (isRecov) stsIcon = `${O}${_getSpinner('braille').substring(0, 1)}${_}`;
170
+ else if (wk.paused) stsIcon = `${R}⏸${_}`;
171
+ else if (wk.dashboardPaused) stsIcon = `${Y}⏸${_}`;
172
+ else if (wk.busy) stsIcon = `${G}${_getSpinner('pulse').substring(0, 1)}${_}`;
173
+ else stsIcon = `${G}●${_}`;
174
+
175
+ const name = (wk.username || wk.account.label || '?').substring(0, colName);
199
176
  const nameStr = `${wk.color}${name}${_}`;
200
177
  const balStr = wk.stats.balance > 0 ? `${Au}⏣${_}${W}${fmtCoins(wk.stats.balance)}${_}` : `${D}⏣-${_}`;
201
178
  const lvl = wk._level || 0;
202
179
  const lvlStr = lvl > 0 ? `${Cy}L${lvl}${_}` : `${D}L???${_}`;
203
180
  const ls = wk._lifesavers;
204
181
  let lsStr;
205
- if (ls === 0) lsStr = `${R}♥${ls}${_}`;
182
+ if (ls === 0) lsStr = `${R}♥${ls}${_}`;
206
183
  else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${_}`;
207
- else if (ls != null) lsStr = `${G}♥${ls}${_}`;
184
+ else if (ls != null) lsStr = `${G}♥${ls}${_}`;
208
185
  else {
209
186
  const p = ctx.PULSE_CHARS[Math.floor(Date.now() / 400) % ctx.PULSE_CHARS.length];
210
187
  lsStr = `${D}${p}♥?${_}`;
@@ -212,41 +189,38 @@ function renderDashboard(ctx) {
212
189
 
213
190
  const earn = wk.stats.coins || 0;
214
191
  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)}${_}`;
192
+ const actRaw = (wk.lastStatus || 'ready').replace(/\x1b\[[0-9;]*m/g, '').substring(0, colAct);
193
+ const actStr = `${D}${pad(actRaw, colAct)}${_}`;
194
+ const numStr = `${D}${pad(String(wk.idx + 1), colNum)}${_}`;
218
195
 
219
- const row = [
196
+ const rowStr = [
220
197
  numStr, pad(stsIcon, colSts), pad(nameStr, colName),
221
198
  pad(balStr, colBal), pad(lvlStr, colLvl), pad(lsStr, colLs),
222
199
  pad(earnStr, colEarn), actStr,
223
- ].join(colGap);
200
+ ].join(gap);
224
201
 
225
- rows.push(`${A}${BOX.dv}${_} ${pad(row, tw - 4)} ${A}${BOX.dv}${_}`);
202
+ writeRow(line(`${A} `, rowStr, `${A} ║`));
226
203
  }
227
204
 
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}${_}`);
205
+ if (extraRows > 0) {
206
+ writeRow(line(`${A}║ `, `${D}+${extraRows} more${_}${' '.repeat(Math.max(0, tw - 16))}`, `${A} ║`));
231
207
  }
232
208
 
233
- rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
209
+ writeRow(line(A + '╚', D + '─'.repeat(tw - 2), A + '╝'));
234
210
 
235
- // ── LIVE FEED ────────────────────────────────────────────────
236
- const logs = ctx.recentLogs.toArray();
211
+ // 4. Live feed
237
212
  if (logs.length > 0) {
238
213
  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}${_}`);
214
+ writeRow(line(A + '╔', D + '─'.repeat(tw - 2), A + '╗'));
215
+ writeRow(line(`${A} `, feedTitle, `${A} ║`));
216
+ writeRow(line(A + '╟', D + '─'.repeat(tw - 2), A + '╢'));
243
217
 
244
218
  for (const entry of logs) {
245
219
  let lineText;
246
220
  if (typeof entry === 'string') {
247
221
  lineText = entry;
248
222
  } 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' }) : '';
223
+ const ts = entry.ts ? new Date(entry.ts).toLocaleTimeString('en-US', { hour12: false }) : '';
250
224
  const user = entry.username ? String(entry.username) : '';
251
225
  const cmd = entry.command ? `[${entry.command}]` : '';
252
226
  const resp = entry.response || '';
@@ -254,34 +228,17 @@ function renderDashboard(ctx) {
254
228
  } else {
255
229
  lineText = String(entry);
256
230
  }
257
- rows.push(`${A}${BOX.dv}${_} ${D}${pad(lineText, tw - 4)}${_} ${A}${BOX.dv}${_}`);
231
+ writeRow(line(`${A} `, `${D}${lineText}${_}`, `${A} ║`));
258
232
  }
259
- rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
233
+ writeRow(line(A + '╚', D + '─'.repeat(tw - 2), A + '╝'));
260
234
  }
261
235
 
262
- // ── FOOTER ───────────────────────────────────────────────────
263
- const footerLine = `${_c.dim}P=pause R=resume S=status Q=quit${_}`;
264
- const modeLabel = ctx.CLOUD_MODE ? `${Cy}☁ CLOUD${_}` : (ctx.CLUSTER_ENABLED ? `${Cy} CLUSTER${_}` : `${D} local${_}`);
265
- const footerFull = `${modeLabel} ${footerLine}`;
266
-
267
- rows.push(`${A}${BOX.dtl}${BOX.dh.repeat(tw - 2)}${BOX.dtr}${_}`);
268
- rows.push(`${A}${BOX.dv}${_} ${pad(footerFull, tw - 4)} ${A}${BOX.dv}${_}`);
269
- rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
270
-
271
- // ── Flush ────────────────────────────────────────────────────
272
- // Clear entire screen buffer first so old startup logs don't bleed through
273
- process.stdout.write('\x1b[2J\x1b[H');
274
- process.stdout.write(_c.hide);
275
- for (const row of rows) {
276
- process.stdout.write(`${_c.clearLine}\r${row}\n`);
277
- }
278
- process.stdout.write(_c.show);
279
- const leftover = ctx.dashboardLines - rows.length;
280
- if (leftover > 0) {
281
- process.stdout.write(`\x1b[${leftover}B\x1b[2K`);
282
- }
236
+ // 5. Footer
237
+ writeRow(line(A + '╔', D + '─'.repeat(tw - 2), A + '╗'));
238
+ writeRow(line(`${A} `, footer, `${A} ║`));
239
+ writeRow(line(A + '╚', D + '─'.repeat(tw - 2), A + '╝'));
283
240
 
284
- return rows.length;
241
+ return rowNum;
285
242
  }
286
243
 
287
244
  module.exports = { renderDashboard };
package/lib/grinder.js CHANGED
@@ -3358,10 +3358,8 @@ async function start(apiKey, apiUrl, opts = {}) {
3358
3358
  setDashboardActive(true);
3359
3359
  // Setup keyboard shortcuts
3360
3360
  setupKeyboardShortcuts();
3361
- // Clear entire screen so startup logs don't create ghost bars
3362
- process.stdout.write('\x1b[2J\x1b[H');
3361
+ // Cursor hide to reduce visual noise during renders
3363
3362
  process.stdout.write(c.hide);
3364
- dashboardLines = 0;
3365
3363
 
3366
3364
  // Re-render on terminal resize so layout adapts to window size
3367
3365
  process.stdout.on('resize', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dankgrinder",
3
- "version": "7.66.0",
3
+ "version": "7.67.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"