solid_queue_monitor 1.3.0 → 2.0.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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -4
  3. data/app/assets/javascripts/solid_queue_monitor/application.js +393 -0
  4. data/app/{services/solid_queue_monitor/stylesheet_generator.rb → assets/stylesheets/solid_queue_monitor/application.css} +23 -12
  5. data/app/controllers/solid_queue_monitor/application_controller.rb +2 -2
  6. data/app/controllers/solid_queue_monitor/assets_controller.rb +52 -0
  7. data/app/controllers/solid_queue_monitor/base_controller.rb +0 -29
  8. data/app/controllers/solid_queue_monitor/failed_jobs_controller.rb +3 -7
  9. data/app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb +3 -6
  10. data/app/controllers/solid_queue_monitor/jobs_controller.rb +3 -7
  11. data/app/controllers/solid_queue_monitor/overview_controller.rb +3 -12
  12. data/app/controllers/solid_queue_monitor/queues_controller.rb +4 -18
  13. data/app/controllers/solid_queue_monitor/ready_jobs_controller.rb +3 -6
  14. data/app/controllers/solid_queue_monitor/recurring_jobs_controller.rb +3 -6
  15. data/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb +3 -7
  16. data/app/controllers/solid_queue_monitor/search_controller.rb +3 -4
  17. data/app/controllers/solid_queue_monitor/workers_controller.rb +24 -8
  18. data/app/helpers/solid_queue_monitor/application_helper.rb +46 -0
  19. data/app/helpers/solid_queue_monitor/chart_helper.rb +293 -0
  20. data/app/helpers/solid_queue_monitor/job_details_helper.rb +66 -0
  21. data/app/helpers/solid_queue_monitor/jobs_helper.rb +134 -0
  22. data/app/helpers/solid_queue_monitor/pagination_helper.rb +23 -0
  23. data/app/helpers/solid_queue_monitor/sort_helper.rb +30 -0
  24. data/app/helpers/solid_queue_monitor/workers_helper.rb +88 -0
  25. data/app/services/solid_queue_monitor/asset_cache.rb +56 -0
  26. data/app/views/layouts/solid_queue_monitor/application.html.erb +25 -0
  27. data/app/views/solid_queue_monitor/failed_jobs/_row.html.erb +26 -0
  28. data/app/views/solid_queue_monitor/failed_jobs/index.html.erb +38 -0
  29. data/app/views/solid_queue_monitor/in_progress_jobs/_row.html.erb +13 -0
  30. data/app/views/solid_queue_monitor/in_progress_jobs/index.html.erb +25 -0
  31. data/app/views/solid_queue_monitor/jobs/_arguments.html.erb +9 -0
  32. data/app/views/solid_queue_monitor/jobs/_error.html.erb +26 -0
  33. data/app/views/solid_queue_monitor/jobs/_execution_history.html.erb +25 -0
  34. data/app/views/solid_queue_monitor/jobs/_header.html.erb +37 -0
  35. data/app/views/solid_queue_monitor/jobs/_metadata.html.erb +22 -0
  36. data/app/views/solid_queue_monitor/jobs/_raw_data.html.erb +11 -0
  37. data/app/views/solid_queue_monitor/jobs/_timeline.html.erb +29 -0
  38. data/app/views/solid_queue_monitor/jobs/_timing.html.erb +9 -0
  39. data/app/views/solid_queue_monitor/jobs/_worker.html.erb +12 -0
  40. data/app/views/solid_queue_monitor/jobs/show.html.erb +22 -0
  41. data/app/views/solid_queue_monitor/overview/_chart.html.erb +1 -0
  42. data/app/views/solid_queue_monitor/overview/_recent_job_row.html.erb +26 -0
  43. data/app/views/solid_queue_monitor/overview/_recent_jobs.html.erb +31 -0
  44. data/app/views/solid_queue_monitor/overview/_stat_card.html.erb +4 -0
  45. data/app/views/solid_queue_monitor/overview/_stats.html.erb +11 -0
  46. data/app/views/solid_queue_monitor/overview/index.html.erb +9 -0
  47. data/app/views/solid_queue_monitor/queues/_job_row.html.erb +26 -0
  48. data/app/views/solid_queue_monitor/queues/_row.html.erb +33 -0
  49. data/app/views/solid_queue_monitor/queues/index.html.erb +18 -0
  50. data/app/views/solid_queue_monitor/queues/show.html.erb +63 -0
  51. data/app/views/solid_queue_monitor/ready_jobs/_row.html.erb +7 -0
  52. data/app/views/solid_queue_monitor/ready_jobs/index.html.erb +25 -0
  53. data/app/views/solid_queue_monitor/recurring_jobs/_row.html.erb +8 -0
  54. data/app/views/solid_queue_monitor/recurring_jobs/index.html.erb +26 -0
  55. data/app/views/solid_queue_monitor/scheduled_jobs/_row.html.erb +7 -0
  56. data/app/views/solid_queue_monitor/scheduled_jobs/index.html.erb +36 -0
  57. data/app/views/solid_queue_monitor/search/_completed_row.html.erb +6 -0
  58. data/app/views/solid_queue_monitor/search/_failed_row.html.erb +6 -0
  59. data/app/views/solid_queue_monitor/search/_job_row.html.erb +9 -0
  60. data/app/views/solid_queue_monitor/search/_recurring_row.html.erb +6 -0
  61. data/app/views/solid_queue_monitor/search/_section.html.erb +25 -0
  62. data/app/views/solid_queue_monitor/search/index.html.erb +23 -0
  63. data/app/views/solid_queue_monitor/shared/_filters.html.erb +48 -0
  64. data/app/views/solid_queue_monitor/shared/_flash.html.erb +17 -0
  65. data/app/views/solid_queue_monitor/shared/_footer.html.erb +3 -0
  66. data/app/views/solid_queue_monitor/shared/_header.html.erb +81 -0
  67. data/app/views/solid_queue_monitor/shared/_jobs_table.html.erb +20 -0
  68. data/app/views/solid_queue_monitor/shared/_pagination.html.erb +25 -0
  69. data/app/views/solid_queue_monitor/workers/_row.html.erb +22 -0
  70. data/app/views/solid_queue_monitor/workers/index.html.erb +82 -0
  71. data/config/routes.rb +6 -1
  72. data/lib/solid_queue_monitor/engine.rb +2 -0
  73. data/lib/solid_queue_monitor/version.rb +1 -1
  74. metadata +57 -17
  75. data/app/presenters/solid_queue_monitor/base_presenter.rb +0 -211
  76. data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +0 -225
  77. data/app/presenters/solid_queue_monitor/in_progress_jobs_presenter.rb +0 -84
  78. data/app/presenters/solid_queue_monitor/job_details_presenter.rb +0 -707
  79. data/app/presenters/solid_queue_monitor/jobs_presenter.rb +0 -144
  80. data/app/presenters/solid_queue_monitor/queue_details_presenter.rb +0 -195
  81. data/app/presenters/solid_queue_monitor/queues_presenter.rb +0 -89
  82. data/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb +0 -81
  83. data/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb +0 -81
  84. data/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb +0 -178
  85. data/app/presenters/solid_queue_monitor/search_results_presenter.rb +0 -190
  86. data/app/presenters/solid_queue_monitor/stats_presenter.rb +0 -36
  87. data/app/presenters/solid_queue_monitor/workers_presenter.rb +0 -325
  88. data/app/services/solid_queue_monitor/chart_presenter.rb +0 -239
  89. data/app/services/solid_queue_monitor/html_generator.rb +0 -427
