solid_queue_monitor 0.3.2 → 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 +4 -4
- data/README.md +11 -2
- data/app/controllers/solid_queue_monitor/application_controller.rb +11 -2
- data/app/controllers/solid_queue_monitor/base_controller.rb +15 -7
- data/app/controllers/solid_queue_monitor/queues_controller.rb +18 -1
- data/app/presenters/solid_queue_monitor/queues_presenter.rb +45 -6
- data/app/services/solid_queue_monitor/html_generator.rb +103 -1
- data/app/services/solid_queue_monitor/queue_pause_service.rb +34 -0
- data/app/services/solid_queue_monitor/stylesheet_generator.rb +201 -0
- data/config/database.yml +3 -0
- data/config/initializers/solid_queue_monitor.rb +2 -0
- data/config/routes.rb +7 -1
- data/lib/generators/solid_queue_monitor/templates/initializer.rb +7 -0
- data/lib/solid_queue_monitor/engine.rb +5 -0
- data/lib/solid_queue_monitor/version.rb +1 -1
- data/lib/solid_queue_monitor.rb +4 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2597291a879f3f6720e783c6aae3fa203434a67652ba24c5e65a6fdd87081120
|
|
4
|
+
data.tar.gz: d01e77ac3c412bf2fcdab1c4f1a02aa072786f7d88ec42798758615159ddc67c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9f95324749998dd5c7a6e0598a25edd14f82bbc32610499dcf688592729f4cbf805bf3ac8da8ad3f776c0dc3231ebaa6fa710fac5537806553cc3cc50f3a7d66
|
|
7
|
+
data.tar.gz: be751aaa21b617d1ba1f09eb473a9bbd83eeccfbe79c918040fe2df420c76258b012fad5c26eefa1aaaafe50e62fdf6d566b08469042006a501d9f354cd49658
|
data/README.md
CHANGED
|
@@ -21,10 +21,12 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
|
|
|
21
21
|
- **Scheduled Jobs**: See upcoming jobs scheduled for future execution with ability to execute immediately or reject permanently
|
|
22
22
|
- **Recurring Jobs**: Manage periodic jobs that run on a schedule
|
|
23
23
|
- **Failed Jobs**: Track and debug failed jobs, with the ability to retry or discard them
|
|
24
|
-
- **Queue Management**: View and filter jobs by queue
|
|
24
|
+
- **Queue Management**: View and filter jobs by queue with pause/resume controls
|
|
25
|
+
- **Pause/Resume Queues**: Temporarily stop processing jobs on specific queues for incident response
|
|
25
26
|
- **Advanced Job Filtering**: Filter jobs by class name, queue, status, and job arguments
|
|
26
27
|
- **Quick Actions**: Retry or discard failed jobs, execute or reject scheduled jobs directly from any view
|
|
27
28
|
- **Performance Optimized**: Designed for high-volume applications with smart pagination
|
|
29
|
+
- **Auto-refresh**: Real-time monitoring with configurable auto-refresh interval and toggle
|
|
28
30
|
- **Optional Authentication**: Secure your dashboard with HTTP Basic Authentication
|
|
29
31
|
- **Responsive Design**: Works on desktop and mobile devices
|
|
30
32
|
- **Zero Dependencies**: No additional JavaScript libraries or frameworks required
|
|
@@ -44,7 +46,7 @@ A lightweight, zero-dependency web interface for monitoring Solid Queue backgrou
|
|
|
44
46
|
Add this line to your application's Gemfile:
|
|
45
47
|
|
|
46
48
|
```ruby
|
|
47
|
-
gem 'solid_queue_monitor', '~> 0.
|
|
49
|
+
gem 'solid_queue_monitor', '~> 0.4.0'
|
|
48
50
|
```
|
|
49
51
|
|
|
50
52
|
Then execute:
|
|
@@ -83,6 +85,13 @@ SolidQueueMonitor.setup do |config|
|
|
|
83
85
|
|
|
84
86
|
# Number of jobs to display per page
|
|
85
87
|
config.jobs_per_page = 25
|
|
88
|
+
|
|
89
|
+
# Auto-refresh settings
|
|
90
|
+
# Enable or disable auto-refresh globally (users can still toggle it in the UI)
|
|
91
|
+
config.auto_refresh_enabled = true
|
|
92
|
+
|
|
93
|
+
# Auto-refresh interval in seconds (default: 30)
|
|
94
|
+
config.auto_refresh_interval = 30
|
|
86
95
|
end
|
|
87
96
|
```
|
|
88
97
|
|
|
@@ -10,8 +10,17 @@ module SolidQueueMonitor
|
|
|
10
10
|
skip_before_action :verify_authenticity_token
|
|
11
11
|
|
|
12
12
|
def set_flash_message(message, type)
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
# Store in instance variable for access in views
|
|
14
|
+
@flash_message = message
|
|
15
|
+
@flash_type = type
|
|
16
|
+
|
|
17
|
+
# Try to use Rails flash if available
|
|
18
|
+
begin
|
|
19
|
+
flash[:notice] = message if type == :success
|
|
20
|
+
flash[:alert] = message if type == :error
|
|
21
|
+
rescue StandardError
|
|
22
|
+
# Flash not available (e.g., no session middleware)
|
|
23
|
+
end
|
|
15
24
|
end
|
|
16
25
|
|
|
17
26
|
private
|
|
@@ -7,13 +7,21 @@ module SolidQueueMonitor
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def render_page(title, content)
|
|
10
|
-
# Get flash message from session
|
|
11
|
-
message =
|
|
12
|
-
message_type =
|
|
13
|
-
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
# Get flash message from instance variable (set by set_flash_message) or session
|
|
11
|
+
message = @flash_message
|
|
12
|
+
message_type = @flash_type
|
|
13
|
+
|
|
14
|
+
# Try to get from session as fallback, but don't fail if session unavailable
|
|
15
|
+
begin
|
|
16
|
+
message ||= session[:flash_message]
|
|
17
|
+
message_type ||= session[:flash_type]
|
|
18
|
+
|
|
19
|
+
# Clear the flash message from session after using it
|
|
20
|
+
session.delete(:flash_message) if message
|
|
21
|
+
session.delete(:flash_type) if message_type
|
|
22
|
+
rescue StandardError
|
|
23
|
+
# Session not available (e.g., no session middleware in tests)
|
|
24
|
+
end
|
|
17
25
|
|
|
18
26
|
html = SolidQueueMonitor::HtmlGenerator.new(
|
|
19
27
|
title: title,
|
|
@@ -6,8 +6,25 @@ module SolidQueueMonitor
|
|
|
6
6
|
@queues = SolidQueue::Job.group(:queue_name)
|
|
7
7
|
.select('queue_name, COUNT(*) as job_count')
|
|
8
8
|
.order('job_count DESC')
|
|
9
|
+
@paused_queues = QueuePauseService.paused_queues
|
|
9
10
|
|
|
10
|
-
render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(@queues).render)
|
|
11
|
+
render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(@queues, @paused_queues).render)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def pause
|
|
15
|
+
queue_name = params[:queue_name]
|
|
16
|
+
result = QueuePauseService.new(queue_name).pause
|
|
17
|
+
|
|
18
|
+
set_flash_message(result[:message], result[:success] ? 'success' : 'error')
|
|
19
|
+
redirect_to queues_path
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def resume
|
|
23
|
+
queue_name = params[:queue_name]
|
|
24
|
+
result = QueuePauseService.new(queue_name).resume
|
|
25
|
+
|
|
26
|
+
set_flash_message(result[:message], result[:success] ? 'success' : 'error')
|
|
27
|
+
redirect_to queues_path
|
|
11
28
|
end
|
|
12
29
|
end
|
|
13
30
|
end
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueMonitor
|
|
4
4
|
class QueuesPresenter < BasePresenter
|
|
5
|
-
def initialize(records)
|
|
5
|
+
def initialize(records, paused_queues = [])
|
|
6
6
|
@records = records
|
|
7
|
+
@paused_queues = paused_queues
|
|
7
8
|
end
|
|
8
9
|
|
|
9
10
|
def render
|
|
@@ -19,10 +20,12 @@ module SolidQueueMonitor
|
|
|
19
20
|
<thead>
|
|
20
21
|
<tr>
|
|
21
22
|
<th>Queue Name</th>
|
|
23
|
+
<th>Status</th>
|
|
22
24
|
<th>Total Jobs</th>
|
|
23
25
|
<th>Ready Jobs</th>
|
|
24
26
|
<th>Scheduled Jobs</th>
|
|
25
27
|
<th>Failed Jobs</th>
|
|
28
|
+
<th>Actions</th>
|
|
26
29
|
</tr>
|
|
27
30
|
</thead>
|
|
28
31
|
<tbody>
|
|
@@ -34,17 +37,53 @@ module SolidQueueMonitor
|
|
|
34
37
|
end
|
|
35
38
|
|
|
36
39
|
def generate_row(queue)
|
|
40
|
+
queue_name = queue.queue_name || 'default'
|
|
41
|
+
paused = @paused_queues.include?(queue_name)
|
|
42
|
+
|
|
37
43
|
<<-HTML
|
|
38
|
-
<tr>
|
|
39
|
-
<td>#{
|
|
44
|
+
<tr class="#{paused ? 'queue-paused' : ''}">
|
|
45
|
+
<td>#{queue_name}</td>
|
|
46
|
+
<td>#{status_badge(paused)}</td>
|
|
40
47
|
<td>#{queue.job_count}</td>
|
|
41
|
-
<td>#{ready_jobs_count(
|
|
42
|
-
<td>#{scheduled_jobs_count(
|
|
43
|
-
<td>#{failed_jobs_count(
|
|
48
|
+
<td>#{ready_jobs_count(queue_name)}</td>
|
|
49
|
+
<td>#{scheduled_jobs_count(queue_name)}</td>
|
|
50
|
+
<td>#{failed_jobs_count(queue_name)}</td>
|
|
51
|
+
<td class="actions-cell">#{action_button(queue_name, paused)}</td>
|
|
44
52
|
</tr>
|
|
45
53
|
HTML
|
|
46
54
|
end
|
|
47
55
|
|
|
56
|
+
def status_badge(paused)
|
|
57
|
+
if paused
|
|
58
|
+
'<span class="status-badge status-paused">Paused</span>'
|
|
59
|
+
else
|
|
60
|
+
'<span class="status-badge status-active">Active</span>'
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def action_button(queue_name, paused)
|
|
65
|
+
if paused
|
|
66
|
+
<<-HTML
|
|
67
|
+
<form action="#{resume_queue_path}" method="post" class="inline-form">
|
|
68
|
+
<input type="hidden" name="queue_name" value="#{queue_name}">
|
|
69
|
+
<button type="submit" class="action-button resume-button" title="Resume queue processing">
|
|
70
|
+
Resume
|
|
71
|
+
</button>
|
|
72
|
+
</form>
|
|
73
|
+
HTML
|
|
74
|
+
else
|
|
75
|
+
<<-HTML
|
|
76
|
+
<form action="#{pause_queue_path}" method="post" class="inline-form"
|
|
77
|
+
onsubmit="return confirm('Are you sure you want to pause the #{queue_name} queue? Workers will stop processing jobs from this queue.');">
|
|
78
|
+
<input type="hidden" name="queue_name" value="#{queue_name}">
|
|
79
|
+
<button type="submit" class="action-button pause-button" title="Pause queue processing">
|
|
80
|
+
Pause
|
|
81
|
+
</button>
|
|
82
|
+
</form>
|
|
83
|
+
HTML
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
48
87
|
def ready_jobs_count(queue_name)
|
|
49
88
|
SolidQueue::ReadyExecution.where(queue_name: queue_name).count
|
|
50
89
|
end
|
|
@@ -50,6 +50,7 @@ module SolidQueueMonitor
|
|
|
50
50
|
</div>
|
|
51
51
|
#{generate_footer}
|
|
52
52
|
</div>
|
|
53
|
+
#{generate_auto_refresh_script}
|
|
53
54
|
HTML
|
|
54
55
|
end
|
|
55
56
|
|
|
@@ -88,7 +89,10 @@ module SolidQueueMonitor
|
|
|
88
89
|
def generate_header
|
|
89
90
|
<<-HTML
|
|
90
91
|
<header>
|
|
91
|
-
<
|
|
92
|
+
<div class="header-top">
|
|
93
|
+
<h1>Solid Queue Monitor</h1>
|
|
94
|
+
#{generate_auto_refresh_controls}
|
|
95
|
+
</div>
|
|
92
96
|
<nav class="navigation">
|
|
93
97
|
<a href="#{root_path}" class="nav-link">Overview</a>
|
|
94
98
|
<a href="#{ready_jobs_path}" class="nav-link">Ready Jobs</a>
|
|
@@ -110,6 +114,104 @@ module SolidQueueMonitor
|
|
|
110
114
|
HTML
|
|
111
115
|
end
|
|
112
116
|
|
|
117
|
+
def generate_auto_refresh_controls
|
|
118
|
+
return '' unless SolidQueueMonitor.auto_refresh_enabled
|
|
119
|
+
|
|
120
|
+
interval = SolidQueueMonitor.auto_refresh_interval
|
|
121
|
+
<<-HTML
|
|
122
|
+
<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.">
|
|
123
|
+
<span class="auto-refresh-indicator" id="auto-refresh-indicator"></span>
|
|
124
|
+
<span class="auto-refresh-countdown" id="auto-refresh-countdown">#{interval}s</span>
|
|
125
|
+
<label class="auto-refresh-switch" title="Toggle auto-refresh">
|
|
126
|
+
<input type="checkbox" id="auto-refresh-toggle" checked>
|
|
127
|
+
<span class="switch-slider"></span>
|
|
128
|
+
</label>
|
|
129
|
+
<button class="refresh-now-btn" id="refresh-now-btn" title="Refresh now">
|
|
130
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
131
|
+
<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"/>
|
|
132
|
+
</svg>
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
HTML
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def generate_auto_refresh_script
|
|
139
|
+
return '' unless SolidQueueMonitor.auto_refresh_enabled
|
|
140
|
+
|
|
141
|
+
"<script>#{auto_refresh_javascript}</script>"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def auto_refresh_javascript
|
|
145
|
+
interval = SolidQueueMonitor.auto_refresh_interval
|
|
146
|
+
<<-JS
|
|
147
|
+
(function() {
|
|
148
|
+
var REFRESH_INTERVAL = #{interval};
|
|
149
|
+
var countdown = REFRESH_INTERVAL;
|
|
150
|
+
var timerId = null;
|
|
151
|
+
var isEnabled = localStorage.getItem('sqm_auto_refresh') !== 'false';
|
|
152
|
+
#{auto_refresh_dom_elements}
|
|
153
|
+
#{auto_refresh_functions}
|
|
154
|
+
#{auto_refresh_event_listeners}
|
|
155
|
+
#{auto_refresh_init}
|
|
156
|
+
})();
|
|
157
|
+
JS
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def auto_refresh_dom_elements
|
|
161
|
+
<<-JS
|
|
162
|
+
var toggle = document.getElementById('auto-refresh-toggle');
|
|
163
|
+
var indicator = document.getElementById('auto-refresh-indicator');
|
|
164
|
+
var countdownEl = document.getElementById('auto-refresh-countdown');
|
|
165
|
+
var refreshBtn = document.getElementById('refresh-now-btn');
|
|
166
|
+
JS
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def auto_refresh_functions
|
|
170
|
+
<<-JS
|
|
171
|
+
function updateUI() {
|
|
172
|
+
if (toggle) toggle.checked = isEnabled;
|
|
173
|
+
if (indicator) indicator.classList.toggle('active', isEnabled);
|
|
174
|
+
if (countdownEl) {
|
|
175
|
+
countdownEl.textContent = countdown + 's';
|
|
176
|
+
countdownEl.style.opacity = isEnabled ? '1' : '0.4';
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function tick() {
|
|
180
|
+
countdown--;
|
|
181
|
+
if (countdown <= 0) { refresh(); } else { updateUI(); }
|
|
182
|
+
}
|
|
183
|
+
function startTimer() {
|
|
184
|
+
stopTimer();
|
|
185
|
+
countdown = REFRESH_INTERVAL;
|
|
186
|
+
updateUI();
|
|
187
|
+
timerId = setInterval(tick, 1000);
|
|
188
|
+
}
|
|
189
|
+
function stopTimer() {
|
|
190
|
+
if (timerId) { clearInterval(timerId); timerId = null; }
|
|
191
|
+
}
|
|
192
|
+
function refresh() { window.location.reload(); }
|
|
193
|
+
function setEnabled(enabled) {
|
|
194
|
+
isEnabled = enabled;
|
|
195
|
+
localStorage.setItem('sqm_auto_refresh', enabled ? 'true' : 'false');
|
|
196
|
+
if (enabled) { startTimer(); } else { stopTimer(); countdown = REFRESH_INTERVAL; updateUI(); }
|
|
197
|
+
}
|
|
198
|
+
JS
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def auto_refresh_event_listeners
|
|
202
|
+
<<-JS
|
|
203
|
+
if (toggle) { toggle.addEventListener('change', function() { setEnabled(this.checked); }); }
|
|
204
|
+
if (refreshBtn) { refreshBtn.addEventListener('click', function() { refresh(); }); }
|
|
205
|
+
JS
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def auto_refresh_init
|
|
209
|
+
<<-JS
|
|
210
|
+
updateUI();
|
|
211
|
+
if (isEnabled) { startTimer(); }
|
|
212
|
+
JS
|
|
213
|
+
end
|
|
214
|
+
|
|
113
215
|
def default_url_options
|
|
114
216
|
{ only_path: true }
|
|
115
217
|
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
|
|
@@ -182,6 +182,30 @@ module SolidQueueMonitor
|
|
|
182
182
|
.solid_queue_monitor .status-failed { background: #fee2e2; color: #991b1b; }
|
|
183
183
|
.solid_queue_monitor .status-scheduled { background: #dbeafe; color: #1e40af; }
|
|
184
184
|
.solid_queue_monitor .status-pending { background: #f3f4f6; color: #374151; }
|
|
185
|
+
.solid_queue_monitor .status-active { background: #d1fae5; color: #065f46; }
|
|
186
|
+
.solid_queue_monitor .status-paused { background: #fef3c7; color: #92400e; }
|
|
187
|
+
|
|
188
|
+
.solid_queue_monitor .queue-paused {
|
|
189
|
+
background-color: #fffbeb;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.solid_queue_monitor .pause-button {
|
|
193
|
+
background: #f59e0b;
|
|
194
|
+
color: white;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.solid_queue_monitor .pause-button:hover {
|
|
198
|
+
background: #d97706;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.solid_queue_monitor .resume-button {
|
|
202
|
+
background: #10b981;
|
|
203
|
+
color: white;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.solid_queue_monitor .resume-button:hover {
|
|
207
|
+
background: #059669;
|
|
208
|
+
}
|
|
185
209
|
|
|
186
210
|
.solid_queue_monitor .execute-btn {
|
|
187
211
|
background: var(--primary-color);
|
|
@@ -585,6 +609,183 @@ module SolidQueueMonitor
|
|
|
585
609
|
.solid_queue_monitor .execute-button:hover {
|
|
586
610
|
background: #2563eb;
|
|
587
611
|
}
|
|
612
|
+
|
|
613
|
+
/* Header top row with title and auto-refresh */
|
|
614
|
+
.solid_queue_monitor .header-top {
|
|
615
|
+
display: flex;
|
|
616
|
+
justify-content: space-between;
|
|
617
|
+
align-items: center;
|
|
618
|
+
margin-bottom: 0.5rem;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* Auto-refresh styles - compact design */
|
|
622
|
+
.solid_queue_monitor .auto-refresh-container {
|
|
623
|
+
position: relative;
|
|
624
|
+
display: flex;
|
|
625
|
+
align-items: center;
|
|
626
|
+
gap: 0.5rem;
|
|
627
|
+
padding: 0.375rem 0.625rem;
|
|
628
|
+
background: white;
|
|
629
|
+
border-radius: 2rem;
|
|
630
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
|
631
|
+
font-size: 0.75rem;
|
|
632
|
+
color: #6b7280;
|
|
633
|
+
cursor: default;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/* Tooltip styles */
|
|
637
|
+
.solid_queue_monitor .auto-refresh-container::after {
|
|
638
|
+
content: attr(data-tooltip);
|
|
639
|
+
position: absolute;
|
|
640
|
+
top: calc(100% + 8px);
|
|
641
|
+
right: 0;
|
|
642
|
+
background: #1f2937;
|
|
643
|
+
color: white;
|
|
644
|
+
padding: 0.5rem 0.75rem;
|
|
645
|
+
border-radius: 0.375rem;
|
|
646
|
+
font-size: 0.75rem;
|
|
647
|
+
line-height: 1.4;
|
|
648
|
+
white-space: nowrap;
|
|
649
|
+
max-width: 280px;
|
|
650
|
+
white-space: normal;
|
|
651
|
+
text-align: left;
|
|
652
|
+
opacity: 0;
|
|
653
|
+
visibility: hidden;
|
|
654
|
+
transition: opacity 0.2s, visibility 0.2s;
|
|
655
|
+
z-index: 1000;
|
|
656
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
657
|
+
pointer-events: none;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/* Tooltip arrow */
|
|
661
|
+
.solid_queue_monitor .auto-refresh-container::before {
|
|
662
|
+
content: "";
|
|
663
|
+
position: absolute;
|
|
664
|
+
top: calc(100% + 2px);
|
|
665
|
+
right: 16px;
|
|
666
|
+
border: 6px solid transparent;
|
|
667
|
+
border-bottom-color: #1f2937;
|
|
668
|
+
opacity: 0;
|
|
669
|
+
visibility: hidden;
|
|
670
|
+
transition: opacity 0.2s, visibility 0.2s;
|
|
671
|
+
z-index: 1001;
|
|
672
|
+
pointer-events: none;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.solid_queue_monitor .auto-refresh-container:hover::after,
|
|
676
|
+
.solid_queue_monitor .auto-refresh-container:hover::before {
|
|
677
|
+
opacity: 1;
|
|
678
|
+
visibility: visible;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.solid_queue_monitor .auto-refresh-indicator {
|
|
682
|
+
width: 6px;
|
|
683
|
+
height: 6px;
|
|
684
|
+
border-radius: 50%;
|
|
685
|
+
background: #d1d5db;
|
|
686
|
+
flex-shrink: 0;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.solid_queue_monitor .auto-refresh-indicator.active {
|
|
690
|
+
background: var(--success-color);
|
|
691
|
+
animation: pulse 2s infinite;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
@keyframes pulse {
|
|
695
|
+
0%, 100% { opacity: 1; }
|
|
696
|
+
50% { opacity: 0.5; }
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.solid_queue_monitor .auto-refresh-countdown {
|
|
700
|
+
font-variant-numeric: tabular-nums;
|
|
701
|
+
font-weight: 500;
|
|
702
|
+
min-width: 1.75rem;
|
|
703
|
+
color: var(--text-color);
|
|
704
|
+
transition: opacity 0.2s;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/* Toggle switch */
|
|
708
|
+
.solid_queue_monitor .auto-refresh-switch {
|
|
709
|
+
position: relative;
|
|
710
|
+
display: inline-block;
|
|
711
|
+
width: 32px;
|
|
712
|
+
height: 18px;
|
|
713
|
+
flex-shrink: 0;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.solid_queue_monitor .auto-refresh-switch input {
|
|
717
|
+
opacity: 0;
|
|
718
|
+
width: 0;
|
|
719
|
+
height: 0;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.solid_queue_monitor .switch-slider {
|
|
723
|
+
position: absolute;
|
|
724
|
+
cursor: pointer;
|
|
725
|
+
top: 0;
|
|
726
|
+
left: 0;
|
|
727
|
+
right: 0;
|
|
728
|
+
bottom: 0;
|
|
729
|
+
background-color: #d1d5db;
|
|
730
|
+
transition: 0.2s;
|
|
731
|
+
border-radius: 18px;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.solid_queue_monitor .switch-slider:before {
|
|
735
|
+
position: absolute;
|
|
736
|
+
content: "";
|
|
737
|
+
height: 14px;
|
|
738
|
+
width: 14px;
|
|
739
|
+
left: 2px;
|
|
740
|
+
bottom: 2px;
|
|
741
|
+
background-color: white;
|
|
742
|
+
transition: 0.2s;
|
|
743
|
+
border-radius: 50%;
|
|
744
|
+
box-shadow: 0 1px 2px rgba(0,0,0,0.2);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.solid_queue_monitor .auto-refresh-switch input:checked + .switch-slider {
|
|
748
|
+
background-color: var(--success-color);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.solid_queue_monitor .auto-refresh-switch input:checked + .switch-slider:before {
|
|
752
|
+
transform: translateX(14px);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.solid_queue_monitor .refresh-now-btn {
|
|
756
|
+
display: flex;
|
|
757
|
+
align-items: center;
|
|
758
|
+
justify-content: center;
|
|
759
|
+
background: transparent;
|
|
760
|
+
border: none;
|
|
761
|
+
padding: 0.25rem;
|
|
762
|
+
border-radius: 0.25rem;
|
|
763
|
+
cursor: pointer;
|
|
764
|
+
color: #9ca3af;
|
|
765
|
+
transition: all 0.2s;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.solid_queue_monitor .refresh-now-btn:hover {
|
|
769
|
+
color: var(--primary-color);
|
|
770
|
+
background: rgba(59, 130, 246, 0.1);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
@media (max-width: 768px) {
|
|
774
|
+
.solid_queue_monitor .header-top {
|
|
775
|
+
flex-direction: column;
|
|
776
|
+
gap: 0.75rem;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
.solid_queue_monitor .auto-refresh-container {
|
|
780
|
+
align-self: center;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/* Hide tooltip on mobile - use native title instead */
|
|
784
|
+
.solid_queue_monitor .auto-refresh-container::after,
|
|
785
|
+
.solid_queue_monitor .auto-refresh-container::before {
|
|
786
|
+
display: none;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
588
789
|
CSS
|
|
589
790
|
end
|
|
590
791
|
end
|
data/config/database.yml
ADDED
|
@@ -4,4 +4,6 @@ SolidQueueMonitor.setup do |config|
|
|
|
4
4
|
config.username = 'admin' # Change this in your application
|
|
5
5
|
config.password = 'password' # Change this in your application
|
|
6
6
|
config.jobs_per_page = 25
|
|
7
|
+
config.auto_refresh_enabled = true # Enable/disable auto-refresh globally
|
|
8
|
+
config.auto_refresh_interval = 30 # Auto-refresh interval in seconds
|
|
7
9
|
end
|
data/config/routes.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Guard against multiple loads of routes file in test environment
|
|
3
4
|
SolidQueueMonitor::Engine.routes.draw do
|
|
4
|
-
|
|
5
|
+
return if SolidQueueMonitor::Engine.routes.routes.any? { |r| r.name == 'root' }
|
|
6
|
+
|
|
7
|
+
root to: 'overview#index'
|
|
5
8
|
|
|
6
9
|
resources :ready_jobs, only: [:index]
|
|
7
10
|
resources :scheduled_jobs, only: [:index]
|
|
@@ -17,4 +20,7 @@ SolidQueueMonitor::Engine.routes.draw do
|
|
|
17
20
|
post 'discard_failed_job/:id', to: 'failed_jobs#discard', as: :discard_failed_job
|
|
18
21
|
post 'retry_failed_jobs', to: 'failed_jobs#retry_all', as: :retry_failed_jobs
|
|
19
22
|
post 'discard_failed_jobs', to: 'failed_jobs#discard_all', as: :discard_failed_jobs
|
|
23
|
+
|
|
24
|
+
post 'pause_queue', to: 'queues#pause', as: :pause_queue
|
|
25
|
+
post 'resume_queue', to: 'queues#resume', as: :resume_queue
|
|
20
26
|
end
|
|
@@ -13,4 +13,11 @@ SolidQueueMonitor.setup do |config|
|
|
|
13
13
|
|
|
14
14
|
# Number of jobs to display per page
|
|
15
15
|
# config.jobs_per_page = 25
|
|
16
|
+
|
|
17
|
+
# Auto-refresh settings
|
|
18
|
+
# Enable or disable auto-refresh globally (users can still toggle it in the UI)
|
|
19
|
+
# config.auto_refresh_enabled = true
|
|
20
|
+
|
|
21
|
+
# Auto-refresh interval in seconds (default: 30)
|
|
22
|
+
# config.auto_refresh_interval = 30
|
|
16
23
|
end
|
|
@@ -9,6 +9,11 @@ module SolidQueueMonitor
|
|
|
9
9
|
# Optional: Add eager loading for production
|
|
10
10
|
config.eager_load_paths << root.join('app', 'services')
|
|
11
11
|
|
|
12
|
+
# Ensure session middleware is available
|
|
13
|
+
initializer 'solid_queue_monitor.middleware' do |app|
|
|
14
|
+
app.config.session_store :cookie_store, key: '_solid_queue_monitor_session' unless app.config.session_store
|
|
15
|
+
end
|
|
16
|
+
|
|
12
17
|
initializer 'solid_queue_monitor.assets' do |app|
|
|
13
18
|
# Optional: Add assets if needed
|
|
14
19
|
end
|
data/lib/solid_queue_monitor.rb
CHANGED
|
@@ -6,13 +6,16 @@ require_relative 'solid_queue_monitor/engine'
|
|
|
6
6
|
module SolidQueueMonitor
|
|
7
7
|
class Error < StandardError; end
|
|
8
8
|
class << self
|
|
9
|
-
attr_accessor :username, :password, :jobs_per_page, :authentication_enabled
|
|
9
|
+
attr_accessor :username, :password, :jobs_per_page, :authentication_enabled,
|
|
10
|
+
:auto_refresh_enabled, :auto_refresh_interval
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
@username = 'admin'
|
|
13
14
|
@password = 'password'
|
|
14
15
|
@jobs_per_page = 25
|
|
15
16
|
@authentication_enabled = false
|
|
17
|
+
@auto_refresh_enabled = true
|
|
18
|
+
@auto_refresh_interval = 30 # seconds
|
|
16
19
|
|
|
17
20
|
def self.setup
|
|
18
21
|
yield self
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solid_queue_monitor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vishal Sadriya
|
|
@@ -71,10 +71,12 @@ files:
|
|
|
71
71
|
- app/services/solid_queue_monitor/failed_job_service.rb
|
|
72
72
|
- app/services/solid_queue_monitor/html_generator.rb
|
|
73
73
|
- app/services/solid_queue_monitor/pagination_service.rb
|
|
74
|
+
- app/services/solid_queue_monitor/queue_pause_service.rb
|
|
74
75
|
- app/services/solid_queue_monitor/reject_job_service.rb
|
|
75
76
|
- app/services/solid_queue_monitor/stats_calculator.rb
|
|
76
77
|
- app/services/solid_queue_monitor/status_calculator.rb
|
|
77
78
|
- app/services/solid_queue_monitor/stylesheet_generator.rb
|
|
79
|
+
- config/database.yml
|
|
78
80
|
- config/initializers/solid_queue_monitor.rb
|
|
79
81
|
- config/routes.rb
|
|
80
82
|
- lib/generators/solid_queue_monitor/install_generator.rb
|