modern_queue_dashboard 0.3.1 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f3b6ba3fdf6b602a2b5b6701717d6768a56ced216ec87b5f8aa36ac94a171c8
4
- data.tar.gz: 7b96e4fd86584d611e1c1ac0b68eccc3239aecdcc1261e8bed105afd682bb17a
3
+ metadata.gz: 714e432f39bf82408372d0cab4d74ae4f0d8b4f822bcb4efedda6aa54c375c81
4
+ data.tar.gz: ad682c7866d2e88a7810c580a70e0985ff3de45c4d3c6202fc3a91d9203d9049
5
5
  SHA512:
6
- metadata.gz: 7280186443a102521984f7987e2a5664c63567cc646f7f5b1867893cdf518ac71643a5051fa40bae050be5bd752fb958ca1b01f14f305d01c643310a44b189b2
7
- data.tar.gz: 710daee003dd37bbc49eb854d3ce106b4100660ab105038711304846271238e2058101590a9f32a4215990322dd3c40adb6b76a3056f1cc9e90ed4054e02c806
6
+ metadata.gz: 8d3d404322ae5e866d3854a9ac680ac9bc6c8f755d8db26ebf0d97d0c0dc3839d377e54e60cce9efa3666e0d8091075cf1dc5d4ac16db11fd43a9d6f0a4fd9f2
7
+ data.tar.gz: 1c67b80700c0acd3a4659a411663f7c80f1d96e2a6134bf2716cac82520ee93d3081126cdd247da8b1730194ba8d243dbe95843eed7f37a0b3f0168bc16aa2fa
data/CHANGELOG.md CHANGED
@@ -1,10 +1,31 @@
1
1
  ## [Unreleased]
2
2
 
3
- ## [0.3.1] - 2025-05-20
3
+ ## [0.4.1] - 2025-05-19
4
4
 
5
- - Fixed compatibility with Solid Queue standard schema structure
6
- - Updated queries to use `scheduled_at` instead of `run_at`
7
- - Improved job status tracking via the appropriate Solid Queue tables
5
+ - Fixed issue with jobs that have no arguments showing "Error parsing arguments"
6
+ - Improved argument handling and display in job tables
7
+
8
+ ## [0.4.0] - 2025-05-19
9
+
10
+ - Changed Tailwind's Sky color to Blue color for better contrast
11
+ - Added jobs table to queue view showing up to 50 most recent jobs with their status
12
+ - Added recent jobs table to main dashboard showing 10 most recent jobs across all queues
13
+ - Added color-coded status indicators for jobs
14
+ - Display detailed error information for failed jobs
15
+
16
+ ## [0.3.2] - 2025-05-19
17
+
18
+ - Fixed issue with load order
19
+
20
+ ## [0.3.1] - 2025-05-19
21
+
22
+ - Fixed compatibility with Solid Queue's database schema
23
+ - Updated queries to properly read from Solid Queue's execution tables:
24
+ - Ready jobs from `solid_queue_ready_executions`
25
+ - Scheduled jobs from `solid_queue_scheduled_executions`
26
+ - Running jobs from `solid_queue_claimed_executions`
27
+ - Failed jobs from `solid_queue_failed_executions`
28
+ - Added error handling for cases where tables might not be available
8
29
  - Updated README to clarify that this dashboard is exclusively for Solid Queue
9
30
 
10
31
  ## [0.3.0] - 2025-05-19
data/README.md CHANGED
@@ -1,9 +1,13 @@
1
1
  # Modern Queue Dashboard
2
2
 
