solid_queue_monitor 1.2.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -1
  3. data/app/assets/javascripts/solid_queue_monitor/application.js +393 -0
  4. data/app/{services/solid_queue_monitor/stylesheet_generator.rb → assets/stylesheets/solid_queue_monitor/application.css} +80 -12
  5. data/app/controllers/solid_queue_monitor/application_controller.rb +2 -2
  6. data/app/controllers/solid_queue_monitor/assets_controller.rb +52 -0
  7. data/app/controllers/solid_queue_monitor/base_controller.rb +0 -28
  8. data/app/controllers/solid_queue_monitor/failed_jobs_controller.rb +3 -6
  9. data/app/controllers/solid_queue_monitor/in_progress_jobs_controller.rb +3 -6
  10. data/app/controllers/solid_queue_monitor/jobs_controller.rb +3 -6
  11. data/app/controllers/solid_queue_monitor/overview_controller.rb +3 -12
  12. data/app/controllers/solid_queue_monitor/queues_controller.rb +4 -18
  13. data/app/controllers/solid_queue_monitor/ready_jobs_controller.rb +3 -6
  14. data/app/controllers/solid_queue_monitor/recurring_jobs_controller.rb +3 -6
  15. data/app/controllers/solid_queue_monitor/scheduled_jobs_controller.rb +3 -6
  16. data/app/controllers/solid_queue_monitor/search_controller.rb +3 -4
  17. data/app/controllers/solid_queue_monitor/workers_controller.rb +24 -8
  18. data/app/helpers/solid_queue_monitor/application_helper.rb +46 -0
  19. data/app/helpers/solid_queue_monitor/chart_helper.rb +293 -0
  20. data/app/helpers/solid_queue_monitor/job_details_helper.rb +66 -0
  21. data/app/helpers/solid_queue_monitor/jobs_helper.rb +134 -0
  22. data/app/helpers/solid_queue_monitor/pagination_helper.rb +23 -0
  23. data/app/helpers/solid_queue_monitor/sort_helper.rb +30 -0
  24. data/app/helpers/solid_queue_monitor/workers_helper.rb +88 -0
  25. data/app/services/solid_queue_monitor/asset_cache.rb +56 -0
  26. data/app/services/solid_queue_monitor/chart_data_service.rb +2 -2
  27. data/app/views/layouts/solid_queue_monitor/application.html.erb +25 -0
  28. data/app/views/solid_queue_monitor/failed_jobs/_row.html.erb +26 -0
  29. data/app/views/solid_queue_monitor/failed_jobs/index.html.erb +38 -0
  30. data/app/views/solid_queue_monitor/in_progress_jobs/_row.html.erb +13 -0
  31. data/app/views/solid_queue_monitor/in_progress_jobs/index.html.erb +25 -0
  32. data/app/views/solid_queue_monitor/jobs/_arguments.html.erb +9 -0
  33. data/app/views/solid_queue_monitor/jobs/_error.html.erb +26 -0
  34. data/app/views/solid_queue_monitor/jobs/_execution_history.html.erb +25 -0
  35. data/app/views/solid_queue_monitor/jobs/_header.html.erb +37 -0
  36. data/app/views/solid_queue_monitor/jobs/_metadata.html.erb +22 -0
  37. data/app/views/solid_queue_monitor/jobs/_raw_data.html.erb +11 -0
  38. data/app/views/solid_queue_monitor/jobs/_timeline.html.erb +29 -0
  39. data/app/views/solid_queue_monitor/jobs/_timing.html.erb +9 -0
  40. data/app/views/solid_queue_monitor/jobs/_worker.html.erb +12 -0
  41. data/app/views/solid_queue_monitor/jobs/show.html.erb +22 -0
  42. data/app/views/solid_queue_monitor/overview/_chart.html.erb +1 -0
  43. data/app/views/solid_queue_monitor/overview/_recent_job_row.html.erb +26 -0
  44. data/app/views/solid_queue_monitor/overview/_recent_jobs.html.erb +31 -0
  45. data/app/views/solid_queue_monitor/overview/_stat_card.html.erb +4 -0
  46. data/app/views/solid_queue_monitor/overview/_stats.html.erb +11 -0
  47. data/app/views/solid_queue_monitor/overview/index.html.erb +9 -0
  48. data/app/views/solid_queue_monitor/queues/_job_row.html.erb +26 -0
  49. data/app/views/solid_queue_monitor/queues/_row.html.erb +33 -0
  50. data/app/views/solid_queue_monitor/queues/index.html.erb +18 -0
  51. data/app/views/solid_queue_monitor/queues/show.html.erb +63 -0
  52. data/app/views/solid_queue_monitor/ready_jobs/_row.html.erb +7 -0
  53. data/app/views/solid_queue_monitor/ready_jobs/index.html.erb +25 -0
  54. data/app/views/solid_queue_monitor/recurring_jobs/_row.html.erb +8 -0
  55. data/app/views/solid_queue_monitor/recurring_jobs/index.html.erb +26 -0
  56. data/app/views/solid_queue_monitor/scheduled_jobs/_row.html.erb +7 -0
  57. data/app/views/solid_queue_monitor/scheduled_jobs/index.html.erb +36 -0
  58. data/app/views/solid_queue_monitor/search/_completed_row.html.erb +6 -0
  59. data/app/views/solid_queue_monitor/search/_failed_row.html.erb +6 -0
  60. data/app/views/solid_queue_monitor/search/_job_row.html.erb +9 -0
  61. data/app/views/solid_queue_monitor/search/_recurring_row.html.erb +6 -0
  62. data/app/views/solid_queue_monitor/search/_section.html.erb +25 -0
  63. data/app/views/solid_queue_monitor/search/index.html.erb +23 -0
  64. data/app/views/solid_queue_monitor/shared/_filters.html.erb +48 -0
  65. data/app/views/solid_queue_monitor/shared/_flash.html.erb +17 -0
  66. data/app/views/solid_queue_monitor/shared/_footer.html.erb +3 -0
  67. data/app/views/solid_queue_monitor/shared/_header.html.erb +81 -0
  68. data/app/views/solid_queue_monitor/shared/_jobs_table.html.erb +20 -0
  69. data/app/views/solid_queue_monitor/shared/_pagination.html.erb +25 -0
  70. data/app/views/solid_queue_monitor/workers/_row.html.erb +22 -0
  71. data/app/views/solid_queue_monitor/workers/index.html.erb +82 -0
  72. data/config/routes.rb +6 -1
  73. data/lib/solid_queue_monitor/engine.rb +2 -0
  74. data/lib/solid_queue_monitor/version.rb +1 -1
  75. metadata +57 -17
  76. data/app/presenters/solid_queue_monitor/base_presenter.rb +0 -211
  77. data/app/presenters/solid_queue_monitor/failed_jobs_presenter.rb +0 -312
  78. data/app/presenters/solid_queue_monitor/in_progress_jobs_presenter.rb +0 -84
  79. data/app/presenters/solid_queue_monitor/job_details_presenter.rb +0 -696
  80. data/app/presenters/solid_queue_monitor/jobs_presenter.rb +0 -144
  81. data/app/presenters/solid_queue_monitor/queue_details_presenter.rb +0 -195
  82. data/app/presenters/solid_queue_monitor/queues_presenter.rb +0 -89
  83. data/app/presenters/solid_queue_monitor/ready_jobs_presenter.rb +0 -81
  84. data/app/presenters/solid_queue_monitor/recurring_jobs_presenter.rb +0 -81
  85. data/app/presenters/solid_queue_monitor/scheduled_jobs_presenter.rb +0 -173
  86. data/app/presenters/solid_queue_monitor/search_results_presenter.rb +0 -190
  87. data/app/presenters/solid_queue_monitor/stats_presenter.rb +0 -36
  88. data/app/presenters/solid_queue_monitor/workers_presenter.rb +0 -320
  89. data/app/services/solid_queue_monitor/chart_presenter.rb +0 -239
  90. data/app/services/solid_queue_monitor/html_generator.rb +0 -401
