roundhouse_ui 0.3.0 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55d676d5630bc1696fd0c44d58494a84263d53c340ceffeb1bc44c04c9201dfa
4
- data.tar.gz: b314d38079f0f8eee84ba9052dd781841fd6dba2fc7f612e82d25466b5c62c9f
3
+ metadata.gz: b76009aa85c003ad62a2e6bacf8efbdf69767bda6e1b9b382de8a606ec3acb8b
4
+ data.tar.gz: 4ae59af49806c3536b6f709f02146338ad69e716257d2223f1fbf5061995390a
5
5
  SHA512:
6
- metadata.gz: 0454ea6d65176e5d5c79893dc52982e985231ff51f6e503624f7d19af60beabec808ced45cc27a2cd3c2021044e5b6d5d7c1bdf26193ef98833a27e6faaad17f
7
- data.tar.gz: 1aa6b74df3071ced45e118dda22aa0d0caadc17a10fd64c4dec45c187dc7d68866e8e417529a6ad424dc467960481a7187de908283e4dc7932c2977ea047e70c
6
+ metadata.gz: 7df7cbc7dbdab13f4821500f1c5e3d8d9ea8357416530662d4e6cd0a38edd427f11eec13d2a7d22ff923b289a5f5dd10edc3ba405217967563bea6e55a7432d4
7
+ data.tar.gz: e9ebfd3e8edf616ed69ae517f92ca2bdda61dd3065e83624d524b4f5d133bd7d395ef7e4cba7d6d477c92dd1547d5495c83fd68822d58a5136db279a64963268
data/README.md CHANGED
@@ -83,6 +83,11 @@ RoundhouseUi.configure do |c|
83
83
  # Fold sidekiq-failures' `failed` set into the Errors view (see below).
84
84
  # No-op unless the sidekiq-failures gem is loaded. Default: off.
85
85
  c.show_sidekiq_failures = true
86
+
87
+ # Set false to hide queue pause/resume controls and the "not enforced" warning,
88
+ # e.g. when you run reliable fetch (super_fetch) instead of RoundhouseUi::Fetch.
89
+ # Default: true.
90
+ # c.pause_enabled = false
86
91
  end
