git-watchtower 2.3.6 → 2.3.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "2.3.6",
3
+ "version": "2.3.8",
4
4
  "description": "Terminal-based Git branch monitor with activity sparklines and optional dev server with live reload",
5
5
  "main": "bin/git-watchtower.js",
6
6
  "bin": {
@@ -283,6 +283,16 @@ function startSlotReels(renderCallback) {
283
283
  * @param {Object|null} winLevel - The win level object from getWinLevel()
284
284
  */
285
285
  function stopSlotReels(hadUpdates = false, renderCallback = null, winLevel = null) {
286
+ // No-op when casino mode is off. The bin's polling path captures
287
+ // `casinoOn` once at the top of pollGitChanges and continues to use that
288
+ // snapshot for the rest of the cycle, so a poll completing AFTER the
289
+ // user toggled casino off would otherwise install a fresh
290
+ // slotResultInterval that fires render() ~20× over the next 3s. The
291
+ // display getters are already guarded by `casinoEnabled`, so the user
292
+ // sees nothing — but render() still burns full-screen redraws and the
293
+ // interval keeps a closure alive. Drop the call cleanly here instead.
294
+ if (!casinoEnabled) return;
295
+
286
296
  if (slotReelInterval) {
287
297
  clearInterval(slotReelInterval);
288
298
  slotReelInterval = null;
@@ -689,6 +699,24 @@ function getStats() {
689
699
  };
690
700
  }
691
701
 
702
+ /**
703
+ * Stats payload safe for SSE serialization. Excludes the random/clock-driven
704
+ * decorative fields (luckMeter, houseEdge, vibesQuality, timeSinceLastHit)
705
+ * because they tick on every call — leaving them in the payload defeats
706
+ * web.js's `lastPushedJson` dedup, so the full ~100 KB state is shipped to
707
+ * every connected client every 500 ms even when nothing has actually
708
+ * changed. The dashboard recomputes these client-side at render time.
709
+ *
710
+ * Stable but slow-ticking fields (sessionDuration, which changes once per
711
+ * minute) are kept — the cost is at most one extra push per minute.
712
+ *
713
+ * @returns {Object}
714
+ */
715
+ function getSerializableStats() {
716
+ const { luckMeter, houseEdge, vibesQuality, timeSinceLastHit, ...stable } = getStats();
717
+ return stable;
718
+ }
719
+
692
720
  /**
693
721
  * Reset session stats
694
722
  */
@@ -778,6 +806,7 @@ module.exports = {
778
806
  // Stats
779
807
  recordPoll,
780
808
  getStats,
809
+ getSerializableStats,
781
810
  resetStats,
782
811
  getStatsDisplay,
783
812
  };
@@ -1241,7 +1241,31 @@ ${pureFnBlock}
1241
1241
  return left + right;
1242
1242
  }
1243
1243
 
1244
+ // Decorative casino fields the server intentionally does NOT include in
1245
+ // the SSE payload because they're random/clock-driven and would defeat
1246
+ // the lastPushedJson dedup. Recompute them locally at render time so the
1247
+ // dashboard stays visually identical to the TUI.
1248
+ const CASINO_VIBES = ['\u{1f60e}','\u{1f525}','✨','\u{1f4ab}','\u{1f31f}','⚡','\u{1f3af}','\u{1f4aa}','\u{1f680}','\u{1f4af}'];
1249
+ function computeCasinoDecorations(cs) {
1250
+ const baseLuck = 50 + Math.random() * 30;
1251
+ const streakBonus = Math.min((cs.consecutivePolls || 0) * 5, 20);
1252
+ return {
1253
+ luckMeter: Math.min(Math.round(baseLuck + streakBonus), 99),
1254
+ houseEdge: Math.round(55 + Math.random() * 45),
1255
+ vibesQuality: CASINO_VIBES[Math.floor(Date.now() / 3000) % CASINO_VIBES.length],
1256
+ };
1257
+ }
1258
+
1244
1259
  function renderCasinoStatsRow(cs) {
1260
+ // Server-supplied (preferred) OR locally computed (default). The
1261
+ // fallback covers older servers that still ship the decorative fields,
1262
+ // plus tests that stub them in explicitly. != null treats undefined and
1263
+ // null as "missing" so legitimate zero values from the server survive.
1264
+ const local = computeCasinoDecorations(cs);
1265
+ const houseEdge = (cs.houseEdge != null) ? cs.houseEdge : local.houseEdge;
1266
+ const luckMeter = (cs.luckMeter != null) ? cs.luckMeter : local.luckMeter;
1267
+ const vibesQuality = cs.vibesQuality || local.vibesQuality;
1268
+
1245
1269
  const netClass = cs.netWinnings >= 0 ? 'pos' : 'neg';
1246
1270
  const netSign = cs.netWinnings >= 0 ? '+' : '';
1247
1271
  const stat = (k, v) => '<span class="stat"><span class="stat-k">' + k + '</span><span class="stat-v">' + v + '</span></span>';
@@ -1256,9 +1280,9 @@ ${pureFnBlock}
1256
1280
  let right = '<div class="stats-group">';
1257
1281
  right += stat('\u{1f4b8} Cost', '<span class="neg">' + dollar + (cs.totalPolls || 0) + '</span>');
1258
1282
  right += stat('\u{1f4b0} Net', '<span class="' + netClass + '">' + netSign + dollar + (cs.netWinnings || 0) + '</span>');
1259
- right += stat('\u{1f3b0} Edge', '<span class="neon">' + (cs.houseEdge || 0) + '%</span>');
1260
- right += stat('\u{1f3b2} Luck', '<span class="gold">' + (cs.luckMeter || 0) + '%</span>');
1261
- right += stat('\u{1f60e} Vibes', escHtml(cs.vibesQuality || ''));
1283
+ right += stat('\u{1f3b0} Edge', '<span class="neon">' + houseEdge + '%</span>');
1284
+ right += stat('\u{1f3b2} Luck', '<span class="gold">' + luckMeter + '%</span>');
1285
+ right += stat('\u{1f60e} Vibes', escHtml(vibesQuality));
1262
1286
  right += stat('\u{1f9e0} Hits', '<span class="pos">' + (cs.dopamineHits || 0) + '</span>');
1263
1287
  if (cs.consecutivePolls > 1) right += stat('\u{1f525} Streak', '<span class="gold">' + cs.consecutivePolls + 'x</span>');
1264
1288
  right += '</div>';
package/src/server/web.js CHANGED
@@ -147,8 +147,12 @@ class WebDashboardServer {
147
147
  // the mode on, so the web dashboard can render the same winnings box
148
148
  // the terminal does. Null when disabled — keeps payload small and
149
149
  // avoids ticking Math.random()/Date.now() into every SSE push when
150
- // nobody's asked for the effect.
151
- casinoStats: s.casinoModeEnabled ? casino.getStats() : null,
150
+ // nobody's asked for the effect. We use `getSerializableStats` (not
151
+ // the full getStats) so the random/clock-driven decorative fields
152
+ // (luckMeter, houseEdge, vibesQuality, timeSinceLastHit) don't
153
+ // defeat the lastPushedJson dedup — the dashboard recomputes them
154
+ // client-side from the stable counters that ARE in the payload.
155
+ casinoStats: s.casinoModeEnabled ? casino.getSerializableStats() : null,
152
156
  projectName: s.projectName,
153
157
 
154
158
  // Activity