@@ -0,0 +1,12 @@
1
+ <% process = @claimed_execution.instance_variable_get(:@process) %>
2
+ <% if process %>
3
+ <div class="job-section">
4
+ <h3 class="section-title">Worker</h3>
5
+ <div class="details-grid">
6
+ <div class="detail-row"><span class="detail-label">Hostname</span><span class="detail-value"><%= process.hostname %></span></div>
7
+ <div class="detail-row"><span class="detail-label">PID</span><span class="detail-value"><%= process.pid %></span></div>
8
+ <div class="detail-row"><span class="detail-label">Process Type</span><span class="detail-value"><%= process.kind %></span></div>
9
+ <div class="detail-row"><span class="detail-label">Started At</span><span class="detail-value"><%= format_datetime(@claimed_execution.created_at) %></span></div>
10
+ </div>
11
+ </div>
12
+ <% end %>
@@ -0,0 +1,22 @@
1
+ <% content_for :title, "Job ##{@job.id}" %>
2
+
3
+ <div class="job-details-page">
4
+ <div class="job-back-link">
5
+ <%= link_to @back_path, class: 'back-link' do %>
6
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
7
+ <path d="M19 12H5M12 19l-7-7 7-7"/>
8
+ </svg>
9
+ Back
10
+ <% end %>
11
+ </div>
12
+
13
+ <%= render 'header' %>
14
+ <%= render 'timeline' %>
15
+ <%= render 'timing' %>
16
+ <%= render 'error' if @failed_execution %>
17
+ <%= render 'arguments' %>
18
+ <%= render 'metadata' %>
19
+ <%= render 'worker' if @claimed_execution %>
20
+ <%= render 'execution_history' %>
21
+ <%= render 'raw_data' %>
22
+ </div>
@@ -0,0 +1 @@
1
+ <%= render_chart(data: @chart_data, time_range: @time_range) %>
@@ -0,0 +1,26 @@
1
+ <% failed_execution = SolidQueue::FailedExecution.find_by(job_id: job.id) if job_status(job) == 'failed' %>
2
+ <tr>
3
+ <td><%= link_to job.id, job_path(job), class: 'job-class-link' %></td>
4
+ <td><%= link_to job.class_name, job_path(job), class: 'job-class-link' %></td>
5
+ <td><%= queue_link(job.queue_name) %></td>
6
+ <td><%= format_arguments(job.arguments) %></td>
7
+ <td><%= job_status_badge(job) %></td>
8
+ <td><%= format_datetime(job.created_at) %></td>
9
+ <td class="actions-cell">
10
+ <% if failed_execution %>
11
+ <div class="job-actions">
12
+ <form method="post" action="<%= retry_failed_job_path(id: failed_execution.id) %>" class="inline-form">
13
+ <input type="hidden" name="redirect_to" value="<%= root_path %>">
14
+ <button type="submit" class="action-button retry-button">Retry</button>
15
+ </form>
16
+ <form method="post"
17
+ action="<%= discard_failed_job_path(id: failed_execution.id) %>"
18
+ class="inline-form"
19
+ data-confirm="Are you sure you want to discard this job?">
20
+ <input type="hidden" name="redirect_to" value="<%= root_path %>">
21
+ <button type="submit" class="action-button discard-button">Discard</button>
22
+ </form>
23
+ </div>
24
+ <% end %>
25
+ </td>
26
+ </tr>
@@ -0,0 +1,31 @@
1
+ <div class="section-wrapper">
2
+ <div class="section">
3
+ <h3>Recent Jobs</h3>
4
+
5
+ <%= render 'solid_queue_monitor/shared/filters',
6
+ action_path: root_path,
7
+ filters: @filters,
8
+ show_status: true %>
9
+
10
+ <% columns = [
11
+ { sort_key: nil, label: 'ID' },
12
+ { sort_key: :class_name, label: 'Job' },
13
+ { sort_key: :queue_name, label: 'Queue' },
14
+ { sort_key: nil, label: 'Arguments' },
15
+ { sort_key: nil, label: 'Status' },
16
+ { sort_key: :created_at, label: 'Created At' },
17
+ { sort_key: nil, label: 'Actions' }
18
+ ] %>
19
+
20
+ <%= render 'solid_queue_monitor/shared/jobs_table',
21
+ jobs: @recent_jobs[:records],
22
+ columns: columns,
23
+ sort: @sort,
24
+ filters: @filters,
25
+ row_partial: 'solid_queue_monitor/overview/recent_job_row' %>
26
+
27
+ <%= render 'solid_queue_monitor/shared/pagination',
28
+ current_page: @recent_jobs[:current_page],
29
+ total_pages: @recent_jobs[:total_pages] %>
30
+ </div>
31
+ </div>
@@ -0,0 +1,4 @@
1
+ <div class="stat-card">
2
+ <h3><%= title %></h3>
3
+ <p><%= value %></p>
4
+ </div>
@@ -0,0 +1,11 @@
1
+ <div class="stats-container">
2
+ <h3>Queue Statistics</h3>
3
+ <div class="stats">
4
+ <%= render 'stat_card', title: 'Active Jobs', value: @stats[:active_jobs] %>
5
+ <%= render 'stat_card', title: 'Ready', value: @stats[:ready] %>
6
+ <%= render 'stat_card', title: 'In Progress', value: @stats[:in_progress] %>
7
+ <%= render 'stat_card', title: 'Scheduled', value: @stats[:scheduled] %>
8
+ <%= render 'stat_card', title: 'Recurring', value: @stats[:recurring] %>
9
+ <%= render 'stat_card', title: 'Failed', value: @stats[:failed] %>
10
+ </div>
11
+ </div>
@@ -0,0 +1,9 @@
1
+ <% content_for :title, 'Overview' %>
2
+
3
+ <%= render 'stats' %>
4
+
5
+ <% if SolidQueueMonitor.show_chart && @chart_data.present? %>
6
+ <%= render 'chart' %>
7
+ <% end %>
8
+
9
+ <%= render 'recent_jobs' %>
@@ -0,0 +1,26 @@
1
+ <% status = job_status(job) %>
2
+ <% failed_execution = SolidQueue::FailedExecution.find_by(job_id: job.id) if status == 'failed' %>
3
+ <tr>
4
+ <td><%= link_to job.id, job_path(job), class: 'job-class-link' %></td>
5
+ <td><%= link_to job.class_name, job_path(job), class: 'job-class-link' %></td>
6
+ <td><%= format_arguments(job.arguments) %></td>
7
+ <td><%= job_status_badge(job) %></td>
8
+ <td><%= format_datetime(job.created_at) %></td>
9
+ <td class="actions-cell">
10
+ <% if failed_execution %>
11
+ <div class="job-actions">
12
+ <form method="post" action="<%= retry_failed_job_path(id: failed_execution.id) %>" class="inline-form">
13
+ <input type="hidden" name="redirect_to" value="<%= queue_details_path(queue_name: @queue_name) %>">
14
+ <button type="submit" class="action-button retry-button">Retry</button>
15
+ </form>
16
+ <form method="post"
17
+ action="<%= discard_failed_job_path(id: failed_execution.id) %>"
18
+ class="inline-form"
19
+ data-confirm="Are you sure you want to discard this job?">
20
+ <input type="hidden" name="redirect_to" value="<%= queue_details_path(queue_name: @queue_name) %>">
21
+ <button type="submit" class="action-button discard-button">Discard</button>
22
+ </form>
23
+ </div>
24
+ <% end %>
25
+ </td>
26
+ </tr>
@@ -0,0 +1,33 @@
1
+ <% queue_name = job.queue_name || 'default' %>
2
+ <% paused = @paused_queues.include?(queue_name) %>
3
+
4
+ <tr class="<%= 'queue-paused' if paused %>">
5
+ <td><%= queue_link(queue_name) %></td>
6
+ <td>
7
+ <% if paused %>
8
+ <span class="status-badge status-paused">Paused</span>
9
+ <% else %>
10
+ <span class="status-badge status-active">Active</span>
11
+ <% end %>
12
+ </td>
13
+ <td><%= job.job_count %></td>
14
+ <td><%= @queue_stats.dig(:ready, queue_name) || 0 %></td>
15
+ <td><%= @queue_stats.dig(:scheduled, queue_name) || 0 %></td>
16
+ <td><%= @queue_stats.dig(:failed, queue_name) || 0 %></td>
17
+ <td class="actions-cell">
18
+ <% if paused %>
19
+ <form action="<%= resume_queue_path %>" method="post" class="inline-form">
20
+ <input type="hidden" name="queue_name" value="<%= queue_name %>">
21
+ <button type="submit" class="action-button resume-button" title="Resume queue processing">Resume</button>
22
+ </form>
23
+ <% else %>
24
+ <form action="<%= pause_queue_path %>"
25
+ method="post"
26
+ class="inline-form"
27
+ data-confirm="Are you sure you want to pause the <%= queue_name %> queue? Workers will stop processing jobs from this queue.">
28
+ <input type="hidden" name="queue_name" value="<%= queue_name %>">
29
+ <button type="submit" class="action-button pause-button" title="Pause queue processing">Pause</button>
30
+ </form>
31
+ <% end %>
32
+ </td>
33
+ </tr>
@@ -0,0 +1,18 @@
1
+ <% content_for :title, 'Queues' %>
2
+
3
+ <% columns = [
4
+ { sort_key: :queue_name, label: 'Queue Name' },
5
+ { sort_key: nil, label: 'Status' },
6
+ { sort_key: :job_count, label: 'Total Jobs' },
7
+ { sort_key: nil, label: 'Ready Jobs' },
8
+ { sort_key: nil, label: 'Scheduled Jobs' },
9
+ { sort_key: nil, label: 'Failed Jobs' },
10
+ { sort_key: nil, label: 'Actions' }
11
+ ] %>
12
+
13
+ <%= render 'solid_queue_monitor/shared/jobs_table',
14
+ jobs: @queues,
15
+ columns: columns,
16
+ sort: @sort,
17
+ filters: {},
18
+ row_partial: 'solid_queue_monitor/queues/row' %>
@@ -0,0 +1,63 @@
1
+ <% content_for :title, "Queue: #{@queue_name}" %>
2
+
3
+ <div class="section-header-row">
4
+ <div class="section-header-left">
5
+ <% if @paused %>
6
+ <span class="status-badge status-paused">Paused</span>
7
+ <% else %>
8
+ <span class="status-badge status-active">Active</span>
9
+ <% end %>
10
+ </div>
11
+ <div class="section-header-right">
12
+ <% if @paused %>
13
+ <form action="<%= resume_queue_path %>" method="post" class="inline-form">
14
+ <input type="hidden" name="queue_name" value="<%= @queue_name %>">
15
+ <input type="hidden" name="redirect_to" value="<%= queue_details_path(queue_name: @queue_name) %>">
16
+ <button type="submit" class="action-button resume-button">Resume Queue</button>
17
+ </form>
18
+ <% else %>
19
+ <form action="<%= pause_queue_path %>" method="post" class="inline-form" data-confirm="Are you sure you want to pause this queue?">
20
+ <input type="hidden" name="queue_name" value="<%= @queue_name %>">
21
+ <input type="hidden" name="redirect_to" value="<%= queue_details_path(queue_name: @queue_name) %>">
22
+ <button type="submit" class="action-button pause-button">Pause Queue</button>
23
+ </form>
24
+ <% end %>
25
+ </div>
26
+ </div>
27
+
28
+ <div class="stats-container">
29
+ <div class="stats">
30
+ <% @counts.each do |label, value| %>
31
+ <%= render 'solid_queue_monitor/overview/stat_card', title: label.to_s.titleize, value: value %>
32
+ <% end %>
33
+ </div>
34
+ </div>
35
+
36
+ <%= render 'solid_queue_monitor/shared/filters',
37
+ action_path: @action_path,
38
+ filters: @filters.merge(queue_name: nil),
39
+ show_status: true %>
40
+
41
+ <% if @jobs[:records].empty? %>
42
+ <p class="empty-message">No jobs in this queue</p>
43
+ <% else %>
44
+ <% columns = [
45
+ { sort_key: nil, label: 'ID' },
46
+ { sort_key: :class_name, label: 'Job' },
47
+ { sort_key: nil, label: 'Arguments' },
48
+ { sort_key: nil, label: 'Status' },
49
+ { sort_key: :created_at, label: 'Created At' },
50
+ { sort_key: nil, label: 'Actions' }
51
+ ] %>
52
+
53
+ <%= render 'solid_queue_monitor/shared/jobs_table',
54
+ jobs: @jobs[:records],
55
+ columns: columns,
56
+ sort: @sort,
57
+ filters: @filters,
58
+ row_partial: 'solid_queue_monitor/queues/job_row' %>
59
+ <% end %>
60
+
61
+ <%= render 'solid_queue_monitor/shared/pagination',
62
+ current_page: @jobs[:current_page],
63
+ total_pages: @jobs[:total_pages] %>
@@ -0,0 +1,7 @@
1
+ <tr>
2
+ <td><%= link_to job.job.class_name, job_path(job.job), class: 'job-class-link' %></td>
3
+ <td><%= queue_link(job.queue_name) %></td>
4
+ <td><%= job.priority %></td>
5
+ <td><%= format_arguments(job.job.arguments) %></td>
6
+ <td><%= format_datetime(job.created_at) %></td>
7
+ </tr>
@@ -0,0 +1,25 @@
1
+ <% content_for :title, 'Ready Jobs' %>
2
+
3
+ <%= render 'solid_queue_monitor/shared/filters',
4
+ action_path: @action_path,
5
+ filters: @filters,
6
+ show_status: false %>
7
+
8
+ <% columns = [
9
+ { sort_key: :class_name, label: 'Job' },
10
+ { sort_key: :queue_name, label: 'Queue' },
11
+ { sort_key: :priority, label: 'Priority' },
12
+ { sort_key: nil, label: 'Arguments' },
13
+ { sort_key: :created_at, label: 'Created At' }
14
+ ] %>
15
+
16
+ <%= render 'solid_queue_monitor/shared/jobs_table',
17
+ jobs: @ready_jobs[:records],
18
+ columns: columns,
19
+ sort: @sort,
20
+ filters: @filters,
21
+ row_partial: 'solid_queue_monitor/ready_jobs/row' %>
22
+
23
+ <%= render 'solid_queue_monitor/shared/pagination',
24
+ current_page: @ready_jobs[:current_page],
25
+ total_pages: @ready_jobs[:total_pages] %>
@@ -0,0 +1,8 @@
1
+ <tr>
2
+ <td><%= job.key %></td>
3
+ <td><%= job.class_name %></td>
4
+ <td><%= job.schedule %></td>
5
+ <td><%= queue_link(job.queue_name) %></td>
6
+ <td><%= job.priority || 'Default' %></td>
7
+ <td><%= format_datetime(job.updated_at) %></td>
8
+ </tr>
@@ -0,0 +1,26 @@
1
+ <% content_for :title, 'Recurring Jobs' %>
2
+
3
+ <%= render 'solid_queue_monitor/shared/filters',
4
+ action_path: @action_path,
5
+ filters: @filters,
6
+ show_status: false %>
7
+
8
+ <% columns = [
9
+ { sort_key: :key, label: 'Key' },
10
+ { sort_key: :class_name, label: 'Job' },
11
+ { sort_key: nil, label: 'Schedule' },
12
+ { sort_key: :queue_name, label: 'Queue' },
13
+ { sort_key: :priority, label: 'Priority' },
14
+ { sort_key: nil, label: 'Last Updated' }
15
+ ] %>
16
+
17
+ <%= render 'solid_queue_monitor/shared/jobs_table',
18
+ jobs: @recurring_jobs[:records],
19
+ columns: columns,
20
+ sort: @sort,
21
+ filters: @filters,
22
+ row_partial: 'solid_queue_monitor/recurring_jobs/row' %>
23
+
24
+ <%= render 'solid_queue_monitor/shared/pagination',
25
+ current_page: @recurring_jobs[:current_page],
26
+ total_pages: @recurring_jobs[:total_pages] %>
@@ -0,0 +1,7 @@
1
+ <tr>
2
+ <td><input type="checkbox" name="job_ids[]" value="<%= job.id %>"></td>
3
+ <td><%= link_to job.job.class_name, job_path(job.job), class: 'job-class-link' %></td>
4
+ <td><%= queue_link(job.queue_name) %></td>
5
+ <td><%= format_datetime(job.scheduled_at) %></td>
6
+ <td><%= format_arguments(job.job.arguments) %></td>
7
+ </tr>
@@ -0,0 +1,36 @@
1
+ <% content_for :title, 'Scheduled Jobs' %>
2
+
3
+ <%= render 'solid_queue_monitor/shared/filters',
4
+ action_path: @action_path,
5
+ filters: @filters,
6
+ show_status: false %>
7
+
8
+ <div class="bulk-actions-bar">
9
+ <button type="button" class="action-button execute-button" id="execute-selected-top" data-action-url="<%= execute_jobs_path %>" disabled>
10
+ Execute Selected
11
+ </button>
12
+ <button type="button" class="action-button discard-button" id="reject-selected-top" data-action-url="<%= reject_jobs_path %>" disabled>
13
+ Reject Selected
14
+ </button>
15
+ </div>
16
+
17
+ <form id="scheduled-jobs-form" method="post">
18
+ <% columns = [
19
+ { sort_key: nil, label: tag.input(type: 'checkbox', id: 'scheduled-jobs-select-all') },
20
+ { sort_key: :class_name, label: 'Job' },
21
+ { sort_key: :queue_name, label: 'Queue' },
22
+ { sort_key: :scheduled_at, label: 'Scheduled At' },
23
+ { sort_key: nil, label: 'Arguments' }
24
+ ] %>
25
+
26
+ <%= render 'solid_queue_monitor/shared/jobs_table',
27
+ jobs: @scheduled_jobs[:records],
28
+ columns: columns,
29
+ sort: @sort,
30
+ filters: @filters,
31
+ row_partial: 'solid_queue_monitor/scheduled_jobs/row' %>
32
+ </form>
33
+
34
+ <%= render 'solid_queue_monitor/shared/pagination',
35
+ current_page: @scheduled_jobs[:current_page],
36
+ total_pages: @scheduled_jobs[:total_pages] %>
@@ -0,0 +1,6 @@
1
+ <tr>
2
+ <td><%= link_to item.class_name, job_path(item), class: 'job-class-link' %></td>
3
+ <td><%= queue_link(item.queue_name) %></td>
4
+ <td><%= format_arguments(item.arguments) %></td>
5
+ <td><span class="job-timestamp"><%= format_datetime(item.finished_at) %></span></td>
6
+ </tr>
@@ -0,0 +1,6 @@
1
+ <tr>
2
+ <td><%= link_to item.job.class_name, job_path(item.job), class: 'job-class-link' %></td>
3
+ <td><%= queue_link(item.job.queue_name) %></td>
4
+ <td><div class="error-message"><%= truncate(item.error.to_s, length: 100) %></div></td>
5
+ <td><span class="job-timestamp"><%= format_datetime(item.created_at) %></span></td>
6
+ </tr>
@@ -0,0 +1,9 @@
1
+ <% solid_queue_job = item.job %>
2
+ <% queue_name = item.respond_to?(:queue_name) && item.queue_name.present? ? item.queue_name : solid_queue_job.queue_name %>
3
+ <% time = item.respond_to?(:scheduled_at) && time_label == 'Scheduled for' ? item.scheduled_at : item.created_at %>
4
+ <tr>
5
+ <td><%= link_to solid_queue_job.class_name, job_path(solid_queue_job), class: 'job-class-link' %></td>
6
+ <td><%= queue_link(queue_name) %></td>
7
+ <td><%= format_arguments(solid_queue_job.arguments) %></td>
8
+ <td><span class="job-timestamp"><%= time_label %>: <%= format_datetime(time) %></span></td>
9
+ </tr>
@@ -0,0 +1,6 @@
1
+ <tr>
2
+ <td><strong><%= item.key %></strong></td>
3
+ <td><%= item.class_name || '-' %></td>
4
+ <td><code><%= item.schedule %></code></td>
5
+ <td><%= queue_link(item.queue_name) %></td>
6
+ </tr>
@@ -0,0 +1,25 @@
1
+ <div class="search-results-section">
2
+ <h3><%= title %> (<%= items.size %>)</h3>
3
+ <div class="table-container">
4
+ <table>
5
+ <thead>
6
+ <tr>
7
+ <% if title == 'Recurring Tasks' %>
8
+ <th>Key</th><th>Class</th><th>Schedule</th><th>Queue</th>
9
+ <% elsif title == 'Failed Jobs' %>
10
+ <th>Job</th><th>Queue</th><th>Error</th><th>Failed At</th>
11
+ <% elsif title == 'Completed Jobs' %>
12
+ <th>Job</th><th>Queue</th><th>Arguments</th><th>Completed At</th>
13
+ <% else %>
14
+ <th>Job</th><th>Queue</th><th>Arguments</th><th>Time</th>
15
+ <% end %>
16
+ </tr>
17
+ </thead>
18
+ <tbody>
19
+ <% items.each do |item| %>
20
+ <%= render row_partial, item: item, time_label: local_assigns[:time_label] %>
21
+ <% end %>
22
+ </tbody>
23
+ </table>
24
+ </div>
25
+ </div>
@@ -0,0 +1,23 @@
1
+ <% content_for :title, 'Search' %>
2
+
3
+ <% if @query.blank? %>
4
+ <div class="empty-state">
5
+ <p>Enter a search term in the header to find jobs across all categories.</p>
6
+ </div>
7
+ <% elsif @total_count.zero? %>
8
+ <div class="empty-state">
9
+ <p>No results found for "<%= @query %>"</p>
10
+ <p class="results-summary">0 results</p>
11
+ </div>
12
+ <% else %>
13
+ <div class="results-summary">
14
+ <p>Found <%= @total_count %> <%= 'result'.pluralize(@total_count) %> for "<%= @query %>"</p>
15
+ </div>
16
+
17
+ <%= render 'section', title: 'Ready Jobs', items: @results[:ready], row_partial: 'solid_queue_monitor/search/job_row', time_label: 'Created at' if @results[:ready].any? %>
18
+ <%= render 'section', title: 'Scheduled Jobs', items: @results[:scheduled], row_partial: 'solid_queue_monitor/search/job_row', time_label: 'Scheduled for' if @results[:scheduled].any? %>
19
+ <%= render 'section', title: 'Failed Jobs', items: @results[:failed], row_partial: 'solid_queue_monitor/search/failed_row' if @results[:failed].any? %>
20
+ <%= render 'section', title: 'In Progress Jobs', items: @results[:in_progress], row_partial: 'solid_queue_monitor/search/job_row', time_label: 'Started at' if @results[:in_progress].any? %>
21
+ <%= render 'section', title: 'Completed Jobs', items: @results[:completed], row_partial: 'solid_queue_monitor/search/completed_row' if @results[:completed].any? %>
22
+ <%= render 'section', title: 'Recurring Tasks', items: @results[:recurring], row_partial: 'solid_queue_monitor/search/recurring_row' if @results[:recurring].any? %>
23
+ <% end %>
@@ -0,0 +1,48 @@
1
+ <% show_status = local_assigns.fetch(:show_status, true) %>
2
+
3
+ <div class="filter-form-container">
4
+ <%= form_with url: action_path, method: :get, local: true, html: { class: 'filter-form' } do |form| %>
5
+ <div class="filter-group">
6
+ <%= form.label :class_name, 'Job Class:' %>
7
+ <%= form.text_field :class_name,
8
+ value: filters[:class_name],
9
+ placeholder: 'Filter by class name' %>
10
+ </div>
11
+
12
+ <div class="filter-group">
13
+ <%= form.label :queue_name, 'Queue:' %>
14
+ <%= form.text_field :queue_name,
15
+ value: filters[:queue_name],
16
+ placeholder: 'Filter by queue' %>
17
+ </div>
18
+
19
+ <div class="filter-group">
20
+ <%= form.label :arguments, 'Arguments:' %>
21
+ <%= form.text_field :arguments,
22
+ value: filters[:arguments],
23
+ placeholder: 'Filter by arguments' %>
24
+ </div>
25
+
26
+ <% if show_status %>
27
+ <div class="filter-group">
28
+ <%= form.label :status, 'Status:' %>
29
+ <%= form.select :status,
30
+ options_for_select(
31
+ [
32
+ ['All Statuses', ''],
33
+ ['Completed', 'completed'],
34
+ ['Failed', 'failed'],
35
+ ['Scheduled', 'scheduled'],
36
+ ['Pending', 'pending']
37
+ ],
38
+ filters[:status]
39
+ ) %>
40
+ </div>
41
+ <% end %>
42
+
43
+ <div class="filter-actions">
44
+ <%= form.submit 'Apply Filters', class: 'filter-button' %>
45
+ <%= link_to 'Reset', action_path, class: 'reset-button' %>
46
+ </div>
47
+ <% end %>
48
+ </div>
@@ -0,0 +1,17 @@
1
+ <%
2
+ message = @flash_message
3
+ message_type = @flash_type
4
+ begin
5
+ message ||= session[:flash_message]
6
+ message_type ||= session[:flash_type]
7
+ session.delete(:flash_message) if message
8
+ session.delete(:flash_type) if message_type
9
+ rescue StandardError
10
+ # Session not available.
11
+ end
12
+ %>
13
+ <% if message %>
14
+ <div id="flash-message" class="message <%= message_class(message_type) %>">
15
+ <%= message %>
16
+ </div>
17
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <footer>
2
+ <p>Powered by Solid Queue Monitor</p>
3
+ </footer>
@@ -0,0 +1,81 @@
1
+ <% nav_items = [
2
+ { path: root_path, label: 'Overview', match: 'Overview' },
3
+ { path: ready_jobs_path, label: 'Ready Jobs', match: 'Ready Jobs' },
4
+ { path: in_progress_jobs_path, label: 'In Progress Jobs', match: 'In Progress' },
5
+ { path: scheduled_jobs_path, label: 'Scheduled Jobs', match: 'Scheduled Jobs' },
6
+ { path: recurring_jobs_path, label: 'Recurring Jobs', match: 'Recurring Jobs' },
7
+ { path: failed_jobs_path, label: 'Failed Jobs', match: 'Failed Jobs' },
8
+ { path: queues_path, label: 'Queues', match: 'Queues' },
9
+ { path: workers_path, label: 'Workers', match: 'Workers' }
10
+ ] %>
11
+
12
+ <header>
13
+ <div class="header-top">
14
+ <h1>
15
+ <%= link_to 'Solid Queue Monitor', root_path, class: 'header-title-link' %>
16
+ </h1>
17
+
18
+ <%= form_with url: search_path, method: :get, local: true, html: { class: 'header-search-form' } do |form| %>
19
+ <%= form.text_field :q,
20
+ value: params[:q],
21
+ placeholder: 'Search by class, queue, job ID, or error...',
22
+ class: 'header-search-input' %>
23
+ <button type="submit" class="header-search-button" title="Search">
24
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor"
25
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
26
+ <circle cx="11" cy="11" r="8"></circle>
27
+ <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
28
+ </svg>
29
+ </button>
30
+ <% end %>
31
+
32
+ <div class="header-controls">
33
+ <% if SolidQueueMonitor.auto_refresh_enabled %>
34
+ <% interval = SolidQueueMonitor.auto_refresh_interval %>
35
+ <div class="auto-refresh-container"
36
+ title="Auto-refresh every <%= interval %>s"
37
+ data-tooltip="Auto-refresh: Dashboard updates automatically every <%= interval %> seconds. Toggle to enable/disable.">
38
+ <span class="auto-refresh-indicator" id="auto-refresh-indicator"></span>
39
+ <span class="auto-refresh-countdown" id="auto-refresh-countdown"><%= interval %>s</span>
40
+ <label class="auto-refresh-switch" title="Toggle auto-refresh">
41
+ <input type="checkbox" id="auto-refresh-toggle" checked>
42
+ <span class="switch-slider"></span>
43
+ </label>
44
+ <button class="refresh-now-btn" id="refresh-now-btn" title="Refresh now">
45
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
46
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
47
+ <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"/>
48
+ </svg>
49
+ </button>
50
+ </div>
51
+ <% end %>
52
+
53
+ <button class="theme-toggle-btn" id="theme-toggle-btn" title="Toggle dark mode">
54
+ <svg class="theme-icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor"
55
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
56
+ <circle cx="12" cy="12" r="5"></circle>
57
+ <line x1="12" y1="1" x2="12" y2="3"></line>
58
+ <line x1="12" y1="21" x2="12" y2="23"></line>
59
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
60
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
61
+ <line x1="1" y1="12" x2="3" y2="12"></line>
62
+ <line x1="21" y1="12" x2="23" y2="12"></line>
63
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
64
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
65
+ </svg>
66
+ <svg class="theme-icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
67
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
68
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
69
+ </svg>
70
+ </button>
71
+ </div>
72
+ </div>
73
+
74
+ <nav class="navigation">
75
+ <% nav_items.each do |item| %>
76
+ <%= link_to item[:label],
77
+ item[:path],
78
+ class: class_names('nav-link', active: content_for(:title).to_s.include?(item[:match])) %>
79
+ <% end %>
80
+ </nav>
81
+ </header>