sidekiq_queue_manager 1.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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +41 -0
- data/INSTALLATION.md +191 -0
- data/README.md +376 -0
- data/app/assets/javascripts/sidekiq_queue_manager/application.js +1836 -0
- data/app/assets/stylesheets/sidekiq_queue_manager/application.css +1018 -0
- data/app/assets/stylesheets/sidekiq_queue_manager/modals.css +838 -0
- data/app/controllers/sidekiq_queue_manager/application_controller.rb +190 -0
- data/app/controllers/sidekiq_queue_manager/assets_controller.rb +87 -0
- data/app/controllers/sidekiq_queue_manager/dashboard_controller.rb +373 -0
- data/app/services/sidekiq_queue_manager/queue_service.rb +475 -0
- data/app/views/layouts/sidekiq_queue_manager/application.html.erb +132 -0
- data/app/views/sidekiq_queue_manager/dashboard/index.html.erb +208 -0
- data/config/routes.rb +48 -0
- data/lib/sidekiq_queue_manager/configuration.rb +157 -0
- data/lib/sidekiq_queue_manager/engine.rb +151 -0
- data/lib/sidekiq_queue_manager/logging_middleware.rb +29 -0
- data/lib/sidekiq_queue_manager/version.rb +12 -0
- data/lib/sidekiq_queue_manager.rb +122 -0
- metadata +227 -0
@@ -0,0 +1,475 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SidekiqQueueManager
|
4
|
+
# Core service class for managing ANY Sidekiq queues
|
5
|
+
#
|
6
|
+
# Automatically discovers and provides management for all Sidekiq queues
|
7
|
+
# in the application. Works with any queue names, job types, and configurations.
|
8
|
+
#
|
9
|
+
# Features:
|
10
|
+
# - Queue pause/resume operations for any queue
|
11
|
+
# - Real-time statistics and monitoring
|
12
|
+
# - Bulk operations across multiple queues
|
13
|
+
# - Configurable queue priorities and protection
|
14
|
+
# - Health monitoring and diagnostics
|
15
|
+
#
|
16
|
+
# All operations return structured responses with success/failure status
|
17
|
+
# and detailed error messages for proper error handling.
|
18
|
+
#
|
19
|
+
class QueueService
|
20
|
+
# Default queue priority for unknown queues (higher number = higher priority)
|
21
|
+
DEFAULT_QUEUE_PRIORITY = 1
|
22
|
+
|
23
|
+
class << self
|
24
|
+
# ========================================
|
25
|
+
# Queue Control Operations
|
26
|
+
# ========================================
|
27
|
+
|
28
|
+
# Pause a specific queue
|
29
|
+
# @param queue_name [String] the name of the queue to pause
|
30
|
+
# @return [Hash] response with success status and message
|
31
|
+
def pause_queue(queue_name)
|
32
|
+
return failure_response('Invalid queue name') unless valid_queue?(queue_name)
|
33
|
+
return failure_response('Cannot pause critical queue') if critical_queue?(queue_name)
|
34
|
+
|
35
|
+
queue = Sidekiq::Queue[queue_name]
|
36
|
+
result = queue.pause
|
37
|
+
|
38
|
+
if operation_successful?(result)
|
39
|
+
update_queue_stats(queue_name, 'paused')
|
40
|
+
log_operation("Queue '#{queue_name}' paused - result: #{result}")
|
41
|
+
success_response("Queue '#{queue_name}' paused successfully")
|
42
|
+
else
|
43
|
+
log_error("Failed to pause queue '#{queue_name}': unexpected result '#{result}'")
|
44
|
+
failure_response("Failed to pause queue '#{queue_name}'")
|
45
|
+
end
|
46
|
+
rescue StandardError => e
|
47
|
+
handle_service_error(e, "pause queue '#{queue_name}'")
|
48
|
+
end
|
49
|
+
|
50
|
+
# Resume a specific queue
|
51
|
+
# @param queue_name [String] the name of the queue to resume
|
52
|
+
# @return [Hash] response with success status and message
|
53
|
+
def resume_queue(queue_name)
|
54
|
+
return failure_response('Invalid queue name') unless valid_queue?(queue_name)
|
55
|
+
|
56
|
+
queue = Sidekiq::Queue[queue_name]
|
57
|
+
result = queue.unpause
|
58
|
+
|
59
|
+
if operation_successful?(result)
|
60
|
+
update_queue_stats(queue_name, 'active')
|
61
|
+
status_msg = result.zero? ? '(already active)' : ''
|
62
|
+
log_operation("Queue '#{queue_name}' resumed - result: #{result} #{status_msg}")
|
63
|
+
success_response("Queue '#{queue_name}' resumed successfully")
|
64
|
+
else
|
65
|
+
log_error("Failed to resume queue '#{queue_name}': unexpected result '#{result}'")
|
66
|
+
failure_response("Failed to resume queue '#{queue_name}'")
|
67
|
+
end
|
68
|
+
rescue StandardError => e
|
69
|
+
handle_service_error(e, "resume queue '#{queue_name}'")
|
70
|
+
end
|
71
|
+
|
72
|
+
# Pause all non-critical queues using Ruby's functional approach
|
73
|
+
# @return [Hash] response with count of paused queues
|
74
|
+
def pause_all_queues
|
75
|
+
results = available_queues.filter_map do |queue_name|
|
76
|
+
next :skipped if critical_queue?(queue_name)
|
77
|
+
|
78
|
+
pause_queue(queue_name)[:success] ? :paused : queue_name
|
79
|
+
end
|
80
|
+
|
81
|
+
paused_count = results.count(:paused)
|
82
|
+
skipped_count = results.count(:skipped)
|
83
|
+
failed_queues = results - %i[paused skipped]
|
84
|
+
|
85
|
+
message = build_bulk_operation_message('pause', paused_count, skipped_count, failed_queues)
|
86
|
+
log_operation(message)
|
87
|
+
|
88
|
+
success_response(message,
|
89
|
+
paused: paused_count,
|
90
|
+
skipped: skipped_count,
|
91
|
+
failed: failed_queues)
|
92
|
+
rescue StandardError => e
|
93
|
+
handle_service_error(e, 'bulk pause operation')
|
94
|
+
end
|
95
|
+
|
96
|
+
# Resume all queues using Ruby's functional approach
|
97
|
+
# @return [Hash] response with count of resumed queues
|
98
|
+
def resume_all_queues
|
99
|
+
results = available_queues.filter_map do |queue_name|
|
100
|
+
next :skipped if critical_queue?(queue_name)
|
101
|
+
|
102
|
+
resume_queue(queue_name)[:success] ? :resumed : queue_name
|
103
|
+
end
|
104
|
+
|
105
|
+
resumed_count = results.count(:resumed)
|
106
|
+
skipped_count = results.count(:skipped)
|
107
|
+
failed_queues = results - %i[resumed skipped]
|
108
|
+
|
109
|
+
message = build_bulk_operation_message('resume', resumed_count, skipped_count, failed_queues)
|
110
|
+
log_operation(message)
|
111
|
+
|
112
|
+
success_response(message,
|
113
|
+
resumed: resumed_count,
|
114
|
+
skipped: skipped_count,
|
115
|
+
failed: failed_queues)
|
116
|
+
rescue StandardError => e
|
117
|
+
handle_service_error(e, 'bulk resume operation')
|
118
|
+
end
|
119
|
+
|
120
|
+
# ========================================
|
121
|
+
# Statistics and Monitoring
|
122
|
+
# ========================================
|
123
|
+
|
124
|
+
# Get comprehensive queue metrics using Ruby's expressive data transformation
|
125
|
+
# @return [Hash] complete queue statistics and global metrics
|
126
|
+
def queue_metrics
|
127
|
+
sidekiq_stats = Sidekiq::Stats.new
|
128
|
+
queue_stats = available_queues.index_with { |name| build_queue_metrics(name) }
|
129
|
+
|
130
|
+
{
|
131
|
+
global_stats: extract_global_stats(sidekiq_stats),
|
132
|
+
queues: queue_stats,
|
133
|
+
timestamp: Time.current.iso8601,
|
134
|
+
cache_buster: Time.current.to_f
|
135
|
+
}
|
136
|
+
rescue StandardError => e
|
137
|
+
handle_service_error(e, 'get queue metrics')
|
138
|
+
end
|
139
|
+
|
140
|
+
# Get status information for a specific queue or all queues
|
141
|
+
# @param queue_name [String, nil] specific queue name or nil for all queues
|
142
|
+
# @return [Hash] queue status information
|
143
|
+
def queue_status(queue_name = nil)
|
144
|
+
return single_queue_status(queue_name) if queue_name
|
145
|
+
|
146
|
+
available_queues.map { |name| single_queue_status(name) }
|
147
|
+
rescue StandardError => e
|
148
|
+
handle_service_error(e, 'get queue status')
|
149
|
+
end
|
150
|
+
|
151
|
+
# ========================================
|
152
|
+
# Advanced Queue Operations
|
153
|
+
# ========================================
|
154
|
+
|
155
|
+
def view_queue_jobs(queue_name, page: 1, per_page: 10)
|
156
|
+
return failure_response('Invalid queue name') unless valid_queue?(queue_name)
|
157
|
+
|
158
|
+
# Ruby idiom: use clamp for bounds checking
|
159
|
+
page = page.to_i.clamp(1, Float::INFINITY)
|
160
|
+
per_page = per_page.to_i.clamp(1, 100) # Max 100 per page for performance
|
161
|
+
|
162
|
+
queue = Sidekiq::Queue[queue_name]
|
163
|
+
total_jobs = queue.size
|
164
|
+
|
165
|
+
# Use Ruby's slice and map for elegant data transformation
|
166
|
+
offset = (page - 1) * per_page
|
167
|
+
jobs = queue.to_a.slice(offset, per_page)&.map&.with_index do |job, index|
|
168
|
+
format_job_data(job, offset + index + 1)
|
169
|
+
end || []
|
170
|
+
|
171
|
+
success_response('Queue jobs retrieved successfully',
|
172
|
+
queue_name: queue_name,
|
173
|
+
size: total_jobs,
|
174
|
+
latency: queue.latency.round(2),
|
175
|
+
jobs: jobs,
|
176
|
+
pagination: build_pagination_data(page, per_page, total_jobs))
|
177
|
+
rescue StandardError => e
|
178
|
+
handle_service_error(e, "get jobs for queue '#{queue_name}'")
|
179
|
+
end
|
180
|
+
|
181
|
+
def delete_job(queue_name, job_id)
|
182
|
+
return failure_response('Invalid queue name') unless valid_queue?(queue_name)
|
183
|
+
return failure_response('Invalid job ID') if job_id.blank?
|
184
|
+
|
185
|
+
queue = Sidekiq::Queue[queue_name]
|
186
|
+
job = queue.find_job(job_id)
|
187
|
+
|
188
|
+
return failure_response('Job not found') unless job
|
189
|
+
|
190
|
+
job.delete
|
191
|
+
log_operation("Job #{job_id} deleted from queue '#{queue_name}'")
|
192
|
+
success_response("Job #{job_id} deleted successfully")
|
193
|
+
rescue StandardError => e
|
194
|
+
handle_service_error(e, "delete job #{job_id} from queue '#{queue_name}'")
|
195
|
+
end
|
196
|
+
|
197
|
+
# Enhanced queue management methods with Ruby idioms
|
198
|
+
%w[set_queue_limit remove_queue_limit set_process_limit remove_process_limit
|
199
|
+
block_queue unblock_queue].each do |method_name|
|
200
|
+
define_method method_name do |queue_name, *args|
|
201
|
+
return failure_response('Invalid queue name') unless valid_queue?(queue_name)
|
202
|
+
|
203
|
+
queue = Sidekiq::Queue[queue_name]
|
204
|
+
operation = method_name.split('_')[0] # 'set', 'remove', 'block', 'unblock'
|
205
|
+
attribute = method_name.match(/_(queue_|process_)?(.+)$/)[2] # 'limit', 'process_limit'
|
206
|
+
|
207
|
+
perform_queue_operation(queue, queue_name, operation, attribute, *args)
|
208
|
+
rescue StandardError => e
|
209
|
+
handle_service_error(e, "#{method_name.humanize.downcase} for queue '#{queue_name}'")
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def clear_queue(queue_name)
|
214
|
+
return failure_response('Invalid queue name') unless valid_queue?(queue_name)
|
215
|
+
return failure_response('Cannot clear critical queue') if critical_queue?(queue_name)
|
216
|
+
|
217
|
+
queue = Sidekiq::Queue[queue_name]
|
218
|
+
jobs_cleared = queue.size
|
219
|
+
queue.clear
|
220
|
+
|
221
|
+
log_operation("Queue '#{queue_name}' cleared - #{jobs_cleared} jobs removed")
|
222
|
+
success_response("Queue '#{queue_name}' cleared successfully", jobs_cleared: jobs_cleared)
|
223
|
+
rescue StandardError => e
|
224
|
+
handle_service_error(e, "clear queue '#{queue_name}'")
|
225
|
+
end
|
226
|
+
|
227
|
+
private
|
228
|
+
|
229
|
+
# ========================================
|
230
|
+
# Queue Information Helpers
|
231
|
+
# ========================================
|
232
|
+
|
233
|
+
def available_queues
|
234
|
+
@available_queues ||= discover_queues
|
235
|
+
end
|
236
|
+
|
237
|
+
# Ruby's functional approach to queue discovery
|
238
|
+
def discover_queues
|
239
|
+
[
|
240
|
+
# Method 1: Sidekiq's built-in discovery
|
241
|
+
-> { Sidekiq::Queue.all.map(&:name) },
|
242
|
+
# Method 2: Redis queues set
|
243
|
+
-> { Sidekiq.redis { |conn| conn.smembers('queues') } },
|
244
|
+
# Method 3: Redis queue keys
|
245
|
+
-> { discover_redis_queue_keys }
|
246
|
+
].flat_map(&:call).uniq.sort
|
247
|
+
end
|
248
|
+
|
249
|
+
def discover_redis_queue_keys
|
250
|
+
Sidekiq.redis do |conn|
|
251
|
+
conn.keys('queue:*').filter_map do |key|
|
252
|
+
key.delete_prefix('queue:').presence
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
# Ruby predicate methods for cleaner conditionals
|
258
|
+
def valid_queue?(queue_name) = queue_name.is_a?(String) && available_queues.include?(queue_name)
|
259
|
+
def critical_queue?(queue_name) = SidekiqQueueManager.critical_queue?(queue_name)
|
260
|
+
def queue_paused?(queue_name) = Sidekiq::Queue[queue_name].paused?
|
261
|
+
def queue_priority(queue_name) = SidekiqQueueManager.queue_priority(queue_name)
|
262
|
+
|
263
|
+
# ========================================
|
264
|
+
# Statistics Builders
|
265
|
+
# ========================================
|
266
|
+
|
267
|
+
def build_queue_metrics(queue_name)
|
268
|
+
queue = Sidekiq::Queue[queue_name]
|
269
|
+
|
270
|
+
{
|
271
|
+
name: queue_name,
|
272
|
+
size: queue.size,
|
273
|
+
latency: queue.latency.round(2),
|
274
|
+
paused: queue_paused?(queue_name),
|
275
|
+
critical: critical_queue?(queue_name),
|
276
|
+
priority: queue_priority(queue_name),
|
277
|
+
busy: busy_workers_for_queue(queue_name),
|
278
|
+
# sidekiq-limit_fetch integration with safe attribute access
|
279
|
+
limit: safe_queue_attribute(queue, :limit),
|
280
|
+
process_limit: safe_queue_attribute(queue, :process_limit),
|
281
|
+
blocked: safe_queue_attribute(queue, :blocking?, false)
|
282
|
+
}
|
283
|
+
end
|
284
|
+
|
285
|
+
def extract_global_stats(sidekiq_stats)
|
286
|
+
{
|
287
|
+
processed: sidekiq_stats.processed,
|
288
|
+
failed: sidekiq_stats.failed,
|
289
|
+
busy: total_busy_workers,
|
290
|
+
enqueued: total_enqueued_jobs,
|
291
|
+
processes: sidekiq_stats.processes_size,
|
292
|
+
workers: sidekiq_stats.workers_size,
|
293
|
+
retry_size: sidekiq_stats.retry_size,
|
294
|
+
dead_size: sidekiq_stats.dead_size,
|
295
|
+
scheduled_size: sidekiq_stats.scheduled_size
|
296
|
+
}
|
297
|
+
end
|
298
|
+
|
299
|
+
# ========================================
|
300
|
+
# Helper Methods
|
301
|
+
# ========================================
|
302
|
+
|
303
|
+
# Ruby idiom: intention-revealing method names
|
304
|
+
def operation_successful?(result) = ['OK', 1, 0].include?(result)
|
305
|
+
def positive_integer?(value) = value.is_a?(Integer) && value.positive?
|
306
|
+
|
307
|
+
def single_queue_status(queue_name)
|
308
|
+
return failure_response('Invalid queue name') unless valid_queue?(queue_name)
|
309
|
+
|
310
|
+
queue = Sidekiq::Queue[queue_name]
|
311
|
+
{
|
312
|
+
name: queue_name,
|
313
|
+
size: queue.size,
|
314
|
+
latency: queue.latency.round(2),
|
315
|
+
paused: queue_paused?(queue_name),
|
316
|
+
critical: critical_queue?(queue_name),
|
317
|
+
priority: queue_priority(queue_name)
|
318
|
+
}
|
319
|
+
end
|
320
|
+
|
321
|
+
def format_job_data(job, position)
|
322
|
+
{
|
323
|
+
position: position,
|
324
|
+
jid: job.jid,
|
325
|
+
class: job.klass,
|
326
|
+
args: job.args,
|
327
|
+
created_at: job.created_at&.strftime('%Y-%m-%d %H:%M:%S'),
|
328
|
+
enqueued_at: job.enqueued_at&.strftime('%Y-%m-%d %H:%M:%S'),
|
329
|
+
retry_count: job['retry_count'] || 0,
|
330
|
+
queue: job.queue
|
331
|
+
}
|
332
|
+
end
|
333
|
+
|
334
|
+
def build_bulk_operation_message(operation, success_count, skipped_count, failed_queues)
|
335
|
+
message = "Bulk #{operation} completed. #{operation.capitalize}d: #{success_count}, Skipped: #{skipped_count}"
|
336
|
+
message += ", Failed: #{failed_queues.join(', ')}" if failed_queues.any?
|
337
|
+
message
|
338
|
+
end
|
339
|
+
|
340
|
+
def perform_queue_operation(queue, queue_name, operation, attribute, *args)
|
341
|
+
case operation
|
342
|
+
when 'set'
|
343
|
+
limit = args.first&.to_i
|
344
|
+
return failure_response('Invalid limit') unless positive_integer?(limit)
|
345
|
+
|
346
|
+
queue.public_send("#{attribute}=", limit)
|
347
|
+
log_operation("Queue '#{queue_name}' #{attribute} set to #{limit}")
|
348
|
+
success_response("Queue '#{queue_name}' #{attribute} set to #{limit}")
|
349
|
+
when 'remove'
|
350
|
+
queue.public_send("#{attribute}=", nil)
|
351
|
+
log_operation("Queue '#{queue_name}' #{attribute} removed")
|
352
|
+
success_response("Queue '#{queue_name}' #{attribute} removed")
|
353
|
+
when 'block'
|
354
|
+
queue.block
|
355
|
+
log_operation("Queue '#{queue_name}' blocked")
|
356
|
+
success_response("Queue '#{queue_name}' blocked successfully")
|
357
|
+
when 'unblock'
|
358
|
+
queue.unblock
|
359
|
+
log_operation("Queue '#{queue_name}' unblocked")
|
360
|
+
success_response("Queue '#{queue_name}' unblocked successfully")
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
# Safe attribute access with Ruby's method handling
|
365
|
+
def safe_queue_attribute(queue, attribute, default = nil)
|
366
|
+
queue.public_send(attribute)
|
367
|
+
rescue StandardError
|
368
|
+
default
|
369
|
+
end
|
370
|
+
|
371
|
+
def busy_workers_for_queue(queue_name)
|
372
|
+
Sidekiq::Workers.new.count { |_, _, work| work['queue'] == queue_name }
|
373
|
+
rescue StandardError => e
|
374
|
+
log_error("Failed to get busy workers for queue '#{queue_name}': #{e.message}")
|
375
|
+
0
|
376
|
+
end
|
377
|
+
|
378
|
+
def total_busy_workers
|
379
|
+
Sidekiq::Workers.new.size
|
380
|
+
rescue StandardError => e
|
381
|
+
log_error("Failed to get total busy workers: #{e.message}")
|
382
|
+
0
|
383
|
+
end
|
384
|
+
|
385
|
+
def total_enqueued_jobs
|
386
|
+
available_queues.sum { |queue_name| Sidekiq::Queue[queue_name].size }
|
387
|
+
rescue StandardError => e
|
388
|
+
log_error("Failed to get total enqueued jobs: #{e.message}")
|
389
|
+
0
|
390
|
+
end
|
391
|
+
|
392
|
+
# ========================================
|
393
|
+
# Response Builders (Ruby's expressive approach)
|
394
|
+
# ========================================
|
395
|
+
|
396
|
+
def success_response(message, **data)
|
397
|
+
build_response(true, message, **data)
|
398
|
+
end
|
399
|
+
|
400
|
+
def failure_response(message, **data)
|
401
|
+
build_response(false, message, **data)
|
402
|
+
end
|
403
|
+
|
404
|
+
def build_response(success, message, **data)
|
405
|
+
{
|
406
|
+
success: success,
|
407
|
+
message: message,
|
408
|
+
timestamp: Time.current.iso8601
|
409
|
+
}.merge(data)
|
410
|
+
end
|
411
|
+
|
412
|
+
# ========================================
|
413
|
+
# Error Handling (Ruby's approach to exceptions)
|
414
|
+
# ========================================
|
415
|
+
|
416
|
+
def handle_service_error(error, operation)
|
417
|
+
log_error("Failed to #{operation}: #{error.message}")
|
418
|
+
failure_response("Failed to #{operation}: #{error.message}")
|
419
|
+
end
|
420
|
+
|
421
|
+
# ========================================
|
422
|
+
# Logging Helpers
|
423
|
+
# ========================================
|
424
|
+
|
425
|
+
def log_operation(message)
|
426
|
+
return unless SidekiqQueueManager.enable_logging?
|
427
|
+
|
428
|
+
Rails.logger.public_send(
|
429
|
+
SidekiqQueueManager.configuration.log_level,
|
430
|
+
"[SidekiqQueueManager] #{message}"
|
431
|
+
)
|
432
|
+
end
|
433
|
+
|
434
|
+
def log_error(message)
|
435
|
+
return unless SidekiqQueueManager.enable_logging?
|
436
|
+
|
437
|
+
Rails.logger.error("[SidekiqQueueManager] #{message}")
|
438
|
+
end
|
439
|
+
|
440
|
+
# ========================================
|
441
|
+
# Data Management
|
442
|
+
# ========================================
|
443
|
+
|
444
|
+
def update_queue_stats(queue_name, status)
|
445
|
+
return unless SidekiqQueueManager.enable_caching?
|
446
|
+
|
447
|
+
stats_key = "#{redis_key_prefix}:#{queue_name}"
|
448
|
+
|
449
|
+
Sidekiq.redis do |conn|
|
450
|
+
conn.hset(stats_key, 'status', status.to_s)
|
451
|
+
conn.hset(stats_key, 'updated_at', Time.current.to_i.to_s)
|
452
|
+
conn.expire(stats_key, SidekiqQueueManager.configuration.cache_ttl)
|
453
|
+
end
|
454
|
+
rescue StandardError => e
|
455
|
+
log_error("Failed to update queue stats for '#{queue_name}': #{e.message}")
|
456
|
+
end
|
457
|
+
|
458
|
+
def redis_key_prefix = SidekiqQueueManager.configuration.redis_key_prefix
|
459
|
+
|
460
|
+
def build_pagination_data(page, per_page, total_items)
|
461
|
+
total_pages = (total_items.to_f / per_page).ceil
|
462
|
+
|
463
|
+
{
|
464
|
+
current_page: page,
|
465
|
+
per_page: per_page,
|
466
|
+
total_pages: total_pages,
|
467
|
+
total_jobs: total_items,
|
468
|
+
has_previous: page > 1,
|
469
|
+
has_next: page < total_pages
|
470
|
+
}
|
471
|
+
end
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
@@ -0,0 +1,132 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en" class="sidekiq-queue-manager">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
7
|
+
|
8
|
+
<title>Sidekiq Queue Manager</title>
|
9
|
+
<meta name="description" content="Professional Sidekiq queue monitoring and management interface">
|
10
|
+
|
11
|
+
<!-- Security Headers -->
|
12
|
+
<meta name="robots" content="noindex, nofollow">
|
13
|
+
|
14
|
+
<!-- Color Scheme Support -->
|
15
|
+
<meta name="color-scheme" content="light dark">
|
16
|
+
<meta name="theme-color" content="#6366f1" media="(prefers-color-scheme: light)">
|
17
|
+
<meta name="theme-color" content="#4f46e5" media="(prefers-color-scheme: dark)">
|
18
|
+
|
19
|
+
<!-- Favicon -->
|
20
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%236366f1'%3e%3cpath stroke-linecap='round' stroke-linejoin='round' d='M3.75 6.75h16.5M3.75 12h16.5M12 17.25h8.25M3.75 17.25h.01v.01h-.01v-.01ZM7.5 17.25h.01v.01H7.5v-.01ZM11.25 17.25h.01v.01h-.01v-.01Z' /%3e%3c/svg%3e">
|
21
|
+
|
22
|
+
<!-- Asset Loading (Compatible with asset pipeline and direct serving) -->
|
23
|
+
<% asset_info = asset_serving_info %>
|
24
|
+
|
25
|
+
<!-- Preload Critical Resources -->
|
26
|
+
<link rel="preload" href="<%= asset_info[:css_path] %>" as="style">
|
27
|
+
<link rel="preload" href="<%= asset_info[:modals_css_path] %>" as="style">
|
28
|
+
<link rel="preload" href="<%= asset_info[:js_path] %>" as="script">
|
29
|
+
|
30
|
+
<!-- Stylesheets -->
|
31
|
+
<% if asset_info[:use_asset_pipeline] %>
|
32
|
+
<%= stylesheet_link_tag 'sidekiq_queue_manager/application', 'data-turbo-track': 'reload' %>
|
33
|
+
<%= stylesheet_link_tag 'sidekiq_queue_manager/modals', 'data-turbo-track': 'reload' %>
|
34
|
+
<% else %>
|
35
|
+
<link rel="stylesheet" href="<%= asset_info[:css_path] %>" data-turbo-track="reload">
|
36
|
+
<link rel="stylesheet" href="<%= asset_info[:modals_css_path] %>" data-turbo-track="reload">
|
37
|
+
<% end %>
|
38
|
+
|
39
|
+
<!-- Theme Initialization Script (Prevent FOUC) -->
|
40
|
+
<script>
|
41
|
+
(function() {
|
42
|
+
const storedTheme = localStorage.getItem('sqm-theme');
|
43
|
+
const systemDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
44
|
+
|
45
|
+
if (storedTheme === 'dark' || (!storedTheme && systemDark)) {
|
46
|
+
document.documentElement.setAttribute('data-theme', 'dark');
|
47
|
+
} else if (storedTheme === 'light') {
|
48
|
+
document.documentElement.setAttribute('data-theme', 'light');
|
49
|
+
}
|
50
|
+
// For 'auto' or no preference, let CSS handle it
|
51
|
+
})();
|
52
|
+
</script>
|
53
|
+
|
54
|
+
<!-- Configuration for JavaScript -->
|
55
|
+
<script type="text/javascript">
|
56
|
+
window.SidekiqQueueManagerConfig = {
|
57
|
+
mountPath: '<%= sidekiq_dashboard.root_path %>',
|
58
|
+
refreshInterval: <%= SidekiqQueueManager.configuration.refresh_interval %>,
|
59
|
+
theme: '<%= SidekiqQueueManager.configuration.theme %>',
|
60
|
+
criticalQueues: <%= SidekiqQueueManager.configuration.critical_queues.to_json.html_safe %>,
|
61
|
+
enableLogging: <%= SidekiqQueueManager.configuration.enable_logging %>
|
62
|
+
};
|
63
|
+
</script>
|
64
|
+
</head>
|
65
|
+
|
66
|
+
<body class="sidekiq-queue-manager">
|
67
|
+
<!-- Skip Link for Accessibility -->
|
68
|
+
<a href="#main-content" class="skip-link sqm-btn sqm-btn-primary"
|
69
|
+
style="position: absolute; top: -3rem; left: 50%; transform: translateX(-50%); z-index: 10001; transition: top 0.2s ease-out;">
|
70
|
+
Skip to main content
|
71
|
+
</a>
|
72
|
+
|
73
|
+
<!-- Main Application Container -->
|
74
|
+
<div class="sqm-container">
|
75
|
+
<!-- Header -->
|
76
|
+
<header class="sqm-header" role="banner">
|
77
|
+
<div>
|
78
|
+
<h1 class="sqm-title">
|
79
|
+
📊 Sidekiq Queue Manager
|
80
|
+
</h1>
|
81
|
+
<p class="sqm-subtitle">
|
82
|
+
Professional queue monitoring and management interface
|
83
|
+
</p>
|
84
|
+
</div>
|
85
|
+
|
86
|
+
<div class="sqm-header-info">
|
87
|
+
<div style="font-size: 0.6875rem; opacity: 0.8;">Version <%= SidekiqQueueManager::VERSION %></div>
|
88
|
+
<div id="sqm-last-updated" style="margin-top: 0.25rem;">
|
89
|
+
Last updated: <span id="sqm-timestamp">Loading...</span>
|
90
|
+
</div>
|
91
|
+
</div>
|
92
|
+
</header>
|
93
|
+
|
94
|
+
<!-- Main Content Area -->
|
95
|
+
<main id="main-content" role="main">
|
96
|
+
<%= yield %>
|
97
|
+
</main>
|
98
|
+
|
99
|
+
<!-- Footer -->
|
100
|
+
<footer class="sqm-footer"
|
101
|
+
style="margin-top: 3rem; padding-top: 2rem; border-top: 1px solid var(--sqm-border); text-align: center; color: var(--sqm-muted-foreground); font-size: 0.75rem;">
|
102
|
+
<p>
|
103
|
+
Powered by
|
104
|
+
<a href="https://github.com/jamalawd/sidekiq_queue_manager"
|
105
|
+
target="_blank"
|
106
|
+
rel="noopener noreferrer"
|
107
|
+
style="color: var(--sqm-primary); text-decoration: none; font-weight: 500;">
|
108
|
+
Sidekiq Queue Manager
|
109
|
+
</a>
|
110
|
+
v<%= SidekiqQueueManager::VERSION %>
|
111
|
+
</p>
|
112
|
+
<p style="margin-top: 0.5rem; font-size: 0.6875rem; opacity: 0.8;">
|
113
|
+
Ruby <%= RUBY_VERSION %> • Rails <%= Rails.version %> • Sidekiq <%= Sidekiq::VERSION %>
|
114
|
+
</p>
|
115
|
+
</footer>
|
116
|
+
</div>
|
117
|
+
|
118
|
+
<!-- JavaScript -->
|
119
|
+
<% if asset_info[:use_asset_pipeline] %>
|
120
|
+
<%= javascript_include_tag 'sidekiq_queue_manager/application', 'data-turbo-track': 'reload' %>
|
121
|
+
<% else %>
|
122
|
+
<script src="<%= asset_info[:js_path] %>" data-turbo-track="reload"></script>
|
123
|
+
<% end %>
|
124
|
+
|
125
|
+
<!-- Skip Link Focus Styles -->
|
126
|
+
<style>
|
127
|
+
.skip-link:focus {
|
128
|
+
top: 1rem;
|
129
|
+
}
|
130
|
+
</style>
|
131
|
+
</body>
|
132
|
+
</html>
|