From 7b78d227a7cfb851a440511a96125b756d4eda78 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Wed, 20 May 2026 17:26:42 -0400 Subject: [PATCH 1/6] feat: queue depth trend sparkline on dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "Queue Depth — Last 12 Hours" card to the dashboard showing estimated queue depth at 12 hourly snapshots. Depth at time T is computed as jobs created before T that have not yet finished by T — DB-agnostic via a single pluck and Ruby-side grouping, mirroring the throughput chart pattern. Bars are purple to distinguish from the blue throughput and red failure rate charts. Empty state shown when no active jobs exist in the window. Co-Authored-By: Claude Sonnet 4.6 --- .../solid_queue_web/_11_throughput.css | 4 +++ .../solid_queue_web/dashboard_controller.rb | 8 +++++ .../solid_queue_web/dashboard/index.html.erb | 30 +++++++++++++++++++ 3 files changed, 42 insertions(+) diff --git a/app/assets/stylesheets/solid_queue_web/_11_throughput.css b/app/assets/stylesheets/solid_queue_web/_11_throughput.css index 8fa054b..b302e59 100644 --- a/app/assets/stylesheets/solid_queue_web/_11_throughput.css +++ b/app/assets/stylesheets/solid_queue_web/_11_throughput.css @@ -90,4 +90,8 @@ .sqd-mini-sparkline__bar--empty { background: var(--border); opacity: 0.5; +} + +.sqd-sparkline__bar--depth { + background: var(--purple); } \ No newline at end of file diff --git a/app/controllers/solid_queue_web/dashboard_controller.rb b/app/controllers/solid_queue_web/dashboard_controller.rb index dc6753e..3675673 100644 --- a/app/controllers/solid_queue_web/dashboard_controller.rb +++ b/app/controllers/solid_queue_web/dashboard_controller.rb @@ -23,6 +23,14 @@ def index to = i == 11 ? now : (11 - i).hours.ago finished_times.count { |t| t >= from && t < to } end + + job_timestamps = SolidQueue::Job + .where("created_at >= ? OR finished_at IS NULL", 72.hours.ago) + .pluck(:created_at, :finished_at) + @depth_sparkline = 12.times.map do |i| + t = i == 11 ? now : (12 - i).hours.ago + job_timestamps.count { |created, finished| created <= t && (finished.nil? || finished > t) } + end end def retry_all_failed diff --git a/app/views/solid_queue_web/dashboard/index.html.erb b/app/views/solid_queue_web/dashboard/index.html.erb index f5d9942..18ae6d4 100644 --- a/app/views/solid_queue_web/dashboard/index.html.erb +++ b/app/views/solid_queue_web/dashboard/index.html.erb @@ -74,6 +74,36 @@ <% end %> +<% current_depth = @stats[:ready] + @stats[:scheduled] + @stats[:claimed] + @stats[:blocked] + @stats[:failed] %> +<% max_depth = [@depth_sparkline.max, 1].max %> +
+
+ Queue Depth — Last 12 Hours +
+ Now: <%= current_depth %> +
+
+ <% if @depth_sparkline.all?(&:zero?) %> +
No active jobs in the last 12 hours
+ <% else %> +
+ <% @depth_sparkline.each_with_index do |depth, i| %> + <% pct = (depth.to_f / max_depth * 100).round %> + <% t = i == 11 ? Time.current : (12 - i).hours.ago %> + <% show_tick = [0, 3, 6, 9, 11].include?(i) %> +
+
+
: <%= depth %> <%= "job".pluralize(depth) %> in queue">
+
+
<%= show_tick ? (i == 11 ? "now" : t.strftime("%-I%p").downcase) : "" %>
+
+ <% end %> +
+ <% end %> +
+
From e054d6583e3d86448ea4257deaed8117bea65a4d Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Wed, 20 May 2026 17:30:54 -0400 Subject: [PATCH 2/6] docs: update CHANGELOG and README for queue depth trend Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + README.md | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d6b012..05ba8bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Failure rate sparkline per queue — a mini 12-bar chart in the Queues table shows the percentage of jobs that failed (vs. completed) in each of the last 12 hours; bars are red, sized proportionally to the failure rate (0–100 %), and include a tooltip with the hour label and exact percentage; empty hours render as a faint border-colored bar; queues with no activity in the last 12 hours show "—" +- Queue depth trend — a "Queue Depth — Last 12 Hours" card on the dashboard shows estimated queue depth at 12 hourly snapshots; depth at time T is the count of jobs created before T that had not yet finished by T; bars are purple (distinct from the blue throughput and red failure rate charts); the card header shows current depth; empty state shown when no active jobs exist in the window ## [0.8.0] - 2026-05-20 diff --git a/README.md b/README.md index aa95736..5050a14 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ SolidQueueWeb surfaces all of this in a browser UI available at any route you ch ## Features -- **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues, recurring tasks, and processes; "Done (1h)" and "Done (24h)" throughput cards; a "Throughput — Last 12 Hours" bar chart showing completed-job counts per hour (pure CSS, no charting library); auto-refreshes every 5 seconds +- **Dashboard** — stat cards showing counts for ready, scheduled, running, blocked, and failed jobs, plus queues, recurring tasks, and processes; "Done (1h)" and "Done (24h)" throughput cards; a "Throughput — Last 12 Hours" bar chart (blue) and a "Queue Depth — Last 12 Hours" bar chart (purple) showing hourly snapshots of active job count; pure CSS, no charting library; auto-refreshes every 5 seconds - **Queues** — all queues sorted by name with size; oldest ready job latency (color-coded, with UTC timestamp tooltip); Done (24h) and Failed (24h) throughput counts; a mini 12-bar failure rate sparkline per queue showing failure % per hour over the last 12 hours; pause/resume controls - **Jobs** — filterable by status (ready, scheduled, claimed, blocked, failed) and by queue; search by job class name with dynamic auto-submit; time-based period filter (1 h / 24 h / 7 d); discard individual or all jobs; Turbo Frame navigation so only the table updates on filter or search; auto-refreshes every 10 seconds - **Failed jobs** — list of failed executions with error details; search by class name; filter by queue; time-based period filter; retry or discard individually or in bulk @@ -112,7 +112,6 @@ No authentication is enforced by default. When the `authenticate` block returns Planned features, roughly ordered by priority: **Observability** -- Queue depth trend — historical queue size over time, not just the current snapshot - Slow job detection — flag jobs exceeding a configurable duration threshold **Operations** From 370a6c6c874ef0c91c60645a9e509094db410dcf Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Wed, 20 May 2026 17:34:30 -0400 Subject: [PATCH 3/6] refactor: extract DashboardStats service object Move all four index queries (stats, throughput, sparkline, depth_sparkline) out of DashboardController into DashboardStats, mirroring the QueueStats pattern. Controller index action is now 5 lines. Co-Authored-By: Claude Sonnet 4.6 --- .../solid_queue_web/dashboard_controller.rb | 35 +++------------ .../solid_queue_web/dashboard_stats.rb | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 app/services/solid_queue_web/dashboard_stats.rb diff --git a/app/controllers/solid_queue_web/dashboard_controller.rb b/app/controllers/solid_queue_web/dashboard_controller.rb index 3675673..8db2869 100644 --- a/app/controllers/solid_queue_web/dashboard_controller.rb +++ b/app/controllers/solid_queue_web/dashboard_controller.rb @@ -1,36 +1,11 @@ module SolidQueueWeb class DashboardController < ApplicationController def index - @stats = { - ready: SolidQueue::ReadyExecution.count, - scheduled: SolidQueue::ScheduledExecution.count, - claimed: SolidQueue::ClaimedExecution.count, - failed: SolidQueue::FailedExecution.count, - blocked: SolidQueue::BlockedExecution.count, - queues: SolidQueue::Job.select(:queue_name).distinct.count, - processes: SolidQueue::Process.count, - recurring: SolidQueue::RecurringTask.count - } - - now = Time.current - finished_times = SolidQueue::Job.where(finished_at: 24.hours.ago..now).pluck(:finished_at) - @throughput = { - completed_1h: finished_times.count { |t| t >= 1.hour.ago }, - completed_24h: finished_times.size - } - @sparkline = 12.times.map do |i| - from = (12 - i).hours.ago - to = i == 11 ? now : (11 - i).hours.ago - finished_times.count { |t| t >= from && t < to } - end - - job_timestamps = SolidQueue::Job - .where("created_at >= ? OR finished_at IS NULL", 72.hours.ago) - .pluck(:created_at, :finished_at) - @depth_sparkline = 12.times.map do |i| - t = i == 11 ? now : (12 - i).hours.ago - job_timestamps.count { |created, finished| created <= t && (finished.nil? || finished > t) } - end + stats = DashboardStats.new + @stats = stats.stats + @throughput = stats.throughput + @sparkline = stats.sparkline + @depth_sparkline = stats.depth_sparkline end def retry_all_failed diff --git a/app/services/solid_queue_web/dashboard_stats.rb b/app/services/solid_queue_web/dashboard_stats.rb new file mode 100644 index 0000000..e08e09c --- /dev/null +++ b/app/services/solid_queue_web/dashboard_stats.rb @@ -0,0 +1,44 @@ +module SolidQueueWeb + class DashboardStats + attr_reader :stats, :throughput, :sparkline, :depth_sparkline + + def initialize + @now = Time.current + compute + end + + private + + def compute + @stats = { + ready: SolidQueue::ReadyExecution.count, + scheduled: SolidQueue::ScheduledExecution.count, + claimed: SolidQueue::ClaimedExecution.count, + failed: SolidQueue::FailedExecution.count, + blocked: SolidQueue::BlockedExecution.count, + queues: SolidQueue::Job.select(:queue_name).distinct.count, + processes: SolidQueue::Process.count, + recurring: SolidQueue::RecurringTask.count + } + + finished_times = SolidQueue::Job.where(finished_at: 24.hours.ago..@now).pluck(:finished_at) + @throughput = { + completed_1h: finished_times.count { |t| t >= 1.hour.ago }, + completed_24h: finished_times.size + } + @sparkline = 12.times.map do |i| + from = (12 - i).hours.ago + to = i == 11 ? @now : (11 - i).hours.ago + finished_times.count { |t| t >= from && t < to } + end + + job_timestamps = SolidQueue::Job + .where("created_at >= ? OR finished_at IS NULL", 72.hours.ago) + .pluck(:created_at, :finished_at) + @depth_sparkline = 12.times.map do |i| + t = i == 11 ? @now : (12 - i).hours.ago + job_timestamps.count { |created, finished| created <= t && (finished.nil? || finished > t) } + end + end + end +end From 95b8ca12c016a3f63a800af721b34b498486943e Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Wed, 20 May 2026 17:39:15 -0400 Subject: [PATCH 4/6] refactor: pass single @stats object to dashboard view Controller index action is now one line. DashboardStats#stats renamed to #counts to avoid @stats.stats in the view. View uses dot notation: @stats.counts[:key], @stats.throughput, @stats.sparkline, @stats.depth_sparkline. Co-Authored-By: Claude Sonnet 4.6 --- .../solid_queue_web/dashboard_controller.rb | 6 +-- .../solid_queue_web/dashboard_stats.rb | 4 +- .../solid_queue_web/dashboard/index.html.erb | 50 +++++++++---------- 3 files changed, 28 insertions(+), 32 deletions(-) diff --git a/app/controllers/solid_queue_web/dashboard_controller.rb b/app/controllers/solid_queue_web/dashboard_controller.rb index 8db2869..93f1fde 100644 --- a/app/controllers/solid_queue_web/dashboard_controller.rb +++ b/app/controllers/solid_queue_web/dashboard_controller.rb @@ -1,11 +1,7 @@ module SolidQueueWeb class DashboardController < ApplicationController def index - stats = DashboardStats.new - @stats = stats.stats - @throughput = stats.throughput - @sparkline = stats.sparkline - @depth_sparkline = stats.depth_sparkline + @stats = DashboardStats.new end def retry_all_failed diff --git a/app/services/solid_queue_web/dashboard_stats.rb b/app/services/solid_queue_web/dashboard_stats.rb index e08e09c..1621792 100644 --- a/app/services/solid_queue_web/dashboard_stats.rb +++ b/app/services/solid_queue_web/dashboard_stats.rb @@ -1,6 +1,6 @@ module SolidQueueWeb class DashboardStats - attr_reader :stats, :throughput, :sparkline, :depth_sparkline + attr_reader :counts, :throughput, :sparkline, :depth_sparkline def initialize @now = Time.current @@ -10,7 +10,7 @@ def initialize private def compute - @stats = { + @counts = { ready: SolidQueue::ReadyExecution.count, scheduled: SolidQueue::ScheduledExecution.count, claimed: SolidQueue::ClaimedExecution.count, diff --git a/app/views/solid_queue_web/dashboard/index.html.erb b/app/views/solid_queue_web/dashboard/index.html.erb index 18ae6d4..3bf0b57 100644 --- a/app/views/solid_queue_web/dashboard/index.html.erb +++ b/app/views/solid_queue_web/dashboard/index.html.erb @@ -3,61 +3,61 @@
<%= link_to jobs_path(status: "ready"), class: "sqd-stat sqd-stat--ready sqd-stat--link" do %> -
<%= @stats[:ready] %>
+
<%= @stats.counts[:ready] %>
Ready
<% end %> <%= link_to jobs_path(status: "scheduled"), class: "sqd-stat sqd-stat--scheduled sqd-stat--link" do %> -
<%= @stats[:scheduled] %>
+
<%= @stats.counts[:scheduled] %>
Scheduled
<% end %> <%= link_to jobs_path(status: "claimed"), class: "sqd-stat sqd-stat--claimed sqd-stat--link" do %> -
<%= @stats[:claimed] %>
+
<%= @stats.counts[:claimed] %>
Running
<% end %> <%= link_to jobs_path(status: "blocked"), class: "sqd-stat sqd-stat--blocked sqd-stat--link" do %> -
<%= @stats[:blocked] %>
+
<%= @stats.counts[:blocked] %>
Blocked
<% end %> <%= link_to failed_jobs_path, class: "sqd-stat sqd-stat--failed sqd-stat--link" do %> -
<%= @stats[:failed] %>
+
<%= @stats.counts[:failed] %>
Failed
<% end %> <%= link_to queues_path, class: "sqd-stat sqd-stat--queues sqd-stat--link" do %> -
<%= @stats[:queues] %>
+
<%= @stats.counts[:queues] %>
Queues
<% end %> <%= link_to recurring_tasks_path, class: "sqd-stat sqd-stat--recurring sqd-stat--link" do %> -
<%= @stats[:recurring] %>
+
<%= @stats.counts[:recurring] %>
Recurring
<% end %> <%= link_to processes_path, class: "sqd-stat sqd-stat--processes sqd-stat--link" do %> -
<%= @stats[:processes] %>
+
<%= @stats.counts[:processes] %>
Processes
<% end %> <%= link_to history_path(period: "1h"), class: "sqd-stat sqd-stat--done sqd-stat--link" do %> -
<%= @throughput[:completed_1h] %>
+
<%= @stats.throughput[:completed_1h] %>
Done (1h)
<% end %> <%= link_to history_path(period: "24h"), class: "sqd-stat sqd-stat--done sqd-stat--link" do %> -
<%= @throughput[:completed_24h] %>
+
<%= @stats.throughput[:completed_24h] %>
Done (24h)
<% end %>
-<% max_val = [@sparkline.max, 1].max %> +<% max_val = [@stats.sparkline.max, 1].max %>
Throughput — Last 12 Hours
- 1h: <%= @throughput[:completed_1h] %> - 24h: <%= @throughput[:completed_24h] %> + 1h: <%= @stats.throughput[:completed_1h] %> + 24h: <%= @stats.throughput[:completed_24h] %>
- <% if @throughput[:completed_24h] == 0 %> + <% if @stats.throughput[:completed_24h] == 0 %>
No completed jobs in the last 24 hours
<% else %>
- <% @sparkline.each_with_index do |count, i| %> + <% @stats.sparkline.each_with_index do |count, i| %> <% pct = (count.to_f / max_val * 100).round %> <% hour_start = (12 - i).hours.ago %> <% show_tick = [0, 3, 6, 9, 11].include?(i) %> @@ -74,8 +74,8 @@ <% end %>
-<% current_depth = @stats[:ready] + @stats[:scheduled] + @stats[:claimed] + @stats[:blocked] + @stats[:failed] %> -<% max_depth = [@depth_sparkline.max, 1].max %> +<% current_depth = @stats.counts[:ready] + @stats.counts[:scheduled] + @stats.counts[:claimed] + @stats.counts[:blocked] + @stats.counts[:failed] %> +<% max_depth = [@stats.depth_sparkline.max, 1].max %>
Queue Depth — Last 12 Hours @@ -83,11 +83,11 @@ Now: <%= current_depth %>
- <% if @depth_sparkline.all?(&:zero?) %> + <% if @stats.depth_sparkline.all?(&:zero?) %>
No active jobs in the last 12 hours
<% else %>
- <% @depth_sparkline.each_with_index do |depth, i| %> + <% @stats.depth_sparkline.each_with_index do |depth, i| %> <% pct = (depth.to_f / max_depth * 100).round %> <% t = i == 11 ? Time.current : (12 - i).hours.ago %> <% show_tick = [0, 3, 6, 9, 11].include?(i) %> @@ -118,37 +118,37 @@
- <% if @stats[:failed] > 0 %> + <% if @stats.counts[:failed] > 0 %>
Failed Jobs

