solid_queue_monitor 0.1.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 (29) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +128 -0
  3. data/Rakefile +23 -0
  4. data/app/controllers/solid_queue_monitor/monitor_controller.rb +250 -0
  5. data/app/presenters/solid_queue_monitor/base_presenter.rb +136 -0
  6. data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +70 -0
  7. data/app/presenters/solid_queue_monitor/jobs_presenter.rb +100 -0
  8. data/app/presenters/solid_queue_monitor/queues_presenter.rb +62 -0
  9. data/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb +72 -0
  10. data/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb +77 -0
  11. data/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb +114 -0
  12. data/app/presenters/solid_queue_monitor/stats_presenter.rb +35 -0
  13. data/app/services/solid_queue_monitor/authentication_service.rb +14 -0
  14. data/app/services/solid_queue_monitor/execute_job_service.rb +28 -0
  15. data/app/services/solid_queue_monitor/html_generator.rb +88 -0
  16. data/app/services/solid_queue_monitor/pagination_service.rb +31 -0
  17. data/app/services/solid_queue_monitor/stats_calculator.rb +15 -0
  18. data/app/services/solid_queue_monitor/status_calculator.rb +14 -0
  19. data/app/services/solid_queue_monitor/stylesheet_generator.rb +395 -0
  20. data/config/initializers/solid_queue_monitor.rb +5 -0
  21. data/config/routes.rb +11 -0
  22. data/lib/generators/solid_queue_monitor/install_generator.rb +23 -0
  23. data/lib/generators/solid_queue_monitor/templates/README.md +23 -0
  24. data/lib/generators/solid_queue_monitor/templates/initializer.rb +14 -0
  25. data/lib/solid_queue_monitor/engine.rb +14 -0
  26. data/lib/solid_queue_monitor/version.rb +5 -0
  27. data/lib/solid_queue_monitor.rb +24 -0
  28. data/lib/tasks/app.rake +135 -0
  29. metadata +240 -0