@@ -1,427 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SolidQueueMonitor
4
- class HtmlGenerator
5
- include Rails.application.routes.url_helpers
6
- include SolidQueueMonitor::Engine.routes.url_helpers
7
-
8
- def initialize(title:, content:, message: nil, message_type: nil, search_query: nil, nonce: nil)
9
- @title = title
10
- @content = content
11
- @message = message
12
- @message_type = message_type
13
- @search_query = search_query
14
- @nonce = nonce
15
- end
16
-
17
- def generate
18
- <<-HTML
19
- <!DOCTYPE html>
20
- <html>
21
- <head>
22
- <title>Solid Queue Monitor - #{@title}</title>
23
- #{generate_head}
24
- </head>
25
- <body class="solid_queue_monitor">
26
- #{generate_body}
27
- </body>
28
- </html>
29
- HTML
30
- end
31
-
32
- private
33
-
34
- def generate_head
35
- <<-HTML
36
- <meta charset="UTF-8">
37
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
38
- #{style_tag_open}
39
- #{SolidQueueMonitor::StylesheetGenerator.new.generate}
40
- </style>
41
- HTML
42
- end
43
-
44
- def generate_body
45
- <<-HTML
46
- #{render_message}
47
- <div class="container">
48
- #{generate_header}
49
- <div class="section">
50
- <h2>#{@title}</h2>
51
- #{@content}
52
- </div>
53
- #{generate_footer}
54
- </div>
55
- #{generate_auto_refresh_script}
56
- #{generate_chart_script}
57
- HTML
58
- end
59
-
60
- def render_message
61
- return '' unless @message
62
-
63
- class_name = @message_type == 'success' ? 'message-success' : 'message-error'
64
- <<-HTML
65
- <div id="flash-message" class="message #{class_name}">#{@message}</div>
66
- #{script_tag_open}
67
- document.addEventListener('DOMContentLoaded', function() {
68
- var el = document.getElementById('flash-message');
69
- if (!el) return;
70
- setTimeout(function() {
71
- el.classList.add('is-fading');
72
- setTimeout(function() { el.classList.add('is-hidden'); }, 500);
73
- }, 5000);
74
- });
75
- </script>
76
- HTML
77
- end
78
-
79
- def generate_header
80
- nav_items = [
81
- { path: root_path, label: 'Overview', match: 'Overview' },
82
- { path: ready_jobs_path, label: 'Ready Jobs', match: 'Ready Jobs' },
83
- { path: in_progress_jobs_path, label: 'In Progress Jobs', match: 'In Progress' },
84
- { path: scheduled_jobs_path, label: 'Scheduled Jobs', match: 'Scheduled Jobs' },
85
- { path: recurring_jobs_path, label: 'Recurring Jobs', match: 'Recurring Jobs' },
86
- { path: failed_jobs_path, label: 'Failed Jobs', match: 'Failed Jobs' },
87
- { path: queues_path, label: 'Queues', match: 'Queues' },
88
- { path: workers_path, label: 'Workers', match: 'Workers' }
89
- ]
90
-
91
- nav_links = nav_items.map do |item|
92
- active_class = @title&.include?(item[:match]) ? 'active' : ''
93
- "<a href=\"#{item[:path]}\" class=\"nav-link #{active_class}\">#{item[:label]}</a>"
94
- end.join("\n ")
95
-
96
- <<-HTML
97
- <header>
98
- <div class="header-top">
99
- <h1><a href="#{root_path}" class="header-title-link">Solid Queue Monitor</a></h1>
100
- #{generate_search_box}
101
- <div class="header-controls">
102
- #{generate_auto_refresh_controls}
103
- #{generate_theme_toggle}
104
- </div>
105
- </div>
106
- <nav class="navigation">
107
- #{nav_links}
108
- </nav>
109
- </header>
110
- HTML
111
- end
112
-
113
- def generate_footer
114
- <<-HTML
115
- <footer>
116
- <p>Powered by Solid Queue Monitor</p>
117
- </footer>
118
- HTML
119
- end
120
-
121
- def generate_search_box
122
- search_value = @search_query ? escape_html(@search_query) : ''
123
- <<-HTML
124
- <form method="get" action="#{search_path}" class="header-search-form">
125
- <input type="text" name="q" value="#{search_value}" placeholder="Search by class, queue, job ID, or error..." class="header-search-input">
126
- <button type="submit" class="header-search-button" title="Search">
127
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
128
- <circle cx="11" cy="11" r="8"></circle>
129
- <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
130
- </svg>
131
- </button>
132
- </form>
133
- HTML
134
- end
135
-
136
- def escape_html(text)
137
- text.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;').gsub('"', '&quot;')
138
- end
139
-
140
- def style_tag_open
141
- @nonce ? %(<style nonce="#{@nonce}">) : '<style>'
142
- end
143
-
144
- def script_tag_open
145
- @nonce ? %(<script nonce="#{@nonce}">) : '<script>'
146
- end
147
-
148
- def generate_auto_refresh_controls
149
- return '' unless SolidQueueMonitor.auto_refresh_enabled
150
-
151
- interval = SolidQueueMonitor.auto_refresh_interval
152
- <<-HTML
153
- <div class="auto-refresh-container" title="Auto-refresh every #{interval}s" data-tooltip="Auto-refresh: Dashboard updates automatically every #{interval} seconds. Toggle to enable/disable.">
154
- <span class="auto-refresh-indicator" id="auto-refresh-indicator"></span>
155
- <span class="auto-refresh-countdown" id="auto-refresh-countdown">#{interval}s</span>
156
- <label class="auto-refresh-switch" title="Toggle auto-refresh">
157
- <input type="checkbox" id="auto-refresh-toggle" checked>
158
- <span class="switch-slider"></span>
159
- </label>
160
- <button class="refresh-now-btn" id="refresh-now-btn" title="Refresh now">
161
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
162
- <path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16"/>
163
- </svg>
164
- </button>
165
- </div>
166
- HTML
167
- end
168
-
169
- def generate_theme_toggle
170
- <<-HTML
171
- <button class="theme-toggle-btn" id="theme-toggle-btn" title="Toggle dark mode">
172
- <svg class="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
173
- <circle cx="12" cy="12" r="5"></circle>
174
- <line x1="12" y1="1" x2="12" y2="3"></line>
175
- <line x1="12" y1="21" x2="12" y2="23"></line>
176
- <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
177
- <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
178
- <line x1="1" y1="12" x2="3" y2="12"></line>
179
- <line x1="21" y1="12" x2="23" y2="12"></line>
180
- <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
181
- <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
182
- </svg>
183
- <svg class="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
184
- <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
185
- </svg>
186
- </button>
187
- HTML
188
- end
189
-
190
- def generate_auto_refresh_script
191
- return '' unless SolidQueueMonitor.auto_refresh_enabled
192
-
193
- "#{script_tag_open}#{auto_refresh_javascript}</script>"
194
- end
195
-
196
- def auto_refresh_javascript
197
- interval = SolidQueueMonitor.auto_refresh_interval
198
- <<-JS
199
- (function() {
200
- var REFRESH_INTERVAL = #{interval};
201
- var countdown = REFRESH_INTERVAL;
202
- var timerId = null;
203
- var isEnabled = localStorage.getItem('sqm_auto_refresh') !== 'false';
204
- #{auto_refresh_dom_elements}
205
- #{auto_refresh_functions}
206
- #{auto_refresh_event_listeners}
207
- #{auto_refresh_init}
208
- })();
209
- JS
210
- end
211
-
212
- def auto_refresh_dom_elements
213
- <<-JS
214
- var toggle = document.getElementById('auto-refresh-toggle');
215
- var indicator = document.getElementById('auto-refresh-indicator');
216
- var countdownEl = document.getElementById('auto-refresh-countdown');
217
- var refreshBtn = document.getElementById('refresh-now-btn');
218
- JS
219
- end
220
-
221
- def auto_refresh_functions
222
- <<-JS
223
- function updateUI() {
224
- if (toggle) toggle.checked = isEnabled;
225
- if (indicator) indicator.classList.toggle('active', isEnabled);
226
- if (countdownEl) {
227
- countdownEl.textContent = countdown + 's';
228
- countdownEl.classList.toggle('countdown-paused', !isEnabled);
229
- }
230
- }
231
- function tick() {
232
- countdown--;
233
- if (countdown <= 0) { refresh(); } else { updateUI(); }
234
- }
235
- function startTimer() {
236
- stopTimer();
237
- countdown = REFRESH_INTERVAL;
238
- updateUI();
239
- timerId = setInterval(tick, 1000);
240
- }
241
- function stopTimer() {
242
- if (timerId) { clearInterval(timerId); timerId = null; }
243
- }
244
- function refresh() { window.location.reload(); }
245
- function setEnabled(enabled) {
246
- isEnabled = enabled;
247
- localStorage.setItem('sqm_auto_refresh', enabled ? 'true' : 'false');
248
- if (enabled) { startTimer(); } else { stopTimer(); countdown = REFRESH_INTERVAL; updateUI(); }
249
- }
250
- JS
251
- end
252
-
253
- def auto_refresh_event_listeners
254
- <<-JS
255
- if (toggle) { toggle.addEventListener('change', function() { setEnabled(this.checked); }); }
256
- if (refreshBtn) { refreshBtn.addEventListener('click', function() { refresh(); }); }
257
- JS
258
- end
259
-
260
- def auto_refresh_init
261
- <<-JS
262
- updateUI();
263
- if (isEnabled) { startTimer(); }
264
- JS
265
- end
266
-
267
- def generate_chart_script
268
- <<-HTML
269
- #{script_tag_open}
270
- #{theme_toggle_javascript}
271
- #{chart_tooltip_javascript}
272
- #{global_behaviors_javascript}
273
- </script>
274
- HTML
275
- end
276
-
277
- def theme_toggle_javascript
278
- <<-JS
279
- (function() {
280
- var body = document.body;
281
- var themeBtn = document.getElementById('theme-toggle-btn');
282
- var storageKey = 'sqm_dark_theme';
283
-
284
- // Check for saved preference or system preference
285
- function getPreferredTheme() {
286
- var saved = localStorage.getItem(storageKey);
287
- if (saved !== null) {
288
- return saved === 'true';
289
- }
290
- // Check system preference
291
- return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
292
- }
293
-
294
- function setTheme(isDark) {
295
- if (isDark) {
296
- body.classList.add('dark-theme');
297
- } else {
298
- body.classList.remove('dark-theme');
299
- }
300
- localStorage.setItem(storageKey, isDark ? 'true' : 'false');
301
- }
302
-
303
- // Initialize theme
304
- setTheme(getPreferredTheme());
305
-
306
- // Toggle on button click
307
- if (themeBtn) {
308
- themeBtn.addEventListener('click', function() {
309
- var isDark = body.classList.contains('dark-theme');
310
- setTheme(!isDark);
311
- });
312
- }
313
-
314
- // Listen for system preference changes
315
- if (window.matchMedia) {
316
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
317
- // Only auto-switch if user hasn't manually set a preference
318
- if (localStorage.getItem(storageKey) === null) {
319
- setTheme(e.matches);
320
- }
321
- });
322
- }
323
- })();
324
- JS
325
- end
326
-
327
- def chart_tooltip_javascript
328
- <<-JS
329
- (function() {
330
- // Chart collapse/expand functionality
331
- var chartSection = document.getElementById('chart-section');
332
- var toggleBtn = document.getElementById('chart-toggle-btn');
333
-
334
- if (chartSection && toggleBtn) {
335
- var isCollapsed = localStorage.getItem('sqm_chart_collapsed') === 'true';
336
-
337
- if (isCollapsed) {
338
- chartSection.classList.add('collapsed');
339
- }
340
-
341
- toggleBtn.addEventListener('click', function() {
342
- chartSection.classList.toggle('collapsed');
343
- var collapsed = chartSection.classList.contains('collapsed');
344
- localStorage.setItem('sqm_chart_collapsed', collapsed ? 'true' : 'false');
345
- });
346
- }
347
-
348
- // Chart tooltip functionality
349
- var tooltip = document.getElementById('chart-tooltip');
350
- if (!tooltip) return;
351
-
352
- var dataPoints = document.querySelectorAll('.data-point');
353
- var seriesNames = { created: 'Created', completed: 'Completed', failed: 'Failed' };
354
-
355
- dataPoints.forEach(function(point) {
356
- point.addEventListener('mouseenter', function(e) {
357
- var series = this.getAttribute('data-series');
358
- var label = this.getAttribute('data-label');
359
- var value = this.getAttribute('data-value');
360
-
361
- tooltip.querySelector('.tooltip-label').textContent = label;
362
- tooltip.querySelector('.tooltip-value').textContent = seriesNames[series] + ': ' + value;
363
- tooltip.classList.add('tooltip-visible');
364
- positionTooltip(e);
365
- });
366
-
367
- point.addEventListener('mousemove', function(e) {
368
- positionTooltip(e);
369
- });
370
-
371
- point.addEventListener('mouseleave', function() {
372
- tooltip.classList.remove('tooltip-visible');
373
- });
374
- });
375
-
376
- function positionTooltip(e) {
377
- var x = e.clientX + 10;
378
- var y = e.clientY - 30;
379
-
380
- if (x + tooltip.offsetWidth > window.innerWidth) {
381
- x = e.clientX - tooltip.offsetWidth - 10;
382
- }
383
- if (y < 0) {
384
- y = e.clientY + 10;
385
- }
386
-
387
- // Dynamic cursor-tracked position, not CSP-restricted.
388
- tooltip.style.left = x + 'px';
389
- tooltip.style.top = y + 'px';
390
- }
391
- })();
392
- JS
393
- end
394
-
395
- def global_behaviors_javascript
396
- <<-JS
397
- document.addEventListener('submit', function(e) {
398
- var form = e.target;
399
- var msg = form.dataset && form.dataset.confirm;
400
- if (msg && !window.confirm(msg)) { e.preventDefault(); }
401
- }, true);
402
-
403
- document.addEventListener('click', function(e) {
404
- var el = e.target.closest('[data-confirm-submit]');
405
- if (!el) return;
406
- e.preventDefault();
407
- var msg = el.dataset.confirm || 'Are you sure?';
408
- if (!window.confirm(msg)) return;
409
- var formId = el.dataset.confirmSubmit;
410
- var form = document.getElementById(formId);
411
- if (form) form.submit();
412
- });
413
-
414
- var timeRangeSelect = document.getElementById('chart-time-select');
415
- if (timeRangeSelect) {
416
- timeRangeSelect.addEventListener('change', function() {
417
- window.location.href = '?time_range=' + this.value;
418
- });
419
- }
420
- JS
421
- end
422
-
423
- def default_url_options
424
- { only_path: true }
425
- end
426
- end
427
- end