solid_queue_monitor 0.4.0 → 0.6.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.
@@ -51,6 +51,7 @@ module SolidQueueMonitor
51
51
  #{generate_footer}
52
52
  </div>
53
53
  #{generate_auto_refresh_script}
54
+ #{generate_chart_script}
54
55
  HTML
55
56
  end
56
57
 
@@ -87,20 +88,32 @@ module SolidQueueMonitor
87
88
  end
88
89
 
89
90
  def generate_header
91
+ nav_items = [
92
+ { path: root_path, label: 'Overview', match: 'Overview' },
93
+ { path: ready_jobs_path, label: 'Ready Jobs', match: 'Ready Jobs' },
94
+ { path: in_progress_jobs_path, label: 'In Progress Jobs', match: 'In Progress' },
95
+ { path: scheduled_jobs_path, label: 'Scheduled Jobs', match: 'Scheduled Jobs' },
96
+ { path: recurring_jobs_path, label: 'Recurring Jobs', match: 'Recurring Jobs' },
97
+ { path: failed_jobs_path, label: 'Failed Jobs', match: 'Failed Jobs' },
98
+ { path: queues_path, label: 'Queues', match: 'Queues' }
99
+ ]
100
+
101
+ nav_links = nav_items.map do |item|
102
+ active_class = @title&.include?(item[:match]) ? 'active' : ''
103
+ "<a href=\"#{item[:path]}\" class=\"nav-link #{active_class}\">#{item[:label]}</a>"
104
+ end.join("\n ")
105
+
90
106
  <<-HTML
91
107
  <header>
92
108
  <div class="header-top">
93
109
  <h1>Solid Queue Monitor</h1>
94
- #{generate_auto_refresh_controls}
110
+ <div class="header-controls">
111
+ #{generate_auto_refresh_controls}
112
+ #{generate_theme_toggle}
113
+ </div>
95
114
  </div>
96
115
  <nav class="navigation">
97
- <a href="#{root_path}" class="nav-link">Overview</a>
98
- <a href="#{ready_jobs_path}" class="nav-link">Ready Jobs</a>
99
- <a href="#{in_progress_jobs_path}" class="nav-link">In Progress Jobs</a>
100
- <a href="#{scheduled_jobs_path}" class="nav-link">Scheduled Jobs</a>
101
- <a href="#{recurring_jobs_path}" class="nav-link">Recurring Jobs</a>
102
- <a href="#{failed_jobs_path}" class="nav-link">Failed Jobs</a>
103
- <a href="#{queues_path}" class="nav-link">Queues</a>
116
+ #{nav_links}
104
117
  </nav>
105
118
  </header>
106
119
  HTML
@@ -135,6 +148,27 @@ module SolidQueueMonitor
135
148
  HTML
136
149
  end
137
150
 
151
+ def generate_theme_toggle
152
+ <<-HTML
153
+ <button class="theme-toggle-btn" id="theme-toggle-btn" title="Toggle dark mode">
154
+ <svg class="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
155
+ <circle cx="12" cy="12" r="5"></circle>
156
+ <line x1="12" y1="1" x2="12" y2="3"></line>
157
+ <line x1="12" y1="21" x2="12" y2="23"></line>
158
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
159
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
160
+ <line x1="1" y1="12" x2="3" y2="12"></line>
161
+ <line x1="21" y1="12" x2="23" y2="12"></line>
162
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
163
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
164
+ </svg>
165
+ <svg class="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
166
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
167
+ </svg>
168
+ </button>
169
+ HTML
170
+ end
171
+
138
172
  def generate_auto_refresh_script
139
173
  return '' unless SolidQueueMonitor.auto_refresh_enabled
140
174
 
@@ -212,6 +246,132 @@ module SolidQueueMonitor
212
246
  JS
213
247
  end
214
248
 