87
92
  ```
88
93
 
@@ -110,7 +115,8 @@ Roundhouse detects whether a fetcher has reported in).
110
115
  > ⚠️ **Sidekiq Pro/Enterprise users:** super_fetch / reliable fetch sets its own
111
116
  > `fetch_class`, and Sidekiq allows only one. Installing `RoundhouseUi::Fetch` would
112
117
  > **replace reliable fetch and lose its crash-recovery guarantees** — don't. On those
113
- > tiers, leave the fetch strategy out; pause/resume stays inert and the UI says so.
118
+ > tiers, leave the fetch strategy out and set `RoundhouseUi.pause_enabled = false` to
119
+ > hide the pause controls and the (otherwise permanent) "not enforced" warning.
114
120
 
115
121
  ## Surfacing sidekiq-failures
116
122
 
@@ -9,9 +9,7 @@ module RoundhouseUi
9
9
 
10
10
  def index
11
11
  @threshold = LONG_RUNNING
12
- @work = Sidekiq::WorkSet.new.map do |process_id, tid, work|
13
- { process: process_id, tid: tid, queue: work.queue, run_at: work.run_at, job: work.job }
14
- end
12
+ @work = Sidekiq::WorkSet.new.map { |process_id, tid, work| normalize(process_id, tid, work) }
15
13
  end
16
14
 
17
15
  def cancel
@@ -21,6 +19,18 @@ module RoundhouseUi
21
19
 
22
20
  private
23
21
 
22
+ # Sidekiq 7+ yields a Sidekiq::Work (queue/run_at/job methods); Sidekiq 6.x
23
+ # yields a plain Hash (string keys, an epoch run_at, a JSON payload). Normalize
24
+ # both to the same shape the view expects (run_at as a Time, job as a JobRecord).
25
+ def normalize(process_id, tid, work)
26
+ if work.respond_to?(:queue)
27
+ { process: process_id, tid: tid, queue: work.queue, run_at: work.run_at, job: work.job }
28
+ else
29
+ { process: process_id, tid: tid, queue: work["queue"],
30
+ run_at: Time.at(work["run_at"]), job: Sidekiq::JobRecord.new(work["payload"]) }
31
+ end
32
+ end
33
+
24
34
  def require_writable!
25
35
  return unless RoundhouseUi.read_only
26
36
  redirect_to busy_path, alert: "Roundhouse is in read-only mode — cancellation is disabled."
@@ -13,6 +13,10 @@ module RoundhouseUi
13
13
  capsules = Hash.new { |h, k| h[k] = { name: k, concurrency: 0, processes: 0, queues: {} } }
14
14
 
15
15
  Sidekiq::ProcessSet.new.each do |process|
16
+ # `capsules` is a Sidekiq 8.0.8+ Process attribute; absent on 6.x/early 7,
17
+ # where calling it would raise NoMethodError. Skip → view shows empty state.
18
+ next unless process.respond_to?(:capsules)
19
+
16
20
  (process.capsules || {}).each do |name, data|
17
21
  cap = capsules[name]
18
22
  cap[:concurrency] += data["concurrency"].to_i
@@ -46,6 +46,8 @@
46
46
  .rh-badge.crit { background:rgba(230,106,96,.16); color:var(--crit); }
47
47
 
48
48
  .rh-main { padding:22px 30px 70px; max-width:1180px; }
49
+ :root[data-width="full"] .rh-main { max-width:none; }
50
+ .rh-iconbtn.is-on { color:var(--text); border-color:var(--accent); }
49
51
  .rh-top { display:flex; align-items:center; gap:14px; margin-bottom:22px; }
50
52
  .rh-top h1 { font-size:21px; font-weight:650; letter-spacing:-.02em; margin:0; }
51
53
  .rh-crumb { color:var(--faint); font-size:13px; }
@@ -148,7 +150,7 @@
148
150
  </style>
149
151
  <%# Apply the saved theme before first paint so it persists across navigations with no flash. %>
150
152
  <script nonce="<%= content_nonce %>">
151
- (function () { try { var t = localStorage.getItem("rh-theme"); if (t) document.documentElement.setAttribute("data-theme", t); } catch (e) {} })();
153
+ (function () { try { var t = localStorage.getItem("rh-theme"); if (t) document.documentElement.setAttribute("data-theme", t); var w = localStorage.getItem("rh-width"); if (w) document.documentElement.setAttribute("data-width", w); } catch (e) {} })();
152
154
  </script>
153
155
  <%# Turbo Drive: link navigation without full-page reloads (kills the flicker). %>
154
156
  <script src="<%= turbo_js_path %>"></script>
@@ -164,15 +166,29 @@
164
166
  return "~" + (s / 86400).toFixed(1) + "d";
165
167
  }
166
168
 
169
+ // Points to display, from the window picker (24 pts ≈ 1m at the 2.5s poll).
170
+ function chartWindow() { var s = document.getElementById("rh-chart-window"); return s ? parseInt(s.value, 10) : 24; }
171
+ // Moving average sized to the window — flattens the per-poll spikes into a
172
+ // sustained line instead of a noisy sawtooth.
173
+ function smooth(arr) {
174
+ var k = Math.max(2, Math.round(arr.length / 12)), out = [];
175
+ for (var i = 0; i < arr.length; i++) {
176
+ var a = Math.max(0, i - k + 1), sum = 0, c = 0;
177
+ for (var j = a; j <= i; j++) { sum += arr[j]; c++; }
178
+ out.push(sum / c);
179
+ }
180
+ return out;
181
+ }
167
182
  function draw() {
168
183
  var cv = document.getElementById("rh-chart"); if (!cv) return;
169
- var ctx = cv.getContext("2d"), w = cv.width, h = cv.height, pad = 6, n = series.length;
170
- ctx.clearRect(0, 0, w, h); if (n < 2) return;
171
- var max = Math.max.apply(null, series) * 1.25 || 1;
184
+ var ctx = cv.getContext("2d"), w = cv.width, h = cv.height, pad = 6;
185
+ ctx.clearRect(0, 0, w, h);
186
+ var pts = smooth(series.slice(-chartWindow())), n = pts.length; if (n < 2) return;
187
+ var max = Math.max.apply(null, pts) * 1.25 || 1;
172
188
  var x = function (i) { return i / (n - 1) * w; }, y = function (v) { return h - pad - v / max * (h - pad * 2); };
173
189
  var g = ctx.createLinearGradient(0, 0, 0, h); g.addColorStop(0, "rgba(110,139,255,.30)"); g.addColorStop(1, "rgba(110,139,255,0)");
174
- ctx.beginPath(); ctx.moveTo(0, h); series.forEach(function (v, i) { ctx.lineTo(x(i), y(v)); }); ctx.lineTo(w, h); ctx.closePath(); ctx.fillStyle = g; ctx.fill();
175
- ctx.beginPath(); series.forEach(function (v, i) { i ? ctx.lineTo(x(i), y(v)) : ctx.moveTo(x(i), y(v)); }); ctx.strokeStyle = "#6E8BFF"; ctx.lineWidth = 2; ctx.lineJoin = "round"; ctx.stroke();
190
+ ctx.beginPath(); ctx.moveTo(0, h); pts.forEach(function (v, i) { ctx.lineTo(x(i), y(v)); }); ctx.lineTo(w, h); ctx.closePath(); ctx.fillStyle = g; ctx.fill();
191
+ ctx.beginPath(); pts.forEach(function (v, i) { i ? ctx.lineTo(x(i), y(v)) : ctx.moveTo(x(i), y(v)); }); ctx.strokeStyle = "#6E8BFF"; ctx.lineWidth = 2; ctx.lineJoin = "round"; ctx.stroke();
176
192
  }
177
193
  function apply(d) {
178
194
  Object.keys(d).forEach(function (k) {
@@ -194,7 +210,7 @@
194
210
  var rate = dt > 0 ? Math.max(0, dp / dt) : 0;
195
211
  var re = document.querySelector('[data-stat="rate"]'); if (re) re.textContent = fmt(Math.round(rate * 60));
196
212
  var ne = document.getElementById("rh-chart-now"); if (ne) ne.textContent = Math.round(rate);
197
- series.push(rate); if (series.length > 60) series.shift(); draw();
213
+ series.push(rate); if (series.length > 360) series.shift(); draw();
198
214
 
199
215
  // Metrics tab live rates (these elements exist only on that page).
200
216
  setText("rh-m-throughput", Math.round(rate) + "/s");
@@ -212,6 +228,7 @@
212
228
  }
213
229
  function startOnce() { if (started) return; started = true; poll(); setInterval(poll, 2500); }
214
230
  function syncTheme() { var b = document.getElementById("rh-theme"); if (b) b.textContent = document.documentElement.getAttribute("data-theme") === "light" ? "☀" : "☾"; }
231
+ function syncWidth() { var b = document.getElementById("rh-width"); if (b) b.classList.toggle("is-on", document.documentElement.getAttribute("data-width") === "full"); }
215
232
 
216
233
  // The sidebar is turbo-permanent (so its badges don't flicker), so we move
217
234
  // the active-nav highlight ourselves on each navigation.
@@ -233,6 +250,14 @@
233
250
  try { localStorage.setItem("rh-theme", next); } catch (_) {}
234
251
  syncTheme();
235
252
  });
253
+ // width toggle (compact 1180px ⟷ full) — same persistence pattern as theme
254
+ document.addEventListener("click", function (e) {
255
+ var b = e.target.closest && e.target.closest("#rh-width"); if (!b) return;
256
+ var next = document.documentElement.getAttribute("data-width") === "full" ? "compact" : "full";
257
+ document.documentElement.setAttribute("data-width", next);
258
+ try { localStorage.setItem("rh-width", next); } catch (_) {}
259
+ syncWidth();
260
+ });
236
261
  // command palette (⌘K)
237
262
  var CMDS = [
238
263
  { label: "Dashboard", path: "<%= root_path %>", icon: "▦" },
@@ -244,12 +269,13 @@
244
269
  { label: "Retries", path: "<%= retries_path %>", icon: "↻" },
245
270
  { label: "Errors", path: "<%= errors_path %>", icon: "⚠" },
246
271
  { label: "Dead", path: "<%= dead_set_path %>", icon: "✕" },
247
- { label: "Capsules", path: "<%= capsules_path %>", icon: "⊞" },
272
+ <% if defined?(Sidekiq::Capsule) %>{ label: "Capsules", path: "<%= capsules_path %>", icon: "⊞" },<% end %>
248
273
  { label: "Redis", path: "<%= redis_info_path %>", icon: "◈" },
249
274
  { label: "Snapshots", path: "<%= snapshots_path %>", icon: "⛁" },
250
275
  { label: "Audit log", path: "<%= audit_log_path %>", icon: "☷" },
251
276
  <% if RoundhouseUi.allow_job_editing %>{ label: "Enqueue job", path: "<%= new_job_path %>", icon: "+" },<% end %>
252
- { label: "Toggle theme", action: "theme", icon: "◐" }
277
+ { label: "Toggle theme", action: "theme", icon: "◐" },
278
+ { label: "Toggle full width", action: "width", icon: "⟷" }
253
279
  ];
254
280
  var palSel = 0, palFiltered = [];
255
281
  function palEl() { return document.getElementById("rh-palette"); }
@@ -280,6 +306,7 @@
280
306
  function palRun(c) {
281
307
  palClose();
282
308
  if (c.action === "theme") { var b = document.getElementById("rh-theme"); if (b) b.click(); return; }
309
+ if (c.action === "width") { var wb = document.getElementById("rh-width"); if (wb) wb.click(); return; }
283
310
  if (c.path) { window.Turbo ? Turbo.visit(c.path) : (location.href = c.path); }
284
311
  }
285
312
  function palOpen() { var p = palEl(), i = palInputEl(); if (!p || !i) return; p.hidden = false; i.value = ""; palSel = 0; palRender(); i.focus(); }
@@ -290,6 +317,17 @@
290
317
  if (e.target && e.target.id === "rh-palette") { palClose(); return; }
291
318
  if (e.target.closest && e.target.closest("#rh-palette-open")) { e.preventDefault(); palOpen(); }
292
319
  });
320
+ // Throughput window picker — persist the choice and redraw the smoothed line.
321
+ function restoreChartWindow() {
322
+ var s = document.getElementById("rh-chart-window"); if (!s) return;
323
+ try { var v = localStorage.getItem("rh-chart-win"); if (v) s.value = v; } catch (_) {}
324
+ }
325
+ document.addEventListener("change", function (e) {
326
+ if (!e.target || e.target.id !== "rh-chart-window") return;
327
+ try { localStorage.setItem("rh-chart-win", e.target.value); } catch (_) {}
328
+ draw();
329
+ });
330
+
293
331
  document.addEventListener("keydown", function (e) {
294
332
  if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) { e.preventDefault(); var p = palEl(); (p && p.hidden) ? palOpen() : palClose(); return; }
295
333
  var p = palEl(); if (!p || p.hidden) return;
@@ -299,8 +337,8 @@
299
337
  else if (e.key === "Enter") { e.preventDefault(); if (palFiltered[palSel]) palRun(palFiltered[palSel]); }
300
338
  });
301
339
 
302
- document.addEventListener("turbo:load", function () { startOnce(); syncTheme(); setActiveNav(); draw(); });
303
- document.addEventListener("DOMContentLoaded", function () { startOnce(); syncTheme(); setActiveNav(); });
340
+ document.addEventListener("turbo:load", function () { startOnce(); syncTheme(); syncWidth(); setActiveNav(); restoreChartWindow(); draw(); });
341
+ document.addEventListener("DOMContentLoaded", function () { startOnce(); syncTheme(); syncWidth(); setActiveNav(); restoreChartWindow(); });
304
342
  document.addEventListener("visibilitychange", function () { if (!document.hidden) poll(); });
305
343
  })();
306
344
  </script>
@@ -330,7 +368,10 @@
330
368
  </nav>
331
369
  <nav class="rh-grp">
332
370
  <div class="rh-grp-label">Infrastructure</div>
333
- <%= nav_link "Capsules", capsules_path, icon: "⊞" %>
371
+ <%# Capsules are a Sidekiq 7+ concept — hide the nav entirely on 6.x. %>
372
+ <% if defined?(Sidekiq::Capsule) %>
373
+ <%= nav_link "Capsules", capsules_path, icon: "⊞" %>
374
+ <% end %>
334
375
  <%= nav_link "Redis", redis_info_path, icon: "◈" %>
335
376
  <%= nav_link "Snapshots", snapshots_path, icon: "⛁" %>
336
377
  <%= nav_link "Audit log", audit_log_path, icon: "☷" %>
@@ -345,6 +386,7 @@
345
386
  <span class="rh-live"><span class="rh-dot"></span> live · <span class="num" data-stat="rate">—</span>/min</span>
346
387
  <% if RoundhouseUi.read_only %><span class="rh-ro">read-only</span><% end %>
347
388
  <button class="rh-kbd" id="rh-palette-open" type="button" title="Command palette (⌘K)">⌘K</button>
389
+ <button class="rh-iconbtn" id="rh-width" type="button" title="Toggle full width" aria-label="Toggle full width">⟷</button>
348
390
  <button class="rh-iconbtn" id="rh-theme" type="button" title="Toggle theme" aria-label="Toggle theme">☾</button>
349
391
  </header>
350
392
 
@@ -42,7 +42,7 @@
42
42
  </div>
43
43
 
44
44
  <div class="rh-chart-wrap">
45
- <div class="top"><h3>Throughput</h3><span class="rh-sub">jobs / sec · live</span><span class="now"><span id="rh-chart-now">—</span>/s</span></div>
45
+ <div class="top"><h3>Throughput</h3><span class="rh-sub">jobs / sec · live</span><span class="now"><span id="rh-chart-now">—</span>/s</span><select id="rh-chart-window" aria-label="Throughput window" style="margin-left:12px;background:var(--panel-2);color:var(--muted);border:1px solid var(--line);border-radius:7px;padding:3px 6px;font:12px var(--sans)"><option value="24">1m</option><option value="120">5m</option><option value="360">15m</option></select></div>
46
46
  <canvas id="rh-chart" width="1100" height="180"></canvas>
47
47
  </div>
48
48
 
@@ -4,7 +4,7 @@
4
4
  <div class="rh-toolbar"><%= link_to "+ Enqueue job", new_job_path, class: "rh-btn rh-btn-primary" %></div>
5
5
  <% end %>
6
6
 
7
- <% unless @fetch_installed %>
7
+ <% if RoundhouseUi.pause_enabled && !@fetch_installed %>
8
8
  <div class="rh-warn">
9
9
  Pausing is <strong>recorded but not enforced</strong> — no Roundhouse fetcher has reported in.
10
10
  Add it to your Sidekiq config: <code>config[:fetch_class] = RoundhouseUi::Fetch</code>
@@ -26,10 +26,12 @@
26
26
  <td class="r"><%= number_with_delimiter q.size %></td>
27
27
  <td class="r <%= "rh-lat-warn" if q.latency > 60 %>"><%= q.latency.round(2) %>s</td>
28
28
  <td class="r">
29
- <% if paused %>
30
- <%= button_to "Resume", resume_queue_path(q.name), method: :post, class: "rh-btn", form_class: "rh-inline" %>
31
- <% else %>
32
- <%= button_to "Pause", pause_queue_path(q.name), method: :post, class: "rh-btn", form_class: "rh-inline" %>
29
+ <% if RoundhouseUi.pause_enabled %>
30
+ <% if paused %>
31
+ <%= button_to "Resume", resume_queue_path(q.name), method: :post, class: "rh-btn", form_class: "rh-inline" %>
32
+ <% else %>
33
+ <%= button_to "Pause", pause_queue_path(q.name), method: :post, class: "rh-btn", form_class: "rh-inline" %>
34
+ <% end %>
33
35
  <% end %>
34
36
  <%= button_to "Snapshot", snapshot_queue_path(q.name), method: :post, class: "rh-btn", form_class: "rh-inline" %>
35
37
  <%= button_to "Purge", purge_queue_path(q.name),
@@ -1,15 +1,17 @@
1
1
  <% content_for :title, "Workers" %>
2
2
 
3
3
  <h2 class="rh-h2">Workers · <%= @processes.size %> <%= "process".pluralize(@processes.size) %></h2>
4
- <p class="rh-sub" style="margin:-4px 0 16px">
5
- Fetch strategy:
6
- <% if @fetch_active %>
7
- <span class="rh-st rh-st-ok">RoundhouseUi::Fetch — pause-aware</span>
8
- <% else %>
9
- <span class="rh-st rh-st-paused">default — pause not enforced</span>
10
- <% end %>
11
- <span class="rh-sub" style="margin-left:6px">crash-reliable fetch (super_fetch) is Sidekiq Pro</span>
12
- </p>
4
+ <% if RoundhouseUi.pause_enabled %>
5
+ <p class="rh-sub" style="margin:-4px 0 16px">
6
+ Fetch strategy:
7
+ <% if @fetch_active %>
8
+ <span class="rh-st rh-st-ok">RoundhouseUi::Fetch — pause-aware</span>
9
+ <% else %>
10
+ <span class="rh-st rh-st-paused">default — pause not enforced</span>
11
+ <% end %>
12
+ <span class="rh-sub" style="margin-left:6px">crash-reliable fetch (super_fetch) is Sidekiq Pro</span>
13
+ </p>
14
+ <% end %>
13
15
  <table class="rh-table">
14
16
  <thead><tr><th>Process</th><th class="r">Threads</th><th>Queues</th><th class="r">Started</th><th class="r">Heartbeat</th><th class="r">Actions</th></tr></thead>
15
17
  <tbody>
@@ -1,3 +1,3 @@
1
1
  module RoundhouseUi
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/roundhouse_ui.rb CHANGED
@@ -57,6 +57,13 @@ module RoundhouseUi
57
57
  # Sidekiq's retry/dead sets, so this is the only way to surface them here.
58
58
  attr_accessor :show_sidekiq_failures
59
59
 
60
+ # Queue pause/resume is only enforced when RoundhouseUi::Fetch is installed
61
+ # as the server's fetch strategy. If you run reliable fetch (Sidekiq
62
+ # Pro/Enterprise super_fetch) you can't also run our fetcher, so pause can't
63
+ # be enforced — set this to false to hide the pause controls and the
64
+ # "not enforced" warning entirely. Default: true.
65
+ attr_accessor :pause_enabled
66
+
60
67
  # Configure in an initializer:
61
68
  #
62
69
  # RoundhouseUi.configure do |c|
@@ -77,4 +84,5 @@ module RoundhouseUi
77
84
  self.allow_job_editing = false
78
85
  self.redact_args = []
79
86
  self.show_sidekiq_failures = false
87
+ self.pause_enabled = true
80
88
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roundhouse_ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - R.J. Robinson