dankgrinder 7.66.0 → 7.68.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,43 @@
1
1
  /**
2
2
  * DankGrinder CLI Dashboard
3
3
  *
4
- * Modern terminal UI with box-drawing characters, gradient accents, and real-time stats.
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.
5
6
  *
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
- * }
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
20
14
  */
21
15
 
22
16
  'use strict';
23
17
 
24
- // ── Color shortcuts (resolved from context) ────────────────────
25
- let _c, _rgb, _BOX, _getSpinner, _gradientText;
18
+ // Local refs (set per render)
19
+ let _c, _rgb, _spinnerFn, _gradientFn;
26
20
 
27
- function initColors(ctx) {
21
+ function init(ctx) {
28
22
  _c = ctx.c;
29
23
  _rgb = ctx.rgb;
30
- _BOX = ctx.BOX;
31
- _getSpinner = ctx.getSpinner;
32
- _gradientText = ctx.gradientText;
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;
33
32
  }
34
33
 
35
34
  function pad(str, width) {
36
- const raw = str.replace(/\x1b\[[0-9;]*m/g, '');
37
- return str + ' '.repeat(Math.max(0, width - raw.length));
35
+ return str + ' '.repeat(Math.max(0, width - vis(str)));
38
36
  }
39
37
 
40
- // ── Formatters ─────────────────────────────────────────────────
38
+ function clearLine(content) {
39
+ return `${_c.clearLine}\r${content}`;
40
+ }
41
41
 
42
42
  function fmtCoins(n) {
43
43
  if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
@@ -59,152 +59,137 @@ function fmtUptime(startTime) {
59
59
  // ── Render ──────────────────────────────────────────────────────
60
60
 
61
61
  function renderDashboard(ctx) {
62
- if (!ctx.dashboardStarted || ctx.workers.length === 0 || ctx.dashboardRendering || ctx.shutdownCalled) return;
62
+ if (!ctx.dashboardStarted || ctx.workers.length === 0 || ctx.dashboardRendering) return;
63
63
 
64
- initColors(ctx);
64
+ init(ctx);
65
65
 
66
66
  const tw = Math.max(process.stdout.columns || 80, 60);
67
+ const _ = _c.reset;
67
68
 
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
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
77
  const W = _c.white;
78
78
  const D = _c.dim;
79
- const _ = _c.reset;
80
- const BOX = _BOX;
81
-
82
- const rows = [];
83
79
 
84
- // ── Aggregate stats ──────────────────────────────────────────
85
- let aggBalance = 0, aggCoins = 0, aggCommands = 0, aggErrors = 0;
80
+ // ── Aggregate stats ───────────────────────────────────────────
81
+ let aggBalance = 0, aggCoins = 0, aggCmds = 0, aggErrors = 0;
86
82
  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;
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;
91
87
  }
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;
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;
102
99
 
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
100
  const modeTag = ctx.CLOUD_MODE ? `${Cy}CLOUD${_}` : (ctx.CLUSTER_ENABLED ? `${Cy}CLUSTER${_}` : `${D}local${_}`);
114
101
 
115
- // Top border
116
- rows.push(`${A}${BOX.dtl}${BOX.dh.repeat(tw - 2)}${BOX.dtr}${_}`);
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 = [];
117
124
 
118
- // Main header line
125
+ // HEADER
126
+ const title = _c.bold + _gradientFn(' DANK GRINDER ', [139, 92, 246], [52, 211, 153]) + _;
119
127
  const h1 = [
120
- titleBar, versionTag, spin,
128
+ title,
129
+ `${D}v${ctx.PKG_VERSION}${_}`,
130
+ `${G}${spin}${_}`,
121
131
  `${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${_}`,
132
+ `${D}bal${_} ${Au}⏣${_}${W}${fmtCoins(aggBalance)}${_}`,
133
+ `${G}+⏣${fmtCoins(perHr)}${_}/h`,
134
+ `${G}${activeCount}${_}/${W}${ctx.workers.length}${_} active`,
126
135
  invalidCount > 0 ? ` ${R}${invalidCount} inv${_}` : '',
127
136
  pausedCount > 0 ? ` ${Y}${pausedCount} pause${_}` : '',
128
137
  recovCount > 0 ? ` ${O}${recovCount} recov${_}` : '',
129
- ` ${D}${aggCommands}${_} cmds`,
138
+ ` ${D}${aggCmds}${_}cmds`,
130
139
  ` ${D}${cpmVal}${_}/min`,
131
- ` ${D}mem${_} ${memMB}MB`,
140
+ ` ${D}${memMB}MB`,
132
141
  modeTag,
133
142
  ].filter(Boolean).join(' ');
134
143
 
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
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
167
151
  const headers = [
168
152
  `${D}${pad('#', colNum)}${_}`,
169
153
  `${D}${pad('S', colSts)}${_}`,
170
- `${_gradientText(pad('Account', colName), [139, 92, 246], [96, 165, 250])}${_}`,
154
+ `${_gradientFn(pad('Account', colName), [139, 92, 246], [96, 165, 250])}${_}`,
171
155
  `${D}${pad('Balance', colBal)}${_}`,
172
156
  `${D}${pad('Lvl', colLvl)}${_}`,
173
157
  `${D}${pad('LS', colLs)}${_}`,
174
158
  `${D}${pad('Earned', colEarn)}${_}`,
175
159
  `${D}${pad('Activity', colAct)}${_}`,
176
- ].join(colGap);
160
+ ].join(gap);
177
161
 
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}${_}`);
162
+ rows.push(border('╔'));
163
+ rows.push(mkRow(headers));
164
+ rows.push(border('╟'));
181
165
 
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);
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;
185
170
 
186
171
  for (const wk of visible) {
187
172
  const isRecov = wk._recoveryAttempts > 0 && wk._errorCooldownUntil > Date.now();
188
173
 
189
174
  let stsIcon;
190
175
  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);
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);
199
184
  const nameStr = `${wk.color}${name}${_}`;
200
185
  const balStr = wk.stats.balance > 0 ? `${Au}⏣${_}${W}${fmtCoins(wk.stats.balance)}${_}` : `${D}⏣-${_}`;
201
186
  const lvl = wk._level || 0;
202
187
  const lvlStr = lvl > 0 ? `${Cy}L${lvl}${_}` : `${D}L???${_}`;
203
188
  const ls = wk._lifesavers;
204
189
  let lsStr;
205
- if (ls === 0) lsStr = `${R}♥${ls}${_}`;
190
+ if (ls === 0) lsStr = `${R}♥${ls}${_}`;
206
191
  else if (ls != null && ls <= 2) lsStr = `${Y}♥${ls}${_}`;
207
- else if (ls != null) lsStr = `${G}♥${ls}${_}`;
192
+ else if (ls != null) lsStr = `${G}♥${ls}${_}`;
208
193
  else {
209
194
  const p = ctx.PULSE_CHARS[Math.floor(Date.now() / 400) % ctx.PULSE_CHARS.length];
210
195
  lsStr = `${D}${p}♥?${_}`;
@@ -212,41 +197,44 @@ function renderDashboard(ctx) {
212
197
 
213
198
  const earn = wk.stats.coins || 0;
214
199
  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}${_}`);
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));
226
216
  }