249
+ def generate_chart_script
250
+ <<-HTML
251
+ <script>
252
+ #{theme_toggle_javascript}
253
+ #{chart_tooltip_javascript}
254
+ </script>
255
+ HTML
256
+ end
257
+
258
+ def theme_toggle_javascript
259
+ <<-JS
260
+ (function() {
261
+ var body = document.body;
262
+ var themeBtn = document.getElementById('theme-toggle-btn');
263
+ var storageKey = 'sqm_dark_theme';
264
+
265
+ // Check for saved preference or system preference
266
+ function getPreferredTheme() {
267
+ var saved = localStorage.getItem(storageKey);
268
+ if (saved !== null) {
269
+ return saved === 'true';
270
+ }
271
+ // Check system preference
272
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
273
+ }
274
+
275
+ function setTheme(isDark) {
276
+ if (isDark) {
277
+ body.classList.add('dark-theme');
278
+ } else {
279
+ body.classList.remove('dark-theme');
280
+ }
281
+ localStorage.setItem(storageKey, isDark ? 'true' : 'false');
282
+ }
283
+
284
+ // Initialize theme
285
+ setTheme(getPreferredTheme());
286
+
287
+ // Toggle on button click
288
+ if (themeBtn) {
289
+ themeBtn.addEventListener('click', function() {
290
+ var isDark = body.classList.contains('dark-theme');
291
+ setTheme(!isDark);
292
+ });
293
+ }
294
+
295
+ // Listen for system preference changes
296
+ if (window.matchMedia) {
297
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
298
+ // Only auto-switch if user hasn't manually set a preference
299
+ if (localStorage.getItem(storageKey) === null) {
300
+ setTheme(e.matches);
301
+ }
302
+ });
303
+ }
304
+ })();
305
+ JS
306
+ end
307
+
308
+ def chart_tooltip_javascript
309
+ <<-JS
310
+ (function() {
311
+ // Chart collapse/expand functionality
312
+ var chartSection = document.getElementById('chart-section');
313
+ var toggleBtn = document.getElementById('chart-toggle-btn');
314
+
315
+ if (chartSection && toggleBtn) {
316
+ var isCollapsed = localStorage.getItem('sqm_chart_collapsed') === 'true';
317
+
318
+ if (isCollapsed) {
319
+ chartSection.classList.add('collapsed');
320
+ }
321
+
322
+ toggleBtn.addEventListener('click', function() {
323
+ chartSection.classList.toggle('collapsed');
324
+ var collapsed = chartSection.classList.contains('collapsed');
325
+ localStorage.setItem('sqm_chart_collapsed', collapsed ? 'true' : 'false');
326
+ });
327
+ }
328
+
329
+ // Chart tooltip functionality
330
+ var tooltip = document.getElementById('chart-tooltip');
331
+ if (!tooltip) return;
332
+
333
+ var dataPoints = document.querySelectorAll('.data-point');
334
+ var seriesNames = { created: 'Created', completed: 'Completed', failed: 'Failed' };
335
+
336
+ dataPoints.forEach(function(point) {
337
+ point.addEventListener('mouseenter', function(e) {
338
+ var series = this.getAttribute('data-series');
339
+ var label = this.getAttribute('data-label');
340
+ var value = this.getAttribute('data-value');
341
+
342
+ tooltip.querySelector('.tooltip-label').textContent = label;
343
+ tooltip.querySelector('.tooltip-value').textContent = seriesNames[series] + ': ' + value;
344
+ tooltip.style.display = 'block';
345
+ positionTooltip(e);
346
+ });
347
+
348
+ point.addEventListener('mousemove', function(e) {
349
+ positionTooltip(e);
350
+ });
351
+
352
+ point.addEventListener('mouseleave', function() {
353
+ tooltip.style.display = 'none';
354
+ });
355
+ });
356
+
357
+ function positionTooltip(e) {
358
+ var x = e.clientX + 10;
359
+ var y = e.clientY - 30;
360
+
361
+ if (x + tooltip.offsetWidth > window.innerWidth) {
362
+ x = e.clientX - tooltip.offsetWidth - 10;
363
+ }
364
+ if (y < 0) {
365
+ y = e.clientY + 10;
366
+ }
367
+
368
+ tooltip.style.left = x + 'px';
369
+ tooltip.style.top = y + 'px';
370
+ }
371
+ })();
372
+ JS
373
+ end
374
+
215
375
  def default_url_options
216
376
  { only_path: true }
217
377
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueMonitor
4
+ class QueuePauseService
5
+ delegate :paused?, to: :@queue
6
+
7
+ def initialize(queue_name)
8
+ @queue_name = queue_name
9
+ @queue = SolidQueue::Queue.new(queue_name)
10
+ end
11
+
12
+ def pause
13
+ return { success: false, message: "Queue '#{@queue_name}' is already paused" } if paused?
14
+
15
+ @queue.pause
16
+ { success: true, message: "Queue '#{@queue_name}' has been paused" }
17
+ rescue StandardError => e
18
+ { success: false, message: "Failed to pause queue: #{e.message}" }
19
+ end
20
+
21
+ def resume
22
+ return { success: false, message: "Queue '#{@queue_name}' is not paused" } unless paused?
23
+
24
+ @queue.resume
25
+ { success: true, message: "Queue '#{@queue_name}' has been resumed" }
26
+ rescue StandardError => e
27
+ { success: false, message: "Failed to resume queue: #{e.message}" }
28
+ end
29
+
30
+ def self.paused_queues
31
+ SolidQueue::Pause.pluck(:queue_name)
32
+ end
33
+ end
34
+ end