3
- A mountable Rails engine that provides a clean, Hotwire-powered dashboard **specifically designed for monitoring [Solid Queue](https://github.com/basecamp/solid_queue)** jobs. Built with Tailwind CSS, Turbo frames, and Stimulus controllers.
3
+ A mountable Rails engine that provides a clean dashboard **specifically designed for monitoring [Solid Queue](https://github.com/basecamp/solid_queue)** jobs. Built with Tailwind CSS, Turbo frames, and Stimulus controllers.
4
4
 
5
5
  ![Dashboard Screenshot](screenshots/dashboard.png)
6
6
 
7
+ ## Why the "Modern Queue Dashboard"?
8
+
9
+ I didn't want to give the impression that there is any association with Solid Queue and because this dashboard could be used with other job backends in the future.
10
+
7
11
  ## Features
8
12
 
9
13
  * High-level metrics - counts for pending, scheduled, running, completed and failed jobs
@@ -79,18 +83,6 @@ class AuthenticatedConstraint
79
83
  end
80
84
  ```
81
85
 
82
- ### With HTTP Basic Auth
83
-
84
- ```ruby
85
- # In config/routes.rb
86
- mount ModernQueueDashboard::Engine, at: "/queue-dashboard", constraints: lambda { |request|
87
- ActiveSupport::SecurityUtils.secure_compare(
88
- ::Digest::SHA256.hexdigest(request.headers["Authorization"].to_s),
89
- ::Digest::SHA256.hexdigest("Basic #{Base64.encode64("username:password")}")
90
- )
91
- }
92
- ```
93
-
94
86
  ## Configuration
95
87
 
96
88
  You can configure the dashboard by creating an initializer:
@@ -5,6 +5,7 @@ module ModernQueueDashboard
5
5
  def index
6
6
  @metrics = Metrics.summary
7
7
  @queues = QueueSummary.with_stats.limit(10)
8
+ @recent_jobs = JobSummary.all_jobs(10) # Show 10 most recent jobs across all queues
8
9
  end
9
10
  end
10
11
  end
@@ -10,10 +10,12 @@ module ModernQueueDashboard
10
10
  @queue_name = params[:id]
11
11
  @queue = QueueSummary.with_stats.detect { |q| q.name == @queue_name }
12
12
 
13
- return if @queue
14
-
15
- flash[:error] = "Queue not found"
16
- redirect_to queues_path
13
+ if @queue
14
+ @jobs = JobSummary.for_queue(@queue_name, 50)
15
+ else
16
+ flash[:error] = "Queue not found"
17
+ redirect_to queues_path
18
+ end
17
19
  end
18
20
  end
19
21
  end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ModernQueueDashboard
4
+ JobStat = Struct.new(:id, :class_name, :queue_name, :arguments, :created_at, :status, :error, keyword_init: true)
5
+
6
+ # Collection class for JobStat objects
7
+ class JobStatCollection
8
+ include Enumerable
9
+
10
+ def initialize(stats)
11
+ @stats = stats
12
+ end
13
+
14
+ def each(&block)
15
+ @stats.each(&block)
16
+ end
17
+
18
+ def limit(num)
19
+ JobStatCollection.new(@stats.take(num))
20
+ end
21
+ end
22
+
23
+ class JobSummary
24
+ class << self
25
+ def for_queue(queue_name, limit = 50)
26
+ return JobStatCollection.new([]) if test_environment?
27
+
28
+ # Get jobs from SolidQueue::Job, ordered by most recently created
29
+ jobs = SolidQueue::Job.where(queue_name: queue_name)
30
+ .order(created_at: :desc)
31
+ .limit(limit)
32
+ .map { |job| job_to_stat(job) }
33
+
34
+ JobStatCollection.new(jobs)
35
+ end
36
+
37
+ def all_jobs(limit = 50)
38
+ return JobStatCollection.new([]) if test_environment?
39
+
40
+ # Get all jobs, ordered by most recently created
41
+ jobs = SolidQueue::Job.order(created_at: :desc)
42
+ .limit(limit)
43
+ .map { |job| job_to_stat(job) }
44
+
45
+ JobStatCollection.new(jobs)
46
+ end
47
+
48
+ private
49
+
50
+ def job_to_stat(job)
51
+ # Determine job status
52
+ status = determine_status(job)
53
+
54
+ # Get error message if job failed
55
+ error = nil
56
+ if status == "failed"
57
+ failed_execution = SolidQueue::FailedExecution.find_by(job_id: job.id)
58
+ error = failed_execution&.error
59
+ end
60
+
61
+ # Parse arguments - handle cases with no arguments properly
62
+ arguments_display = if job.arguments.nil? || job.arguments.empty?
63
+ "None"
64
+ else
65
+ begin
66
+ arguments_data = JSON.parse(job.arguments)
67
+ format_arguments(arguments_data)
68
+ rescue JSON::ParserError
69
+ # If we can't parse as JSON, show as is
70
+ job.arguments.to_s
71
+ end
72
+ end
73
+
74
+ # Create job stat
75
+ JobStat.new(
76
+ id: job.id,
77
+ class_name: job.class_name,
78
+ queue_name: job.queue_name,
79
+ arguments: arguments_display,
80
+ created_at: job.created_at,
81
+ status: status,
82
+ error: error
83
+ )
84
+ rescue => e
85
+ # Handle any parsing errors
86
+ JobStat.new(
87
+ id: job.id,
88
+ class_name: job.class_name,
89
+ queue_name: job.queue_name,
90
+ arguments: "Error parsing arguments",
91
+ created_at: job.created_at,
92
+ status: status,
93
+ error: error || e.message
94
+ )
95
+ end
96
+
97
+ def determine_status(job)
98
+ if job.finished_at.present?
99
+ "completed"
100
+ elsif SolidQueue::FailedExecution.exists?(job_id: job.id)
101
+ "failed"
102
+ elsif SolidQueue::ClaimedExecution.exists?(job_id: job.id)
103
+ "running"
104
+ elsif SolidQueue::ScheduledExecution.exists?(job_id: job.id)
105
+ "scheduled"
106
+ elsif SolidQueue::ReadyExecution.exists?(job_id: job.id)
107
+ "pending"
108
+ else
109
+ "unknown"
110
+ end
111
+ end
112
+
113
+ def format_arguments(args)
114
+ return "None" if args.blank?
115
+
116
+ if args.is_a?(Array) && args.length > 0
117
+ # Truncate long arguments to prevent display issues
118
+ args.map do |arg|
119
+ if arg.is_a?(String) && arg.length > 100
120
+ arg[0..100] + "..."
121
+ else
122
+ arg.to_s
123
+ end
124
+ end.join(", ")
125
+ else
126
+ args.to_s
127
+ end
128
+ end
129
+
130
+ def test_environment?
131
+ ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
132
+ end
133
+ end
134
+ end
135
+ end
@@ -6,7 +6,7 @@ module ModernQueueDashboard
6
6
  class Metrics
7
7
  class << self
8
8
  def summary
9
- [
9
+ metrics = [
10
10
  Metric.new(key: :pending, label: "Pending", value: pending_count),
11
11
  Metric.new(key: :scheduled, label: "Scheduled", value: scheduled_count),
12
12
  Metric.new(key: :running, label: "Running", value: running_count),
@@ -14,35 +14,69 @@ module ModernQueueDashboard
14
14
  Metric.new(key: :completed, label: "Completed", value: completed_count),
15
15
  Metric.new(key: :latency, label: "Latency", value: latency_seconds)
16
16
  ]
17
+ metrics
17
18
  end
18
19
 
19
20
  private
20
21
 
21
22
  def pending_count
22
- SolidQueue::Job.where("run_at <= ?", Time.current).count
23
+ begin
24
+ count = SolidQueue::ReadyExecution.count
25
+ count
26
+ rescue
27
+ 0
28
+ end
23
29
  end
24
30
 
25
31
  def scheduled_count
26
- SolidQueue::Job.where("run_at > ?", Time.current).count
32
+ begin
33
+ count = SolidQueue::ScheduledExecution.count
34
+ count
35
+ rescue
36
+ 0
37
+ end
27
38
  end
28
39
 
29
40
  def running_count
30
- SolidQueue::Execution.where(finished_at: nil).count
41
+ begin
42
+ count = SolidQueue::ClaimedExecution.count
43
+ count
44
+ rescue
45
+ 0
46
+ end
31
47
  end
32
48
 
33
49
  def failed_count
34
- SolidQueue::FailedExecution.count
50
+ begin
51
+ count = SolidQueue::FailedExecution.count
52
+ count
53
+ rescue
54
+ 0
55
+ end
35
56
  end
36
57
 
37
58
  def completed_count
38
- SolidQueue::Execution.where.not(finished_at: nil).count
59
+ begin
60
+ count = SolidQueue::Job.where.not(finished_at: nil).count
61
+ count
62
+ rescue
63
+ 0
64
+ end
39
65
  end
40
66
 
41
67
  def latency_seconds
42
- oldest = SolidQueue::Job.order(:created_at).limit(1).pick(:created_at)
43
- return 0 unless oldest
68
+ begin
69
+ oldest = SolidQueue::ReadyExecution
70
+ .joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_ready_executions.job_id")
71
+ .order("solid_queue_ready_executions.created_at")
72
+ .limit(1)
73
+ .pick("solid_queue_jobs.created_at")
44
74
 
45
- (Time.current - oldest).to_i
75
+ latency = oldest ? (Time.now - oldest).to_i : 0
76
+ latency
77
+ rescue
78
+ 0
79
+ end
46
80
  end
47
81
  end
48
82
  end
@@ -3,50 +3,109 @@
3
3
  module ModernQueueDashboard
4
4
  QueueStat = Struct.new(:name, :pending, :scheduled, :running, :failed, :latency, keyword_init: true)
5
5
 
6
+ # Collection class for QueueStat objects
7
+ class QueueStatCollection
8
+ include Enumerable
9
+
10
+ def initialize(stats)
11
+ @stats = stats
12
+ end
13
+
14
+ def each(&block)
15
+ @stats.each(&block)
16
+ end
17
+
18
+ def limit(num)
19
+ QueueStatCollection.new(@stats.take(num))
20
+ end
21
+ end
22
+
6
23
  class QueueSummary
7
24
  class << self
8
25
  def with_stats
9
- queue_names.map { |q| stats_for(q) }.sort_by(&:name)
26
+ return QueueStatCollection.new([]) if test_environment?
27
+
28
+ # Get all unique queue names from Solid Queue jobs
29
+ base_names = SolidQueue::Job.distinct.pluck(:queue_name)
30
+
31
+ # Also check execution tables in case job records were deleted but executions remain
32
+ ready_names = SolidQueue::ReadyExecution.distinct.pluck(:queue_name)
33
+ scheduled_names = SolidQueue::ScheduledExecution.distinct.pluck(:queue_name)
34
+
35
+ # Combine all queue names
36
+ names = (base_names + ready_names + scheduled_names).uniq
37
+
38
+ # Ensure we always have at least the default queue
39
+ names = [ "default" ] if names.empty?
40
+
41
+ stats = names.map { |q| stats_for(q) }.sort_by(&:name)
42
+ QueueStatCollection.new(stats)
10
43
  end
11
44
 
12
45
  private
13
46
 
14
47
  def queue_names
15
- SolidQueue::Job.distinct.pluck(:queue_name)
48
+ # This is now handled in with_stats
49
+ names = SolidQueue::Job.distinct.pluck(:queue_name)
50
+ names = [ "default" ] if names.empty?
51
+ names
16
52
  end
17
53
 
18
54
  def stats_for(name)
19
- # Pending: Jobs scheduled for now or in the past but not finished
20
- pending = SolidQueue::Job.where(queue_name: name)
21
- .where("scheduled_at <= ? AND finished_at IS NULL", Time.current)
22
- .count
23
-
24
- # Scheduled: Jobs scheduled for the future
25
- scheduled = SolidQueue::Job.where(queue_name: name)
26
- .where("scheduled_at > ? AND finished_at IS NULL", Time.current)
27
- .count
28
-
29
- # Running: Jobs that are currently claimed but not finished
30
- running = SolidQueue::ClaimedExecution.joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_claimed_executions.job_id")
31
- .where("solid_queue_jobs.queue_name = ?", name)
32
- .count
33
-
34
- # Failed: Jobs that have failed executions
35
- failed = SolidQueue::FailedExecution.joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_failed_executions.job_id")
36
- .where("solid_queue_jobs.queue_name = ?", name)
37
- .count
38
-
39
- # Latency: Time since oldest unfinished job was scheduled
40
- oldest_scheduled_at = SolidQueue::Job.where(queue_name: name)
41
- .where(finished_at: nil)
42
- .order(:scheduled_at)
43
- .limit(1)
44
- .pick(:scheduled_at)
45
-
46
- latency = oldest_scheduled_at ? (Time.current - oldest_scheduled_at).to_i : 0
55
+ # Pending: Jobs in ready_executions table
56
+ pending = begin
57
+ SolidQueue::ReadyExecution.where(queue_name: name).count
58
+ rescue
59
+ 0
60
+ end
61
+
62
+ # Scheduled: Jobs in scheduled_executions table
63
+ scheduled = begin
64
+ SolidQueue::ScheduledExecution.where(queue_name: name).count
65
+ rescue
66
+ 0
67
+ end
68
+
69
+ # Running: Jobs in claimed_executions joined with jobs
70
+ running = begin
71
+ SolidQueue::ClaimedExecution
72
+ .joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_claimed_executions.job_id")
73
+ .where("solid_queue_jobs.queue_name = ?", name)
74
+ .count
75
+ rescue
76
+ 0
77
+ end
78
+
79
+ # Failed: Jobs with failed executions
80
+ failed = begin
81
+ SolidQueue::FailedExecution
82
+ .joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_failed_executions.job_id")
83
+ .where("solid_queue_jobs.queue_name = ?", name)
84
+ .count
85
+ rescue
86
+ 0
87
+ end
88
+
89
+ # Latency: Time since oldest job in ready_executions was created
90
+ oldest_ready_job = begin
91
+ SolidQueue::ReadyExecution
92
+ .joins("INNER JOIN solid_queue_jobs ON solid_queue_jobs.id = solid_queue_ready_executions.job_id")
93
+ .where("solid_queue_ready_executions.queue_name = ?", name)
94
+ .order("solid_queue_ready_executions.created_at")
95
+ .limit(1)
96
+ .pick("solid_queue_jobs.created_at")
97
+ rescue
98
+ nil
99
+ end
100
+
101
+ latency = oldest_ready_job ? (Time.now - oldest_ready_job).to_i : 0
47
102
 
48
103
  QueueStat.new(name:, pending:, scheduled:, running:, failed:, latency:)
49
104
  end
105
+
106
+ def test_environment?
107
+ ENV["RAILS_ENV"] == "test" || ENV["RACK_ENV"] == "test"
108
+ end
50
109
  end
51
110
  end
52
111
  end
@@ -13,7 +13,7 @@
13
13
  <turbo-frame id="metric_<%= metric.key %>">
14
14
  <div class="bg-white shadow-sm rounded p-4 text-center">
15
15
  <p class="text-sm text-gray-500"><%= metric.label %></p>
16
- <p class="text-2xl font-bold text-sky-500"><%= metric.value %></p>
16
+ <p class="text-2xl font-bold text-blue-500"><%= metric.value %></p>
17
17
  </div>
18
18
  </turbo-frame>
19
19
  <% end %>
@@ -35,7 +35,7 @@
35
35
  <tbody class="bg-white divide-y divide-gray-200">
36
36
  <% @queues.each do |queue| %>
37
37
  <tr>
38
- <td class="px-6 py-4 whitespace-nowrap font-medium text-sky-500">
38
+ <td class="px-6 py-4 whitespace-nowrap font-medium text-blue-500">
39
39
  <%= link_to queue.name, queue_path(queue.name), data: { turbo_frame: "_top" } %>
40
40
  </td>
41
41
  <td class="px-6 py-4 text-right"><%= queue.pending %></td>
@@ -48,4 +48,55 @@
48
48
  </tbody>
49
49
  </table>
50
50
  </div>
51
+
52
+ <!-- Recent Jobs Table -->
53
+ <h2 class="text-2xl font-semibold mt-6">Recent Jobs</h2>
54
+ <div class="bg-white shadow rounded mt-4">
55
+ <div class="overflow-x-auto">
56
+ <table class="min-w-full divide-y divide-gray-200">
57
+ <thead class="bg-gray-50">
58
+ <tr>
59
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
60
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Queue</th>
61
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Job Class</th>
62
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
63
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created At</th>
64
+ </tr>
65
+ </thead>
66
+ <tbody class="bg-white divide-y divide-gray-200">
67
+ <% if @recent_jobs.any? %>
68
+ <% @recent_jobs.each do |job| %>
69
+ <tr>
70
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500"><%= job.id %></td>
71
+ <td class="px-4 py-3 whitespace-nowrap font-medium text-blue-500">
72
+ <%= link_to job.queue_name, queue_path(job.queue_name), data: { turbo_frame: "_top" } %>
73
+ </td>
74
+ <td class="px-4 py-3 whitespace-nowrap text-sm font-medium"><%= job.class_name %></td>
75
+ <td class="px-4 py-3 whitespace-nowrap text-sm">
76
+ <% status_color = case job.status
77
+ when 'completed' then 'bg-green-100 text-green-800'
78
+ when 'running' then 'bg-blue-100 text-blue-800'
79
+ when 'scheduled' then 'bg-yellow-100 text-yellow-800'
80
+ when 'pending' then 'bg-gray-100 text-gray-800'
81
+ when 'failed' then 'bg-red-100 text-red-800'
82
+ else 'bg-gray-100 text-gray-800'
83
+ end %>
84
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full <%= status_color %>">
85
+ <%= job.status %>
86
+ </span>
87
+ </td>
88
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
89
+ <%= job.created_at.strftime('%Y-%m-%d %H:%M:%S') %>
90
+ </td>
91
+ </tr>
92
+ <% end %>
93
+ <% else %>
94
+ <tr>
95
+ <td colspan="5" class="px-4 py-3 text-center text-sm text-gray-500">No jobs found</td>
96
+ </tr>
97
+ <% end %>
98
+ </tbody>
99
+ </table>
100
+ </div>
101
+ </div>
51
102
  </div>
@@ -19,7 +19,7 @@
19
19
  <tbody class="bg-white divide-y divide-gray-200">
20
20
  <% @queues.each do |queue| %>
21
21
  <tr>
22
- <td class="px-6 py-4 whitespace-nowrap font-medium text-sky-500">
22
+ <td class="px-6 py-4 whitespace-nowrap font-medium text-blue-500">
23
23
  <%= link_to queue.name, queue_path(queue.name), data: { turbo_frame: "_top" } %>
24
24
  </td>
25
25
  <td class="px-6 py-4 text-right"><%= queue.pending %></td>
@@ -2,7 +2,7 @@
2
2
  <h1 class="text-3xl font-semibold">Modern Queue Dashboard</h1>
3
3
 
4
4
  <div class="flex items-center space-x-2 mb-4">
5
- <%= link_to "← Back to Queues", queues_path, class: "text-sky-500" %>
5
+ <%= link_to "← Back to Queues", queues_path, class: "text-blue-500" %>
6
6
  </div>
7
7
 
8
8
  <h2 class="text-2xl font-semibold">Queue: <%= @queue_name %></h2>
@@ -12,19 +12,19 @@
12
12
  <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
13
13
  <div class="bg-white shadow-sm rounded p-4">
14
14
  <p class="text-sm text-gray-500">Pending Jobs</p>
15
- <p class="text-2xl font-bold text-sky-500"><%= @queue.pending %></p>
15
+ <p class="text-2xl font-bold text-blue-500"><%= @queue.pending %></p>
16
16
  </div>
17
17
  <div class="bg-white shadow-sm rounded p-4">
18
18
  <p class="text-sm text-gray-500">Scheduled Jobs</p>
19
- <p class="text-2xl font-bold text-sky-500"><%= @queue.scheduled %></p>
19
+ <p class="text-2xl font-bold text-blue-500"><%= @queue.scheduled %></p>
20
20
  </div>
21
21
  <div class="bg-white shadow-sm rounded p-4">
22
22
  <p class="text-sm text-gray-500">Running Jobs</p>
23
- <p class="text-2xl font-bold text-sky-500"><%= @queue.running %></p>
23
+ <p class="text-2xl font-bold text-blue-500"><%= @queue.running %></p>
24
24
  </div>
25
25
  <div class="bg-white shadow-sm rounded p-4">
26
26
  <p class="text-sm text-gray-500">Failed Jobs</p>
27
- <p class="text-2xl font-bold text-sky-500"><%= @queue.failed %></p>
27
+ <p class="text-2xl font-bold text-blue-500"><%= @queue.failed %></p>
28
28
  </div>
29
29
  </div>
30
30
 
@@ -43,6 +43,63 @@
43
43
  </div>
44
44
  </div>
45
45
  </div>
46
+
47
+ <!-- Jobs Table -->
48
+ <div class="bg-white shadow rounded p-6">
49
+ <h3 class="text-xl font-semibold mb-4">Recent Jobs</h3>
50
+
51
+ <div class="overflow-x-auto">
52
+ <table class="min-w-full divide-y divide-gray-200">
53
+ <thead class="bg-gray-50">
54
+ <tr>
55
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
56
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Job Class</th>
57
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
58
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Arguments</th>
59
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created At</th>
60
+ </tr>
61
+ </thead>
62
+ <tbody class="bg-white divide-y divide-gray-200">
63
+ <% if @jobs.any? %>
64
+ <% @jobs.each do |job| %>
65
+ <tr>
66
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500"><%= job.id %></td>
67
+ <td class="px-4 py-3 whitespace-nowrap text-sm font-medium"><%= job.class_name %></td>
68
+ <td class="px-4 py-3 whitespace-nowrap text-sm">
69
+ <% status_color = case job.status
70
+ when 'completed' then 'bg-green-100 text-green-800'
71
+ when 'running' then 'bg-blue-100 text-blue-800'
72
+ when 'scheduled' then 'bg-yellow-100 text-yellow-800'
73
+ when 'pending' then 'bg-gray-100 text-gray-800'
74
+ when 'failed' then 'bg-red-100 text-red-800'
75
+ else 'bg-gray-100 text-gray-800'
76
+ end %>
77
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full <%= status_color %>">
78
+ <%= job.status %>
79
+ </span>
80
+ </td>
81
+ <td class="px-4 py-3 text-sm text-gray-500 max-w-xs truncate"><%= job.arguments %></td>
82
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500">
83
+ <%= job.created_at.strftime('%Y-%m-%d %H:%M:%S') %>
84
+ </td>
85
+ </tr>
86
+ <% if job.status == 'failed' && job.error.present? %>
87
+ <tr class="bg-red-50">
88
+ <td colspan="5" class="px-4 py-2 text-xs text-red-700 font-mono">
89
+ <%= job.error %>
90
+ </td>
91
+ </tr>
92
+ <% end %>
93
+ <% end %>
94
+ <% else %>
95
+ <tr>
96
+ <td colspan="5" class="px-4 py-3 text-center text-sm text-gray-500">No jobs found in this queue</td>
97
+ </tr>
98
+ <% end %>
99
+ </tbody>
100
+ </table>
101
+ </div>
102
+ </div>
46
103
  <% else %>
47
104
  <div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
48
105
  Queue not found.
data/config/routes.rb CHANGED
@@ -4,4 +4,7 @@ ModernQueueDashboard::Engine.routes.draw do
4
4
  root to: "dashboard#index"
5
5
 
6
6
  resources :queues, only: %i[index show]
7
+
8
+ # Debug route for troubleshooting
9
+ get "debug", to: "dashboard#debug"
7
10
  end
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+
6
+ puts "Loading files..."
7
+ require_relative 'lib/modern_queue_dashboard'
8
+ require_relative 'app/models/modern_queue_dashboard/queue_summary'
9
+
10
+ # Check if constants are loaded
11
+ puts "ModernQueueDashboard defined? #{defined?(ModernQueueDashboard) != nil}"
12
+ puts "QueueSummary defined? #{defined?(ModernQueueDashboard::QueueSummary) != nil}"
13
+ puts "QueueStatCollection defined? #{defined?(ModernQueueDashboard::QueueStatCollection) != nil}"
14
+
15
+ if defined?(ModernQueueDashboard::QueueSummary)
16
+ puts "ModernQueueDashboard::QueueSummary location: #{ModernQueueDashboard::QueueSummary.method(:with_stats).source_location}"
17
+ end
@@ -7,6 +7,9 @@ module ModernQueueDashboard
7
7
  class Engine < ::Rails::Engine
8
8
  isolate_namespace ModernQueueDashboard
9
9
 
10
+ # Set up paths for models
11
+ config.paths.add "app/models", eager_load: true
12
+
10
13
  # Precompile CSS & JS builds that ship with the gem
11
14
  initializer "modern_queue_dashboard.assets" do |app|
12
15
  # Check if the app uses the asset pipeline that requires explicit precompilation
@@ -1,43 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ModernQueueDashboard
4
+ # Represents a single metric
4
5
  Metric = Struct.new(:key, :label, :value, keyword_init: true)
5
6
 
7
+ # Provides metrics related to jobs
6
8
  class Metrics
7
9
  def self.summary
8
10
  [
9
- Metric.new(key: :pending, label: "Pending", value: count_pending_jobs),
10
- Metric.new(key: :scheduled, label: "Scheduled", value: count_scheduled_jobs),
11
- Metric.new(key: :running, label: "Running", value: count_running_jobs),
12
- Metric.new(key: :failed, label: "Failed", value: count_failed_jobs),
13
- Metric.new(key: :completed, label: "Completed", value: count_completed_jobs),
14
- Metric.new(key: :latency, label: "Latency", value: calculate_latency)
11
+ Metric.new(key: :pending, label: "Pending", value: 0),
12
+ Metric.new(key: :scheduled, label: "Scheduled", value: 0),
13
+ Metric.new(key: :running, label: "Running", value: 0),
14
+ Metric.new(key: :failed, label: "Failed", value: 0),
15
+ Metric.new(key: :completed, label: "Completed", value: 0),
16
+ Metric.new(key: :latency, label: "Avg. Latency", value: 0)
15
17
  ]
16
18
  end
17
-
18
- # These methods are placeholders and will need to be implemented using the SolidQueue models
19
- def self.count_pending_jobs
20
- 0
21
- end
22
-
23
- def self.count_scheduled_jobs
24
- 0
25
- end
26
-
27
- def self.count_running_jobs
28
- 0
29
- end
30
-
31
- def self.count_failed_jobs
32
- 0
33
- end
34
-
35
- def self.count_completed_jobs
36
- 0
37
- end
38
-
39
- def self.calculate_latency
40
- 0
41
- end
42
19
  end
43
20
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ModernQueueDashboard
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -4,9 +4,6 @@ require "rails"
4
4
  require "action_controller/railtie"
5
5
  require "active_support/dependencies"
6
6
  require_relative "modern_queue_dashboard/version"
7
- require_relative "modern_queue_dashboard/engine"
8
- require_relative "modern_queue_dashboard/metrics"
9
- require_relative "modern_queue_dashboard/queue_summary"
10
7
 
11
8
  module ModernQueueDashboard
12
9
  class Error < StandardError; end
@@ -31,5 +28,11 @@ module ModernQueueDashboard
31
28
  yield(configuration)
32
29
  end
33
30
  end
34
- # Your code goes here...
35
31
  end
32
+
33
+ # Load engine which will handle autoloading of app files
34
+ require_relative "modern_queue_dashboard/engine"
35
+
36
+ # Explicitly require models for compatibility
37
+ require_relative "../app/models/modern_queue_dashboard/queue_summary"
38
+ require_relative "../app/models/modern_queue_dashboard/metrics"
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: modern_queue_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clayton Lengel-Zigich
@@ -150,6 +150,7 @@ files:
150
150
  - app/controllers/modern_queue_dashboard/application_controller.rb
151
151
  - app/controllers/modern_queue_dashboard/dashboard_controller.rb
152
152
  - app/controllers/modern_queue_dashboard/queues_controller.rb
153
+ - app/models/modern_queue_dashboard/job_summary.rb
153
154
  - app/models/modern_queue_dashboard/metrics.rb
154
155
  - app/models/modern_queue_dashboard/queue_summary.rb
155
156
  - app/views/layouts/modern_queue_dashboard/application.html.erb
@@ -157,14 +158,15 @@ files:
157
158
  - app/views/modern_queue_dashboard/queues/index.html.erb
158
159
  - app/views/modern_queue_dashboard/queues/show.html.erb
159
160
  - config/routes.rb
161
+ - debug_load_order.rb
160
162
  - docs/PLAN.md
161
163
  - lib/modern_queue_dashboard.rb
162
164
  - lib/modern_queue_dashboard/engine.rb
163
165
  - lib/modern_queue_dashboard/metrics.rb
164
- - lib/modern_queue_dashboard/queue_summary.rb
165
166
  - lib/modern_queue_dashboard/version.rb
166
167
  - package-lock.json
167
168
  - package.json
169
+ - screenshots/dashboard.png
168
170
  - sig/modern_queue_dashboard.rbs
169
171
  - tailwind.config.js
170
172
  homepage: https://github.com/clayton/modern_queue_dashboard
@@ -1,38 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ModernQueueDashboard
4
- QueueStat = Struct.new(:name, :pending, :scheduled, :running, :failed, :latency, keyword_init: true)
5
-
6
- class QueueSummary
7
- def self.with_stats
8
- # This is a placeholder and will need actual implementation using SolidQueue
9
- results = [
10
- QueueStat.new(
11
- name: "default",
12
- pending: 0,
13
- scheduled: 0,
14
- running: 0,
15
- failed: 0,
16
- latency: 0
17
- )
18
- ]
19
- QueueStatCollection.new(results)
20
- end
21
- end
22
-
23
- class QueueStatCollection
24
- include Enumerable
25
-
26
- def initialize(stats)
27
- @stats = stats
28
- end
29
-
30
- def each(&)
31
- @stats.each(&)
32
- end
33
-
34
- def limit(num)
35
- QueueStatCollection.new(@stats.take(num))
36
- end
37
- end
38
- end