- <%= pluralize(@stats[:failed], "failed job") %> need attention. + <%= pluralize(@stats.counts[:failed], "failed job") %> need attention.

<%= button_to "Retry All Failed", retry_all_failed_path, method: :post, class: "sqd-btn sqd-btn--primary", - data: { confirm: "Retry all #{@stats[:failed]} failed #{"job".pluralize(@stats[:failed])}?" } %> + data: { confirm: "Retry all #{@stats.counts[:failed]} failed #{"job".pluralize(@stats.counts[:failed])}?" } %> <%= link_to "Review →", failed_jobs_path, class: "sqd-btn sqd-btn--muted" %>
<% end %> - <% if @stats[:blocked] > 0 %> + <% if @stats.counts[:blocked] > 0 %>
Blocked Jobs

- <%= pluralize(@stats[:blocked], "blocked job") %>. + <%= pluralize(@stats.counts[:blocked], "blocked job") %>.

<%= button_to "Discard All Blocked", discard_all_blocked_path, method: :post, class: "sqd-btn sqd-btn--danger", - data: { confirm: "Discard all #{@stats[:blocked]} blocked #{"job".pluralize(@stats[:blocked])}? This cannot be undone." } %> + data: { confirm: "Discard all #{@stats.counts[:blocked]} blocked #{"job".pluralize(@stats.counts[:blocked])}? This cannot be undone." } %> <%= link_to "Review →", jobs_path(status: "blocked"), class: "sqd-btn sqd-btn--muted" %>
From 23b9bada8a23453d6ed25a7a49510a21dddca71f Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Wed, 20 May 2026 17:48:55 -0400 Subject: [PATCH 5/6] refactor: extract BlockedJobsController#destroy from DashboardController DashboardController#discard_all_blocked was a custom-named action; extract it to BlockedJobsController#destroy following the 7-action Rails convention. Dashboard "Retry All Failed" button now routes to the existing RetryFailedJobsController#create (retry_all_failed_jobs_path) eliminating DashboardController#retry_all_failed as well. Routes: drop two custom POST routes; add singular resource :blocked_jobs. Specs: move blocked-jobs coverage to blocked_jobs_spec.rb; retry_all coverage already exists in failed_jobs_spec.rb. Co-Authored-By: Claude Sonnet 4.6 --- .../blocked_jobs_controller.rb | 11 +++ .../solid_queue_web/dashboard_controller.rb | 16 ----- .../solid_queue_web/dashboard/index.html.erb | 6 +- config/routes.rb | 3 +- .../solid_queue_web/blocked_jobs_spec.rb | 38 +++++++++++ .../solid_queue_web/dashboard_spec.rb | 68 ------------------- 6 files changed, 53 insertions(+), 89 deletions(-) create mode 100644 app/controllers/solid_queue_web/blocked_jobs_controller.rb create mode 100644 spec/requests/solid_queue_web/blocked_jobs_spec.rb diff --git a/app/controllers/solid_queue_web/blocked_jobs_controller.rb b/app/controllers/solid_queue_web/blocked_jobs_controller.rb new file mode 100644 index 0000000..4a25db6 --- /dev/null +++ b/app/controllers/solid_queue_web/blocked_jobs_controller.rb @@ -0,0 +1,11 @@ +module SolidQueueWeb + class BlockedJobsController < ApplicationController + def destroy + jobs = SolidQueue::BlockedExecution.includes(:job).map(&:job) + SolidQueue::BlockedExecution.discard_all_from_jobs(jobs) + redirect_to root_path, notice: "#{jobs.size} blocked #{"job".pluralize(jobs.size)} discarded." + rescue => e + redirect_to root_path, alert: "Could not discard blocked jobs: #{e.message}" + end + end +end diff --git a/app/controllers/solid_queue_web/dashboard_controller.rb b/app/controllers/solid_queue_web/dashboard_controller.rb index 93f1fde..d4ed830 100644 --- a/app/controllers/solid_queue_web/dashboard_controller.rb +++ b/app/controllers/solid_queue_web/dashboard_controller.rb @@ -3,21 +3,5 @@ class DashboardController < ApplicationController def index @stats = DashboardStats.new end - - def retry_all_failed - jobs = SolidQueue::FailedExecution.includes(:job).map(&:job) - SolidQueue::FailedExecution.retry_all(jobs) - redirect_to root_path, notice: "#{jobs.size} failed #{"job".pluralize(jobs.size)} queued for retry." - rescue => e - redirect_to root_path, alert: "Could not retry failed jobs: #{e.message}" - end - - def discard_all_blocked - jobs = SolidQueue::BlockedExecution.includes(:job).map(&:job) - SolidQueue::BlockedExecution.discard_all_from_jobs(jobs) - redirect_to root_path, notice: "#{jobs.size} blocked #{"job".pluralize(jobs.size)} discarded." - rescue => e - redirect_to root_path, alert: "Could not discard blocked jobs: #{e.message}" - end end end diff --git a/app/views/solid_queue_web/dashboard/index.html.erb b/app/views/solid_queue_web/dashboard/index.html.erb index 3bf0b57..3b63cbc 100644 --- a/app/views/solid_queue_web/dashboard/index.html.erb +++ b/app/views/solid_queue_web/dashboard/index.html.erb @@ -127,7 +127,7 @@