@@ -0,0 +1,100 @@
1
+ module SolidQueueMonitor
2
+ class JobsPresenter < BasePresenter
3
+ include Rails.application.routes.url_helpers
4
+ include SolidQueueMonitor::Engine.routes.url_helpers
5
+
6
+ def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
7
+ @jobs = jobs
8
+ @current_page = current_page
9
+ @total_pages = total_pages
10
+ @filters = filters
11
+ end
12
+
13
+ def render
14
+ <<-HTML
15
+ <div class="section-wrapper">
16
+ <div class="section">
17
+ <h3>Recent Jobs</h3>
18
+ #{generate_filter_form}
19
+ #{generate_table}
20
+ #{generate_pagination(@current_page, @total_pages)}
21
+ </div>
22
+ </div>
23
+ HTML
24
+ end
25
+
26
+ private
27
+
28
+ def generate_filter_form
29
+ <<-HTML
30
+ <div class="filter-form-container">
31
+ <form method="get" action="" class="filter-form">
32
+ <div class="filter-group">
33
+ <label for="class_name">Job Class:</label>
34
+ <input type="text" name="class_name" id="class_name" value="#{@filters[:class_name]}" placeholder="Filter by class name">
35
+ </div>
36
+
37
+ <div class="filter-group">
38
+ <label for="queue_name">Queue:</label>
39
+ <input type="text" name="queue_name" id="queue_name" value="#{@filters[:queue_name]}" placeholder="Filter by queue">
40
+ </div>
41
+
42
+ <div class="filter-group">
43
+ <label for="status">Status:</label>
44
+ <select name="status" id="status">
45
+ <option value="">All Statuses</option>
46
+ <option value="completed" #{@filters[:status] == 'completed' ? 'selected' : ''}>Completed</option>
47
+ <option value="failed" #{@filters[:status] == 'failed' ? 'selected' : ''}>Failed</option>
48
+ <option value="scheduled" #{@filters[:status] == 'scheduled' ? 'selected' : ''}>Scheduled</option>
49
+ <option value="pending" #{@filters[:status] == 'pending' ? 'selected' : ''}>Pending</option>
50
+ </select>
51
+ </div>
52
+
53
+ <div class="filter-actions">
54
+ <button type="submit" class="filter-button">Apply Filters</button>
55
+ <a href="#{root_path}" class="reset-button">Reset</a>
56
+ </div>
57
+ </form>
58
+ </div>
59
+ HTML
60
+ end
61
+
62
+ def generate_table
63
+ <<-HTML
64
+ <div class="table-container">
65
+ <table>
66
+ <thead>
67
+ <tr>
68
+ <th>ID</th>
69
+ <th>Job</th>
70
+ <th>Queue</th>
71
+ <th>Status</th>
72
+ <th>Created At</th>
73
+ </tr>
74
+ </thead>
75
+ <tbody>
76
+ #{@jobs.map { |job| generate_row(job) }.join}
77
+ </tbody>
78
+ </table>
79
+ </div>
80
+ HTML
81
+ end
82
+
83
+ def generate_row(job)
84
+ status = job_status(job)
85
+ <<-HTML
86
+ <tr>
87
+ <td>#{job.id}</td>
88
+ <td>#{job.class_name}</td>
89
+ <td>#{job.queue_name}</td>
90
+ <td><span class='status-badge status-#{status}'>#{status}</span></td>
91
+ <td>#{format_datetime(job.created_at)}</td>
92
+ </tr>
93
+ HTML
94
+ end
95
+
96
+ def job_status(job)
97
+ SolidQueueMonitor::StatusCalculator.new(job).calculate
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,62 @@
1
+ module SolidQueueMonitor
2
+ class QueuesPresenter < BasePresenter
3
+ def initialize(records)
4
+ @records = records
5
+ end
6
+
7
+ def render
8
+ section_wrapper('Queues', generate_table)
9
+ end
10
+
11
+ private
12
+
13
+ def generate_table
14
+ <<-HTML
15
+ <div class="table-container">
16
+ <table>
17
+ <thead>
18
+ <tr>
19
+ <th>Queue Name</th>
20
+ <th>Total Jobs</th>
21
+ <th>Ready Jobs</th>
22
+ <th>Scheduled Jobs</th>
23
+ <th>Failed Jobs</th>
24
+ </tr>
25
+ </thead>
26
+ <tbody>
27
+ #{@records.map { |queue| generate_row(queue) }.join}
28
+ </tbody>
29
+ </table>
30
+ </div>
31
+ HTML
32
+ end
33
+
34
+ def generate_row(queue)
35
+ <<-HTML
36
+ <tr>
37
+ <td>#{queue.queue_name || 'default'}</td>
38
+ <td>#{queue.job_count}</td>
39
+ <td>#{ready_jobs_count(queue.queue_name)}</td>
40
+ <td>#{scheduled_jobs_count(queue.queue_name)}</td>
41
+ <td>#{failed_jobs_count(queue.queue_name)}</td>
42
+ </tr>
43
+ HTML
44
+ end
45
+
46
+ private
47
+
48
+ def ready_jobs_count(queue_name)
49
+ SolidQueue::ReadyExecution.where(queue_name: queue_name).count
50
+ end
51
+
52
+ def scheduled_jobs_count(queue_name)
53
+ SolidQueue::ScheduledExecution.where(queue_name: queue_name).count
54
+ end
55
+
56
+ def failed_jobs_count(queue_name)
57
+ SolidQueue::FailedExecution.joins(:job)
58
+ .where(solid_queue_jobs: { queue_name: queue_name })
59
+ .count
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,72 @@
1
+ module SolidQueueMonitor
2
+ class ReadyJobsPresenter < BasePresenter
3
+ def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
4
+ @jobs = jobs
5
+ @current_page = current_page
6
+ @total_pages = total_pages
7
+ @filters = filters
8
+ end
9
+
10
+ def render
11
+ section_wrapper('Ready Jobs', generate_filter_form + generate_table + generate_pagination(@current_page, @total_pages))
12
+ end
13
+
14
+ private
15
+
16
+ def generate_filter_form
17
+ <<-HTML
18
+ <div class="filter-form-container">
19
+ <form method="get" action="" class="filter-form">
20
+ <div class="filter-group">
21
+ <label for="class_name">Job Class:</label>
22
+ <input type="text" name="class_name" id="class_name" value="#{@filters[:class_name]}" placeholder="Filter by class name">
23
+ </div>
24
+
25
+ <div class="filter-group">
26
+ <label for="queue_name">Queue:</label>
27
+ <input type="text" name="queue_name" id="queue_name" value="#{@filters[:queue_name]}" placeholder="Filter by queue">
28
+ </div>
29
+
30
+ <div class="filter-actions">
31
+ <button type="submit" class="filter-button">Apply Filters</button>
32
+ <a href="#{ready_jobs_path}" class="reset-button">Reset</a>
33
+ </div>
34
+ </form>
35
+ </div>
36
+ HTML
37
+ end
38
+
39
+ def generate_table
40
+ <<-HTML
41
+ <div class="table-container">
42
+ <table>
43
+ <thead>
44
+ <tr>
45
+ <th>Job</th>
46
+ <th>Queue</th>
47
+ <th>Priority</th>
48
+ <th>Arguments</th>
49
+ <th>Created At</th>
50
+ </tr>
51
+ </thead>
52
+ <tbody>
53
+ #{@jobs.map { |execution| generate_row(execution) }.join}
54
+ </tbody>
55
+ </table>
56
+ </div>
57
+ HTML
58
+ end
59
+
60
+ def generate_row(execution)
61
+ <<-HTML
62
+ <tr>
63
+ <td>#{execution.job.class_name}</td>
64
+ <td>#{execution.queue_name}</td>
65
+ <td>#{execution.priority}</td>
66
+ <td>#{format_arguments(execution.job.arguments)}</td>
67
+ <td>#{format_datetime(execution.created_at)}</td>
68
+ </tr>
69
+ HTML
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,77 @@
1
+ module SolidQueueMonitor
2
+ class RecurringJobsPresenter < BasePresenter
3
+ include Rails.application.routes.url_helpers
4
+ include SolidQueueMonitor::Engine.routes.url_helpers
5
+
6
+ def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
7
+ @jobs = jobs
8
+ @current_page = current_page
9
+ @total_pages = total_pages
10
+ @filters = filters
11
+ end
12
+
13
+ def render
14
+ section_wrapper('Recurring Jobs', generate_filter_form + generate_table + generate_pagination(@current_page, @total_pages))
15
+ end
16
+
17
+ private
18
+
19
+ def generate_filter_form
20
+ <<-HTML
21
+ <div class="filter-form-container">
22
+ <form method="get" action="" class="filter-form">
23
+ <div class="filter-group">
24
+ <label for="class_name">Job Class:</label>
25
+ <input type="text" name="class_name" id="class_name" value="#{@filters[:class_name]}" placeholder="Filter by class name">
26
+ </div>
27
+
28
+ <div class="filter-group">
29
+ <label for="queue_name">Queue:</label>
30
+ <input type="text" name="queue_name" id="queue_name" value="#{@filters[:queue_name]}" placeholder="Filter by queue">
31
+ </div>
32
+
33
+ <div class="filter-actions">
34
+ <button type="submit" class="filter-button">Apply Filters</button>
35
+ <a href="#{recurring_jobs_path}" class="reset-button">Reset</a>
36
+ </div>
37
+ </form>
38
+ </div>
39
+ HTML
40
+ end
41
+
42
+ def generate_table
43
+ <<-HTML
44
+ <div class="table-container">
45
+ <table>
46
+ <thead>
47
+ <tr>
48
+ <th>Key</th>
49
+ <th>Job</th>
50
+ <th>Schedule</th>
51
+ <th>Queue</th>
52
+ <th>Priority</th>
53
+ <th>Last Updated</th>
54
+ </tr>
55
+ </thead>
56
+ <tbody>
57
+ #{@jobs.map { |task| generate_row(task) }.join}
58
+ </tbody>
59
+ </table>
60
+ </div>
61
+ HTML
62
+ end
63
+
64
+ def generate_row(task)
65
+ <<-HTML
66
+ <tr>
67
+ <td>#{task.key}</td>
68
+ <td>#{task.class_name}</td>
69
+ <td>#{task.schedule}</td>
70
+ <td>#{task.queue_name}</td>
71
+ <td>#{task.priority || 'Default'}</td>
72
+ <td>#{format_datetime(task.updated_at)}</td>
73
+ </tr>
74
+ HTML
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,114 @@
1
+ module SolidQueueMonitor
2
+ class ScheduledJobsPresenter < BasePresenter
3
+
4
+ include Rails.application.routes.url_helpers
5
+ include SolidQueueMonitor::Engine.routes.url_helpers
6
+
7
+ def initialize(jobs, current_page: 1, total_pages: 1, filters: {})
8
+ @jobs = jobs
9
+ @current_page = current_page
10
+ @total_pages = total_pages
11
+ @filters = filters
12
+ end
13
+
14
+ def render
15
+ section_wrapper('Scheduled Jobs', generate_filter_form + generate_table_with_actions)
16
+ end
17
+
18
+ private
19
+
20
+ def generate_filter_form
21
+ <<-HTML
22
+ <div class="filter-form-container">
23
+ <form method="get" action="" class="filter-form">
24
+ <div class="filter-group">
25
+ <label for="class_name">Job Class:</label>
26
+ <input type="text" name="class_name" id="class_name" value="#{@filters[:class_name]}" placeholder="Filter by class name">
27
+ </div>
28
+
29
+ <div class="filter-group">
30
+ <label for="queue_name">Queue:</label>
31
+ <input type="text" name="queue_name" id="queue_name" value="#{@filters[:queue_name]}" placeholder="Filter by queue">
32
+ </div>
33
+
34
+ <div class="filter-actions">
35
+ <button type="submit" class="filter-button">Apply Filters</button>
36
+ <a href="#{scheduled_jobs_path}" class="reset-button">Reset</a>
37
+ </div>
38
+ </form>
39
+ </div>
40
+ HTML
41
+ end
42
+
43
+ def generate_table_with_actions
44
+ <<-HTML
45
+ <form action="#{execute_jobs_path}" method="POST">
46
+ #{generate_table}
47
+ <div class="table-actions">
48
+ <button type="submit" class="execute-btn" id="bulk-execute" disabled>Execute Selected</button>
49
+ </div>
50
+ </form>
51
+ <script>
52
+ document.addEventListener('DOMContentLoaded', function() {
53
+ const selectAllCheckbox = document.querySelector('th input[type="checkbox"]');
54
+ const jobCheckboxes = document.getElementsByName('job_ids[]');
55
+
56
+ selectAllCheckbox.addEventListener('change', function() {
57
+ jobCheckboxes.forEach(checkbox => checkbox.checked = this.checked);
58
+ updateExecuteButton();
59
+ });
60
+
61
+ jobCheckboxes.forEach(checkbox => {
62
+ checkbox.addEventListener('change', function() {
63
+ selectAllCheckbox.checked = Array.from(jobCheckboxes).every(cb => cb.checked);
64
+ updateExecuteButton();
65
+ });
66
+ });
67
+ });
68
+
69
+ function updateExecuteButton() {
70
+ const checkboxes = document.getElementsByName('job_ids[]');
71
+ const checked = Array.from(checkboxes).some(cb => cb.checked);
72
+ document.getElementById('bulk-execute').disabled = !checked;
73
+ }
74
+ </script>
75
+ HTML
76
+ end
77
+
78
+ def generate_table
79
+ <<-HTML
80
+ <div class="table-container">
81
+ <table>
82
+ <thead>
83
+ <tr>
84
+ <th width="50"><input type="checkbox"></th>
85
+ <th>Job</th>
86
+ <th>Queue</th>
87
+ <th>Scheduled At</th>
88
+ <th>Arguments</th>
89
+ </tr>
90
+ </thead>
91
+ <tbody>
92
+ #{@jobs.map { |execution| generate_row(execution) }.join}
93
+ </tbody>
94
+ </table>
95
+ </div>
96
+ #{generate_pagination(@current_page, @total_pages)}
97
+ HTML
98
+ end
99
+
100
+ def generate_row(execution)
101
+ <<-HTML
102
+ <tr>
103
+ <td>
104
+ <input type="checkbox" name="job_ids[]" value="#{execution.id}" onchange="updateExecuteButton()">
105
+ </td>
106
+ <td>#{execution.job.class_name}</td>
107
+ <td>#{execution.queue_name}</td>
108
+ <td>#{format_datetime(execution.scheduled_at)}</td>
109
+ <td>#{format_arguments(execution.job.arguments)}</td>
110
+ </tr>
111
+ HTML
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,35 @@
1
+ module SolidQueueMonitor
2
+ class StatsPresenter < BasePresenter
3
+ def initialize(stats)
4
+ @stats = stats
5
+ end
6
+
7
+ def render
8
+ <<-HTML
9
+ <div class="stats-container">
10
+ <h3>Queue Statistics</h3>
11
+ <div class="stats">
12
+ #{generate_stat_card('Total Jobs', @stats[:total_jobs])}
13
+ #{generate_stat_card('Unique Queues', @stats[:unique_queues])}
14
+ #{generate_stat_card('Ready', @stats[:ready])}
15
+ #{generate_stat_card('Scheduled', @stats[:scheduled])}
16
+ #{generate_stat_card('Failed', @stats[:failed])}
17
+ #{generate_stat_card('Completed', @stats[:completed])}
18
+ #{generate_stat_card('Recurring', @stats[:recurring])}
19
+ </div>
20
+ </div>
21
+ HTML
22
+ end
23
+
24
+ private
25
+
26
+ def generate_stat_card(title, value)
27
+ <<-HTML
28
+ <div class="stat-card">
29
+ <h3>#{title}</h3>
30
+ <p>#{value}</p>
31
+ </div>
32
+ HTML
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,14 @@
1
+ module SolidQueueMonitor
2
+ class AuthenticationService
3
+ def self.authenticate(username, password)
4
+ return true unless SolidQueueMonitor.authentication_enabled
5
+
6
+ username == SolidQueueMonitor.username &&
7
+ password == SolidQueueMonitor.password
8
+ end
9
+
10
+ def self.authentication_required?
11
+ SolidQueueMonitor.authentication_enabled
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ module SolidQueueMonitor
2
+ class ExecuteJobService
3
+ def call(id)
4
+ execution = SolidQueue::ScheduledExecution.find(id)
5
+ move_to_ready_queue(execution)
6
+ end
7
+
8
+ def execute_many(ids)
9
+ SolidQueue::ScheduledExecution.where(id: ids).find_each do |execution|
10
+ move_to_ready_queue(execution)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def move_to_ready_queue(execution)
17
+ ActiveRecord::Base.transaction do
18
+ SolidQueue::ReadyExecution.create!(
19
+ job: execution.job,
20
+ queue_name: execution.queue_name,
21
+ priority: execution.priority
22
+ )
23
+
24
+ execution.destroy
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,88 @@
1
+ module SolidQueueMonitor
2
+ class HtmlGenerator
3
+ include Rails.application.routes.url_helpers
4
+ include SolidQueueMonitor::Engine.routes.url_helpers
5
+
6
+ def initialize(title:, content:, message: nil, message_type: nil)
7
+ @title = title
8
+ @content = content
9
+ @message = message
10
+ @message_type = message_type
11
+ end
12
+
13
+ def generate
14
+ <<-HTML
15
+ <!DOCTYPE html>
16
+ <html>
17
+ <head>
18
+ <title>Solid Queue Monitor - #{@title}</title>
19
+ #{generate_head}
20
+ </head>
21
+ <body>
22
+ #{generate_body}
23
+ </body>
24
+ </html>
25
+ HTML
26
+ end
27
+
28
+ private
29
+
30
+ def generate_head
31
+ <<-HTML
32
+ <meta charset="UTF-8">
33
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
34
+ <style>
35
+ #{SolidQueueMonitor::StylesheetGenerator.new.generate}
36
+ </style>
37
+ HTML
38
+ end
39
+
40
+ def generate_body
41
+ <<-HTML
42
+ #{render_message}
43
+ <div class="container">
44
+ #{generate_header}
45
+ <div class="section">
46
+ <h2>#{@title}</h2>
47
+ #{@content}
48
+ </div>
49
+ #{generate_footer}
50
+ </div>
51
+ HTML
52
+ end
53
+
54
+ def render_message
55
+ return '' unless @message
56
+ class_name = @message_type == 'success' ? 'message-success' : 'message-error'
57
+ "<div class='message #{class_name}'>#{@message}</div>"
58
+ end
59
+
60
+ def generate_header
61
+ <<-HTML
62
+ <header>
63
+ <h1>Solid Queue Monitor</h1>
64
+ <nav class="navigation">
65
+ <a href="#{root_path}" class="nav-link">Overview</a>
66
+ <a href="#{ready_jobs_path}" class="nav-link">Ready Jobs</a>
67
+ <a href="#{recurring_jobs_path}" class="nav-link">Recurring Jobs</a>
68
+ <a href="#{scheduled_jobs_path}" class="nav-link">Scheduled Jobs</a>
69
+ <a href="#{failed_jobs_path}" class="nav-link">Failed Jobs</a>
70
+ <a href="#{queues_path}" class="nav-link">Queues</a>
71
+ </nav>
72
+ </header>
73
+ HTML
74
+ end
75
+
76
+ def generate_footer
77
+ <<-HTML
78
+ <footer>
79
+ <p>Powered by Solid Queue Monitor</p>
80
+ </footer>
81
+ HTML
82
+ end
83
+
84
+ def default_url_options
85
+ { only_path: true }
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,31 @@
1
+ module SolidQueueMonitor
2
+ class PaginationService
3
+ def initialize(relation, page, per_page)
4
+ @relation = relation
5
+ @page = page
6
+ @per_page = per_page
7
+ end
8
+
9
+ def paginate
10
+ {
11
+ records: paginated_records,
12
+ total_pages: total_pages,
13
+ current_page: @page
14
+ }
15
+ end
16
+
17
+ private
18
+
19
+ def offset
20
+ (@page - 1) * @per_page
21
+ end
22
+
23
+ def total_pages
24
+ (@relation.count.to_f / @per_page).ceil
25
+ end
26
+
27
+ def paginated_records
28
+ @relation.limit(@per_page).offset(offset)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,15 @@
1
+ module SolidQueueMonitor
2
+ class StatsCalculator
3
+ def self.calculate
4
+ {
5
+ total_jobs: SolidQueue::Job.count,
6
+ unique_queues: SolidQueue::Job.distinct.count(:queue_name),
7
+ scheduled: SolidQueue::ScheduledExecution.count,
8
+ ready: SolidQueue::ReadyExecution.count,
9
+ failed: SolidQueue::FailedExecution.count,
10
+ completed: SolidQueue::Job.where.not(finished_at: nil).count,
11
+ recurring: SolidQueue::RecurringTask.count
12
+ }
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,14 @@
1
+ module SolidQueueMonitor
2
+ class StatusCalculator
3
+ def initialize(job)
4
+ @job = job
5
+ end
6
+
7
+ def calculate
8
+ return 'completed' if @job.finished_at.present?
9
+ return 'failed' if @job.failed?
10
+ return 'scheduled' if @job.scheduled_at&.future?
11
+ 'pending'
12
+ end
13
+ end
14
+ end