227
217
 
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}${_}`);
218
+ if (extraRows > 0) {
219
+ rows.push(mkRow(`${D}+${extraRows} more${_}`));
231
220
  }
232
221
 
233
- rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
222
+ rows.push(border('╚'));
234
223
 
235
- // ── LIVE FEED ────────────────────────────────────────────────
224
+ // LIVE FEED
236
225
  const logs = ctx.recentLogs.toArray();
237
226
  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}${_}`);
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('╟'));
243
231
 
244
232
  for (const entry of logs) {
245
233
  let lineText;
246
234
  if (typeof entry === 'string') {
247
235
  lineText = entry;
248
236
  } 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' }) : '';
237
+ const ts = entry.ts ? new Date(entry.ts).toLocaleTimeString('en-US', { hour12: false }) : '';
250
238
  const user = entry.username ? String(entry.username) : '';
251
239
  const cmd = entry.command ? `[${entry.command}]` : '';
252
240
  const resp = entry.response || '';
@@ -254,32 +242,22 @@ function renderDashboard(ctx) {
254
242
  } else {
255
243
  lineText = String(entry);
256
244
  }
257
- rows.push(`${A}${BOX.dv}${_} ${D}${pad(lineText, tw - 4)}${_} ${A}${BOX.dv}${_}`);
245
+ rows.push(mkRow(`${D}${lineText}${_}`));
258
246
  }
259
- rows.push(`${A}${BOX.dbl}${BOX.dh.repeat(tw - 2)}${BOX.dbr}${_}`);
247
+ rows.push(border('╚'));
260
248
  }
261
249
 
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}`;
250
+ // FOOTER
251
+ rows.push(border('╔'));
252
+ rows.push(mkRow(`${modeTag} ${D}P=pause R=resume S=status Q=quit${_}`));
253
+ rows.push(border('╚'));
266
254
 
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);
255
+ // ── Flush: write all rows, each clears its line first ─────────
256
+ // This is how top/htop work — overwrite each row in place.
257
+ // No full-screen clear, no cursor tricks. Simple and reliable.
275
258
  for (const row of rows) {
276
259
  process.stdout.write(`${_c.clearLine}\r${row}\n`);
277
260
  }
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
- }
283
261
 
284
262
  return rows.length;
285
263
  }
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.68.0",
4
4
  "description": "Dank Memer automation engine — grind coins while you sleep",
5
5
  "bin": {
6
6
  "dankgrinder": "bin/dankgrinder.js"