<%= pluralize(@stats.counts[:failed], "failed job") %> need attention.

- <%= button_to "Retry All Failed", retry_all_failed_path, + <%= button_to "Retry All Failed", retry_all_failed_jobs_path, method: :post, class: "sqd-btn sqd-btn--primary", data: { confirm: "Retry all #{@stats.counts[:failed]} failed #{"job".pluralize(@stats.counts[:failed])}?" } %> @@ -145,8 +145,8 @@

<%= pluralize(@stats.counts[:blocked], "blocked job") %>.

- <%= button_to "Discard All Blocked", discard_all_blocked_path, - method: :post, + <%= button_to "Discard All Blocked", blocked_jobs_path, + method: :delete, class: "sqd-btn sqd-btn--danger", data: { confirm: "Discard all #{@stats.counts[:blocked]} blocked #{"job".pluralize(@stats.counts[:blocked])}? This cannot be undone." } %> <%= link_to "Review →", jobs_path(status: "blocked"), class: "sqd-btn sqd-btn--muted" %> diff --git a/config/routes.rb b/config/routes.rb index 713b71a..fbf7be9 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,6 @@ SolidQueueWeb::Engine.routes.draw do root to: "dashboard#index" - post "retry_all_failed", to: "dashboard#retry_all_failed", as: :retry_all_failed - post "discard_all_blocked", to: "dashboard#discard_all_blocked", as: :discard_all_blocked + resource :blocked_jobs, only: [:destroy] get "search", to: "search#index", as: :search get "history", to: "history#index", as: :history diff --git a/spec/requests/solid_queue_web/blocked_jobs_spec.rb b/spec/requests/solid_queue_web/blocked_jobs_spec.rb new file mode 100644 index 0000000..b45a001 --- /dev/null +++ b/spec/requests/solid_queue_web/blocked_jobs_spec.rb @@ -0,0 +1,38 @@ +require "rails_helper" + +RSpec.describe "BlockedJobs", type: :request do + let!(:blocked_job) do + j = SolidQueue::Job.create!( + queue_name: "default", class_name: "TestJob", + arguments: {}.to_json, active_job_id: SecureRandom.uuid + ) + j.ready_execution&.destroy + j.update!(concurrency_key: "TestJob/1") + allow_any_instance_of(SolidQueue::BlockedExecution).to receive(:set_expires_at) + SolidQueue::BlockedExecution.create!( + job: j, queue_name: j.queue_name, priority: j.priority, expires_at: 1.hour.from_now + ) + j + end + + describe "DELETE /jobs/blocked_jobs" do + it "discards all blocked jobs and redirects to the dashboard" do + delete "/jobs/blocked_jobs" + expect(response).to redirect_to("/jobs/") + follow_redirect! + expect(response.body).to include("discarded") + end + + it "removes the blocked execution" do + expect { delete "/jobs/blocked_jobs" }.to change(SolidQueue::BlockedExecution, :count).by(-1) + end + + it "handles failure gracefully" do + allow(SolidQueue::BlockedExecution).to receive(:discard_all_from_jobs).and_raise(RuntimeError, "boom") + delete "/jobs/blocked_jobs" + expect(response).to redirect_to("/jobs/") + follow_redirect! + expect(response.body).to include("Could not discard blocked jobs") + end + end +end diff --git a/spec/requests/solid_queue_web/dashboard_spec.rb b/spec/requests/solid_queue_web/dashboard_spec.rb index 87a3d77..088ad0d 100644 --- a/spec/requests/solid_queue_web/dashboard_spec.rb +++ b/spec/requests/solid_queue_web/dashboard_spec.rb @@ -46,74 +46,6 @@ end end - describe "POST /jobs/retry_all_failed" do - let!(:failed_job) do - j = SolidQueue::Job.create!( - queue_name: "default", class_name: "TestJob", - arguments: {}.to_json, active_job_id: SecureRandom.uuid - ) - SolidQueue::FailedExecution.create!( - job: j, - error: { exception_class: "RuntimeError", message: "boom", backtrace: [] } - ) - j - end - - it "retries all failed jobs and redirects to the dashboard" do - post "/jobs/retry_all_failed" - expect(response).to redirect_to("/jobs/") - follow_redirect! - expect(response.body).to include("queued for retry") - end - - it "removes the failed execution" do - expect { post "/jobs/retry_all_failed" }.to change(SolidQueue::FailedExecution, :count).by(-1) - end - - it "handles failure gracefully" do - allow(SolidQueue::FailedExecution).to receive(:retry_all).and_raise(RuntimeError, "boom") - post "/jobs/retry_all_failed" - expect(response).to redirect_to("/jobs/") - follow_redirect! - expect(response.body).to include("Could not retry failed jobs") - end - end - - describe "POST /jobs/discard_all_blocked" do - let!(:blocked_job) do - j = SolidQueue::Job.create!( - queue_name: "default", class_name: "TestJob", - arguments: {}.to_json, active_job_id: SecureRandom.uuid - ) - j.ready_execution&.destroy - j.update!(concurrency_key: "TestJob/1") - allow_any_instance_of(SolidQueue::BlockedExecution).to receive(:set_expires_at) - SolidQueue::BlockedExecution.create!( - job: j, queue_name: j.queue_name, priority: j.priority, expires_at: 1.hour.from_now - ) - j - end - - it "discards all blocked jobs and redirects to the dashboard" do - post "/jobs/discard_all_blocked" - expect(response).to redirect_to("/jobs/") - follow_redirect! - expect(response.body).to include("discarded") - end - - it "removes the blocked execution" do - expect { post "/jobs/discard_all_blocked" }.to change(SolidQueue::BlockedExecution, :count).by(-1) - end - - it "handles failure gracefully" do - allow(SolidQueue::BlockedExecution).to receive(:discard_all_from_jobs).and_raise(RuntimeError, "boom") - post "/jobs/discard_all_blocked" - expect(response).to redirect_to("/jobs/") - follow_redirect! - expect(response.body).to include("Could not discard blocked jobs") - end - end - describe "authentication" do after { SolidQueueWeb.instance_variable_set(:@authenticate, nil) } From a464c78e482d2be01184c8fd9fd82cf4f276f9ff Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Wed, 20 May 2026 17:51:24 -0400 Subject: [PATCH 6/6] docs: add Rails controller conventions to roadmap Track remaining non-standard actions: QueuesController pause/resume and Queues::JobsController discard_all. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 5050a14..2a8c285 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,9 @@ Planned features, roughly ordered by priority: - Multi-database support — when Solid Queue runs on a separate database from the host app - Read replica support — route dashboard queries to a replica to avoid impacting the primary +**Code quality** +- Rails controller conventions — complete the 7-action refactor: `QueuesController#pause` / `#resume` → `Queues::PausesController#create` / `#destroy`; `Queues::JobsController#discard_all` → map to `#destroy` matching the pattern already applied to `JobsController` + Pull requests for any of these are welcome. See [Contributing](#contributing) below. ## Contributing