roundhouse_ui 0.4.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: 30b9ee99930148d47a69b86e7a4c079231f8a2504a9a24aaa17a625e39e96429
4
- data.tar.gz: 333e7892afa4690288d35c671249f3fad6c3a19e304f9b4952346fe0f873a9fd
3
+ metadata.gz: b76009aa85c003ad62a2e6bacf8efbdf69767bda6e1b9b382de8a606ec3acb8b
4
+ data.tar.gz: 4ae59af49806c3536b6f709f02146338ad69e716257d2223f1fbf5061995390a
5
5
  SHA512:
6
- metadata.gz: 9391d5af40b1fa0db7de47fab56009cf6d11bb80f2c16d4d65bc4b7b2b376eb7ac7cff595dfe0ebdaa0df401436f811cfc1f9a3d52028aef491c7d5821b0e322
7
- data.tar.gz: 8099ad548223bbb7d91f13a51b86e62010eb269f446d380c95fa4a416a1f4f95979631e9cbdd5115973bfa98d363ab04f5ef9d1801462a51ce215a5a28f57abe
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
@@ -166,15 +166,29 @@
166
166
  return "~" + (s / 86400).toFixed(1) + "d";
167
167
  }
168
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
+ }
169
182
  function draw() {
170
183
  var cv = document.getElementById("rh-chart"); if (!cv) return;
171
- var ctx = cv.getContext("2d"), w = cv.width, h = cv.height, pad = 6, n = series.length;
172
- ctx.clearRect(0, 0, w, h); if (n < 2) return;
173
- 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;
174
188
  var x = function (i) { return i / (n - 1) * w; }, y = function (v) { return h - pad - v / max * (h - pad * 2); };
175
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)");
176
- 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();
177
- 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();
178
192
  }
179
193
  function apply(d) {
180
194
  Object.keys(d).forEach(function (k) {
@@ -196,7 +210,7 @@
196
210
  var rate = dt > 0 ? Math.max(0, dp / dt) : 0;
197
211
  var re = document.querySelector('[data-stat="rate"]'); if (re) re.textContent = fmt(Math.round(rate * 60));
198
212
  var ne = document.getElementById("rh-chart-now"); if (ne) ne.textContent = Math.round(rate);
199
- series.push(rate); if (series.length > 60) series.shift(); draw();
213
+ series.push(rate); if (series.length > 360) series.shift(); draw();
200
214
 
201
215
  // Metrics tab live rates (these elements exist only on that page).
202
216
  setText("rh-m-throughput", Math.round(rate) + "/s");
@@ -255,7 +269,7 @@
255
269
  { label: "Retries", path: "<%= retries_path %>", icon: "↻" },
256
270
  { label: "Errors", path: "<%= errors_path %>", icon: "⚠" },
257
271
  { label: "Dead", path: "<%= dead_set_path %>", icon: "✕" },
258
- { label: "Capsules", path: "<%= capsules_path %>", icon: "⊞" },
272
+ <% if defined?(Sidekiq::Capsule) %>{ label: "Capsules", path: "<%= capsules_path %>", icon: "⊞" },<% end %>
259
273
  { label: "Redis", path: "<%= redis_info_path %>", icon: "◈" },
260
274
  { label: "Snapshots", path: "<%= snapshots_path %>", icon: "⛁" },
261
275
  { label: "Audit log", path: "<%= audit_log_path %>", icon: "☷" },
@@ -303,6 +317,17 @@
303
317
  if (e.target && e.target.id === "rh-palette") { palClose(); return; }
304
318
  if (e.target.closest && e.target.closest("#rh-palette-open")) { e.preventDefault(); palOpen(); }
305
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
+
306
331
  document.addEventListener("keydown", function (e) {
307
332
  if ((e.metaKey || e.ctrlKey) && (e.key === "k" || e.key === "K")) { e.preventDefault(); var p = palEl(); (p && p.hidden) ? palOpen() : palClose(); return; }
308
333
  var p = palEl(); if (!p || p.hidden) return;
@@ -312,8 +337,8 @@
312
337
  else if (e.key === "Enter") { e.preventDefault(); if (palFiltered[palSel]) palRun(palFiltered[palSel]); }
313
338
  });
314
339
 
315
- document.addEventListener("turbo:load", function () { startOnce(); syncTheme(); syncWidth(); setActiveNav(); draw(); });
316
- document.addEventListener("DOMContentLoaded", function () { startOnce(); syncTheme(); syncWidth(); setActiveNav(); });
340
+ document.addEventListener("turbo:load", function () { startOnce(); syncTheme(); syncWidth(); setActiveNav(); restoreChartWindow(); draw(); });
341
+ document.addEventListener("DOMContentLoaded", function () { startOnce(); syncTheme(); syncWidth(); setActiveNav(); restoreChartWindow(); });
317
342
  document.addEventListener("visibilitychange", function () { if (!document.hidden) poll(); });
318
343
  })();
319
344
  </script>
@@ -343,7 +368,10 @@
343
368
  </nav>
344
369
  <nav class="rh-grp">
345
370
  <div class="rh-grp-label">Infrastructure</div>
346
- <%= 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 %>
347
375
  <%= nav_link "Redis", redis_info_path, icon: "◈" %>
348
376
  <%= nav_link "Snapshots", snapshots_path, icon: "⛁" %>
349
377
  <%= nav_link "Audit log", audit_log_path, icon: "☷" %>
@@ -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.4.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.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - R.J. Robinson