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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 21024cd4e55a7bc73fde9161c68186a293976fe9fe6149dde1e0c48c0638f1b0
4
+ data.tar.gz: b0bfa87b894001e9a14e8666a26073334302b34e89f9e723ed0266c9ced4cfd9
5
+ SHA512:
6
+ metadata.gz: 82e972e2209c95dcc4693f7a1bc0ac0794743f1ccd02405708730005164c3a652a648bb816f0eaf15ccae92da2ca0ca7d1498877502428fc9e6d6c78698f98fc
7
+ data.tar.gz: c695e22c0957ba4b3449c9a0fe46e503f6d32d4e2282bd52ee2d7271a13880e16829cae7b8c4ee74d2fb2422f44f4d4a50ba39e79b3c20d46d9907458d2e79ab
data/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # SolidQueueMonitor
2
+
3
+ A lightweight, zero-dependency web interface for monitoring Solid Queue jobs in Rails applications.
4
+
5
+ ## Key Advantages
6
+
7
+ - **Works in API-only Rails Applications**: Unlike other monitoring gems that require a full Rails application with asset pipeline or webpacker, SolidQueueMonitor works seamlessly in API-only Rails applications.
8
+ - **No External Dependencies**: No JavaScript frameworks, no CSS libraries, no additional gems required - just pure Rails.
9
+ - **Self-contained UI**: All HTML, CSS, and JavaScript are generated server-side, making deployment simple and reliable.
10
+ - **Minimal Footprint**: Adds minimal overhead to your application while providing powerful monitoring capabilities.
11
+
12
+ ## Features
13
+
14
+ - **Dashboard Overview**: Get a quick snapshot of your job queue with statistics and counts
15
+ - **Job Filtering**: Filter jobs by class name, queue name, and status
16
+ - **Job Management**: Execute scheduled jobs on demand
17
+ - **Failed Job Inspection**: View detailed error information for failed jobs
18
+ - **Queue Monitoring**: Track job distribution across different queues
19
+ - **Pagination**: Navigate through large job lists with ease
20
+ - **Optional Authentication**: Secure your dashboard with HTTP Basic Authentication
21
+ - **Responsive Design**: Works on desktop and mobile devices
22
+ - **Zero Dependencies**: No additional JavaScript libraries or frameworks required
23
+
24
+ ## Screenshots
25
+
26
+ ### Dashboard Overview
27
+
28
+ ![Dashboard Overview](screenshots/dashboard.png)
29
+
30
+ ### Recurring Jobs
31
+
32
+ ![Recurring Jobs](screenshots/recurring_jobs.png)
33
+
34
+ ## Installation
35
+
36
+ Add this line to your application's Gemfile:
37
+
38
+ ```ruby
39
+ gem 'solid_queue_monitor'
40
+ ```
41
+
42
+ Then execute:
43
+
44
+ ```bash
45
+ $ bundle install
46
+ ```
47
+
48
+ After bundling, run the generator:
49
+
50
+ ```bash
51
+ rails generate solid_queue_monitor:install
52
+ ```
53
+
54
+ This will:
55
+
56
+ 1. Create an initializer at `config/initializers/solid_queue_monitor.rb`
57
+ 2. Add required routes to your `config/routes.rb`
58
+
59
+ ## Configuration
60
+
61
+ You can configure Solid Queue Monitor by editing the initializer:
62
+
63
+ ```ruby
64
+ # config/initializers/solid_queue_monitor.rb
65
+ SolidQueueMonitor.setup do |config|
66
+ # Enable or disable authentication
67
+ # By default, authentication is disabled for ease of setup
68
+ config.authentication_enabled = false
69
+
70
+ # Set the username for HTTP Basic Authentication (only used if authentication is enabled)
71
+ config.username = 'admin'
72
+
73
+ # Set the password for HTTP Basic Authentication (only used if authentication is enabled)
74
+ config.password = 'password'
75
+
76
+ # Number of jobs to display per page
77
+ config.jobs_per_page = 25
78
+ end
79
+ ```
80
+
81
+ ### Authentication
82
+
83
+ By default, Solid Queue Monitor does not require authentication to access the dashboard. This makes it easy to get started in development environments.
84
+
85
+ For production environments, it's strongly recommended to enable authentication:
86
+
87
+ 1. **Enable authentication**: Set `config.authentication_enabled = true` in the initializer
88
+ 2. **Configure secure credentials**: Set `username` and `password` to strong values in the initializer
89
+
90
+ ## Usage
91
+
92
+ After installation, visit `/solid_queue` in your browser to access the dashboard.
93
+
94
+ The dashboard provides several views:
95
+
96
+ - **Overview**: Shows statistics and recent jobs
97
+ - **Ready Jobs**: Jobs that are ready to be executed
98
+ - **Scheduled Jobs**: Jobs scheduled for future execution
99
+ - **Failed Jobs**: Jobs that have failed with error details
100
+ - **Queues**: Distribution of jobs across different queues
101
+
102
+ ### API-only Applications
103
+
104
+ For API-only Rails applications, SolidQueueMonitor works out of the box without requiring you to enable the asset pipeline or webpacker. This makes it an ideal choice for monitoring background jobs in modern API-based architectures.
105
+
106
+ ## Contributing
107
+
108
+ Contributions are welcome! Here's how you can contribute:
109
+
110
+ 1. Fork the repository
111
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
112
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
113
+ 4. Push to the branch (`git push origin my-new-feature`)
114
+ 5. Create a new Pull Request
115
+
116
+ Please make sure to update tests as appropriate and follow the existing code style.
117
+
118
+ ### Development
119
+
120
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
121
+
122
+ ## License
123
+
124
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
125
+
126
+ ## Code of Conduct
127
+
128
+ Everyone interacting in the SolidQueueMonitor project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/yourusername/solid_queue_monitor/blob/main/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
9
+
10
+ namespace :db do
11
+ task :setup do
12
+ require 'fileutils'
13
+ FileUtils.mkdir_p 'spec/dummy/db'
14
+ system("cd spec/dummy && bundle exec rails db:environment:set RAILS_ENV=test")
15
+ system("cd spec/dummy && bundle exec rails db:schema:load RAILS_ENV=test")
16
+ end
17
+ end
18
+
19
+ task :prepare_test_env do
20
+ Rake::Task["db:setup"].invoke
21
+ end
22
+
23
+ task :spec => :prepare_test_env
@@ -0,0 +1,250 @@
1
+ module SolidQueueMonitor
2
+ class MonitorController < ActionController::Base
3
+ include ActionController::HttpAuthentication::Basic::ControllerMethods
4
+
5
+ before_action :authenticate, if: -> { SolidQueueMonitor::AuthenticationService.authentication_required? }
6
+ layout false
7
+ skip_before_action :verify_authenticity_token, only: [:execute_jobs]
8
+
9
+ def index
10
+ @stats = SolidQueueMonitor::StatsCalculator.calculate
11
+
12
+ # Get all jobs with pagination
13
+ @recent_jobs = paginate(filter_jobs(SolidQueue::Job.order(created_at: :desc)))
14
+
15
+ # Preload failed job information
16
+ preload_job_statuses(@recent_jobs[:records])
17
+
18
+ render_page('Overview', generate_overview_content)
19
+ end
20
+
21
+ def ready_jobs
22
+ base_query = SolidQueue::ReadyExecution.includes(:job).order(created_at: :desc)
23
+ @ready_jobs = paginate(filter_ready_jobs(base_query))
24
+ render_page('Ready Jobs', SolidQueueMonitor::ReadyJobsPresenter.new(@ready_jobs[:records],
25
+ current_page: @ready_jobs[:current_page],
26
+ total_pages: @ready_jobs[:total_pages],
27
+ filters: filter_params
28
+ ).render)
29
+ end
30
+
31
+ def scheduled_jobs
32
+ base_query = SolidQueue::ScheduledExecution.includes(:job).order(scheduled_at: :asc)
33
+ @scheduled_jobs = paginate(filter_scheduled_jobs(base_query))
34
+ render_page('Scheduled Jobs', SolidQueueMonitor::ScheduledJobsPresenter.new(@scheduled_jobs[:records],
35
+ current_page: @scheduled_jobs[:current_page],
36
+ total_pages: @scheduled_jobs[:total_pages],
37
+ filters: filter_params
38
+ ).render)
39
+ end
40
+
41
+ def recurring_jobs
42
+ base_query = filter_recurring_jobs(SolidQueue::RecurringTask.order(:key))
43
+ @recurring_jobs = paginate(base_query)
44
+ render_page('Recurring Jobs', SolidQueueMonitor::RecurringJobsPresenter.new(@recurring_jobs[:records],
45
+ current_page: @recurring_jobs[:current_page],
46
+ total_pages: @recurring_jobs[:total_pages],
47
+ filters: filter_params
48
+ ).render)
49
+ end
50
+
51
+ def failed_jobs
52
+ base_query = SolidQueue::FailedExecution.includes(:job).order(created_at: :desc)
53
+ @failed_jobs = paginate(filter_failed_jobs(base_query))
54
+ render_page('Failed Jobs', SolidQueueMonitor::FailedJobsPresenter.new(@failed_jobs[:records],
55
+ current_page: @failed_jobs[:current_page],
56
+ total_pages: @failed_jobs[:total_pages],
57
+ filters: filter_params
58
+ ).render)
59
+ end
60
+
61
+ def queues
62
+ @queues = SolidQueue::Job.group(:queue_name)
63
+ .select('queue_name, COUNT(*) as job_count')
64
+ .order('job_count DESC')
65
+ render_page('Queues', SolidQueueMonitor::QueuesPresenter.new(@queues).render)
66
+ end
67
+
68
+ def execute_jobs
69
+ if params[:job_ids].present?
70
+ SolidQueueMonitor::ExecuteJobService.new.execute_many(params[:job_ids])
71
+ redirect_url = "#{scheduled_jobs_path}?message=Selected jobs moved to ready queue&message_type=success"
72
+ else
73
+ redirect_url = "#{scheduled_jobs_path}?message=No jobs selected&message_type=error"
74
+ end
75
+ redirect_to redirect_url
76
+ end
77
+
78
+ private
79
+
80
+ def authenticate
81
+ authenticate_or_request_with_http_basic do |username, password|
82
+ SolidQueueMonitor::AuthenticationService.authenticate(username, password)
83
+ end
84
+ end
85
+
86
+ def paginate(relation)
87
+ PaginationService.new(relation, current_page, per_page).paginate
88
+ end
89
+
90
+ def render_page(title, content)
91
+ html = SolidQueueMonitor::HtmlGenerator.new(
92
+ title: title,
93
+ content: content,
94
+ message: params[:notice] || params[:alert],
95
+ message_type: params[:notice] ? 'success' : 'error'
96
+ ).generate
97
+
98
+ render html: html.html_safe
99
+ end
100
+
101
+ def generate_overview_content
102
+ SolidQueueMonitor::StatsPresenter.new(@stats).render +
103
+ SolidQueueMonitor::JobsPresenter.new(@recent_jobs[:records],
104
+ current_page: @recent_jobs[:current_page],
105
+ total_pages: @recent_jobs[:total_pages],
106
+ filters: filter_params
107
+ ).render
108
+ end
109
+
110
+ def current_page
111
+ (params[:page] || 1).to_i
112
+ end
113
+
114
+ def per_page
115
+ SolidQueueMonitor.jobs_per_page
116
+ end
117
+
118
+ # Preload job statuses to avoid N+1 queries
119
+ def preload_job_statuses(jobs)
120
+ return if jobs.empty?
121
+
122
+ # Get all job IDs
123
+ job_ids = jobs.map(&:id)
124
+
125
+ # Find all failed jobs in a single query
126
+ failed_job_ids = SolidQueue::FailedExecution.where(job_id: job_ids).pluck(:job_id)
127
+
128
+ # Find all scheduled jobs in a single query
129
+ scheduled_job_ids = SolidQueue::ScheduledExecution.where(job_id: job_ids).pluck(:job_id)
130
+
131
+ # Attach the status information to each job
132
+ jobs.each do |job|
133
+ job.instance_variable_set(:@failed, failed_job_ids.include?(job.id))
134
+ job.instance_variable_set(:@scheduled, scheduled_job_ids.include?(job.id))
135
+ end
136
+
137
+ # Define the method to check if a job is failed
138
+ SolidQueue::Job.class_eval do
139
+ def failed?
140
+ if instance_variable_defined?(:@failed)
141
+ @failed
142
+ else
143
+ SolidQueue::FailedExecution.exists?(job_id: id)
144
+ end
145
+ end
146
+
147
+ def scheduled?
148
+ if instance_variable_defined?(:@scheduled)
149
+ @scheduled
150
+ else
151
+ SolidQueue::ScheduledExecution.exists?(job_id: id)
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ def filter_jobs(relation)
158
+ relation = relation.where("class_name LIKE ?", "%#{params[:class_name]}%") if params[:class_name].present?
159
+ relation = relation.where("queue_name LIKE ?", "%#{params[:queue_name]}%") if params[:queue_name].present?
160
+
161
+ if params[:status].present?
162
+ case params[:status]
163
+ when 'completed'
164
+ relation = relation.where.not(finished_at: nil)
165
+ when 'failed'
166
+ failed_job_ids = SolidQueue::FailedExecution.pluck(:job_id)
167
+ relation = relation.where(id: failed_job_ids)
168
+ when 'scheduled'
169
+ scheduled_job_ids = SolidQueue::ScheduledExecution.pluck(:job_id)
170
+ relation = relation.where(id: scheduled_job_ids)
171
+ when 'pending'
172
+ # Pending jobs are those that are not completed, failed, or scheduled
173
+ failed_job_ids = SolidQueue::FailedExecution.pluck(:job_id)
174
+ scheduled_job_ids = SolidQueue::ScheduledExecution.pluck(:job_id)
175
+ relation = relation.where(finished_at: nil)
176
+ .where.not(id: failed_job_ids + scheduled_job_ids)
177
+ end
178
+ end
179
+
180
+ relation
181
+ end
182
+
183
+ def filter_ready_jobs(relation)
184
+ return relation unless params[:class_name].present? || params[:queue_name].present?
185
+
186
+ if params[:class_name].present?
187
+ job_ids = SolidQueue::Job.where("class_name LIKE ?", "%#{params[:class_name]}%").pluck(:id)
188
+ relation = relation.where(job_id: job_ids)
189
+ end
190
+
191
+ if params[:queue_name].present?
192
+ relation = relation.where("queue_name LIKE ?", "%#{params[:queue_name]}%")
193
+ end
194
+
195
+ relation
196
+ end
197
+
198
+ def filter_scheduled_jobs(relation)
199
+ return relation unless params[:class_name].present? || params[:queue_name].present?
200
+
201
+ if params[:class_name].present?
202
+ job_ids = SolidQueue::Job.where("class_name LIKE ?", "%#{params[:class_name]}%").pluck(:id)
203
+ relation = relation.where(job_id: job_ids)
204
+ end
205
+
206
+ if params[:queue_name].present?
207
+ relation = relation.where("queue_name LIKE ?", "%#{params[:queue_name]}%")
208
+ end
209
+
210
+ relation
211
+ end
212
+
213
+ def filter_recurring_jobs(relation)
214
+ return relation unless params[:class_name].present? || params[:queue_name].present?
215
+
216
+ if params[:class_name].present?
217
+ relation = relation.where("class_name LIKE ?", "%#{params[:class_name]}%")
218
+ end
219
+
220
+ if params[:queue_name].present?
221
+ relation = relation.where("queue_name LIKE ?", "%#{params[:queue_name]}%")
222
+ end
223
+
224
+ relation
225
+ end
226
+
227
+ def filter_failed_jobs(relation)
228
+ return relation unless params[:class_name].present? || params[:queue_name].present?
229
+
230
+ if params[:class_name].present?
231
+ job_ids = SolidQueue::Job.where("class_name LIKE ?", "%#{params[:class_name]}%").pluck(:id)
232
+ relation = relation.where(job_id: job_ids)
233
+ end
234
+
235
+ if params[:queue_name].present?
236
+ relation = relation.where("queue_name LIKE ?", "%#{params[:queue_name]}%")
237
+ end
238
+
239
+ relation
240
+ end
241
+
242
+ def filter_params
243
+ {
244
+ class_name: params[:class_name],
245
+ queue_name: params[:queue_name],
246
+ status: params[:status]
247
+ }
248
+ end
249
+ end
250
+ end
@@ -0,0 +1,136 @@
1
+ module SolidQueueMonitor
2
+ class BasePresenter
3
+ include ActionView::Helpers::DateHelper
4
+ include ActionView::Helpers::TextHelper
5
+ include Rails.application.routes.url_helpers
6
+ include SolidQueueMonitor::Engine.routes.url_helpers
7
+
8
+ def default_url_options
9
+ { only_path: true }
10
+ end
11
+
12
+ def section_wrapper(title, content)
13
+ <<-HTML
14
+ <div class="section-wrapper">
15
+ <div class="section">
16
+ #{content}
17
+ </div>
18
+ </div>
19
+ HTML
20
+ end
21
+
22
+ def generate_pagination(current_page, total_pages)
23
+ return '' if total_pages <= 1
24
+
25
+ links = []
26
+
27
+ # Previous page link
28
+ if current_page > 1
29
+ links << "<a href='?page=#{current_page - 1}#{query_params}' class='pagination-link'>&laquo; Previous</a>"
30
+ else
31
+ links << "<span class='pagination-link disabled'>&laquo; Previous</span>"
32
+ end
33
+
34
+ # Page number links
35
+ if total_pages <= 7
36
+ # Show all pages if there are 7 or fewer
37
+ (1..total_pages).each do |page|
38
+ links << page_link(page, current_page)
39
+ end
40
+ else
41
+ # Show first page, last page, and pages around current
42
+ links << page_link(1, current_page)
43
+
44
+ if current_page > 3
45
+ links << "<span class='pagination-gap'>...</span>"
46
+ end
47
+
48
+ start_page = [current_page - 1, 2].max
49
+ end_page = [current_page + 1, total_pages - 1].min
50
+
51
+ (start_page..end_page).each do |page|
52
+ links << page_link(page, current_page)
53
+ end
54
+
55
+ if current_page < total_pages - 2
56
+ links << "<span class='pagination-gap'>...</span>"
57
+ end
58
+
59
+ links << page_link(total_pages, current_page)
60
+ end
61
+
62
+ # Next page link
63
+ if current_page < total_pages
64
+ links << "<a href='?page=#{current_page + 1}#{query_params}' class='pagination-link'>Next &raquo;</a>"
65
+ else
66
+ links << "<span class='pagination-link disabled'>Next &raquo;</span>"
67
+ end
68
+
69
+ <<-HTML
70
+ <div class="pagination">
71
+ #{links.join}
72
+ </div>
73
+ HTML
74
+ end
75
+
76
+ def calculate_visible_pages(current_page, total_pages)
77
+ if total_pages <= 7
78
+ (1..total_pages).to_a
79
+ else
80
+ case current_page
81
+ when 1..3
82
+ [1, 2, 3, 4, :gap, total_pages]
83
+ when (total_pages - 2)..total_pages
84
+ [1, :gap, total_pages - 3, total_pages - 2, total_pages - 1, total_pages]
85
+ else
86
+ [1, :gap, current_page - 1, current_page, current_page + 1, :gap, total_pages]
87
+ end
88
+ end
89
+ end
90
+
91
+ def format_datetime(datetime)
92
+ return '-' unless datetime
93
+ datetime.strftime('%Y-%m-%d %H:%M:%S')
94
+ end
95
+
96
+ def format_arguments(arguments)
97
+ return '-' unless arguments.present?
98
+
99
+ if arguments.is_a?(Array) && arguments.length == 1 && arguments[0].is_a?(Hash)
100
+ # Handle ActiveJob-style arguments
101
+ format_hash(arguments[0])
102
+ else
103
+ "<code>#{arguments.to_json}</code>"
104
+ end
105
+ end
106
+
107
+ def format_hash(hash)
108
+ return '-' unless hash.present?
109
+
110
+ formatted = hash.map do |key, value|
111
+ "<strong>#{key}:</strong> #{value.to_s.truncate(50)}"
112
+ end.join(', ')
113
+
114
+ "<code>#{formatted}</code>"
115
+ end
116
+
117
+ private
118
+
119
+ def page_link(page, current_page)
120
+ if page == current_page
121
+ "<span class='pagination-current'>#{page}</span>"
122
+ else
123
+ "<a href='?page=#{page}#{query_params}' class='pagination-link'>#{page}</a>"
124
+ end
125
+ end
126
+
127
+ def query_params
128
+ params = []
129
+ params << "class_name=#{CGI.escape(@filters[:class_name])}" if @filters && @filters[:class_name].present?
130
+ params << "queue_name=#{CGI.escape(@filters[:queue_name])}" if @filters && @filters[:queue_name].present?
131
+ params << "status=#{CGI.escape(@filters[:status])}" if @filters && @filters[:status].present?
132
+
133
+ params.empty? ? '' : "&#{params.join('&')}"
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,70 @@
1
+ module SolidQueueMonitor
2
+ class FailedJobsPresenter < 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('Failed 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="#{failed_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>Error</th>
47
+ <th>Failed At</th>
48
+ <th>Arguments</th>
49
+ </tr>
50
+ </thead>
51
+ <tbody>
52
+ #{@jobs.map { |execution| generate_row(execution) }.join}
53
+ </tbody>
54
+ </table>
55
+ </div>
56
+ HTML
57
+ end
58
+
59
+ def generate_row(execution)
60
+ <<-HTML
61
+ <tr>
62
+ <td>#{execution.job.class_name}</td>
63
+ <td class="error-message">#{execution.error['message']}</td>
64
+ <td>#{format_datetime(execution.created_at)}</td>
65
+ <td>#{format_arguments(execution.job.arguments)}</td>
66
+ </tr>
67
+ HTML
68
+ end
69
+ end
70
+ end