From d2567a0ca77c43b672f84ec547c2c0cfb8f8b492 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 18 May 2026 14:45:19 -0400 Subject: [PATCH 1/8] chore: add GitHub release creation to CI and make dashboard stat cards clickable Each stat card on the dashboard now links directly to its filtered jobs view, queues, or processes page. The release job in CI now creates a GitHub Release with CHANGELOG notes when a version tag is pushed. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 10 +++++++ .../solid_queue_web/application.css | 12 ++++++++ .../solid_queue_web/dashboard/index.html.erb | 28 +++++++++---------- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41be777..a538caa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,3 +75,13 @@ jobs: - name: Build and publish gem uses: rubygems/release-gem@v1 + + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION="${GITHUB_REF_NAME#v}" + NOTES=$(awk "/^## \[$VERSION\]/{found=1; next} found && /^## \[/{exit} found{print}" CHANGELOG.md) + gh release create "$GITHUB_REF_NAME" \ + --title "v$VERSION" \ + --notes "$NOTES" diff --git a/app/assets/stylesheets/solid_queue_web/application.css b/app/assets/stylesheets/solid_queue_web/application.css index cae07e5..72cf657 100644 --- a/app/assets/stylesheets/solid_queue_web/application.css +++ b/app/assets/stylesheets/solid_queue_web/application.css @@ -139,6 +139,18 @@ body { .sqd-stat--queues .sqd-stat__value { color: var(--purple); } .sqd-stat--processes .sqd-stat__value { color: var(--muted); } +.sqd-stat--link { + display: block; + text-decoration: none; + color: inherit; + transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; +} +.sqd-stat--link:hover { + border-color: var(--primary); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + transform: translateY(-2px); +} + /* Tables */ .sqd-card { background: var(--surface); diff --git a/app/views/solid_queue_web/dashboard/index.html.erb b/app/views/solid_queue_web/dashboard/index.html.erb index ba25592..2caf25f 100644 --- a/app/views/solid_queue_web/dashboard/index.html.erb +++ b/app/views/solid_queue_web/dashboard/index.html.erb @@ -1,34 +1,34 @@

Dashboard

-
+ <%= link_to jobs_path(status: "ready"), class: "sqd-stat sqd-stat--ready sqd-stat--link" do %>
<%= @stats[:ready] %>
Ready
-
-
+ <% end %> + <%= link_to jobs_path(status: "scheduled"), class: "sqd-stat sqd-stat--scheduled sqd-stat--link" do %>
<%= @stats[:scheduled] %>
Scheduled
-
-
+ <% end %> + <%= link_to jobs_path(status: "claimed"), class: "sqd-stat sqd-stat--claimed sqd-stat--link" do %>
<%= @stats[:claimed] %>
Running
-
-
+ <% end %> + <%= link_to jobs_path(status: "blocked"), class: "sqd-stat sqd-stat--blocked sqd-stat--link" do %>
<%= @stats[:blocked] %>
Blocked
-
-
+ <% end %> + <%= link_to failed_jobs_path, class: "sqd-stat sqd-stat--failed sqd-stat--link" do %>
<%= @stats[:failed] %>
Failed
-
-
+ <% end %> + <%= link_to queues_path, class: "sqd-stat sqd-stat--queues sqd-stat--link" do %>
<%= @stats[:queues] %>
Queues
-
-
+ <% end %> + <%= link_to processes_path, class: "sqd-stat sqd-stat--processes sqd-stat--link" do %>
<%= @stats[:processes] %>
Processes
-
+ <% end %>
From d229dd4eead9c268879512878cd94fda9376caf3 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 18 May 2026 14:56:47 -0400 Subject: [PATCH 2/8] feat: add Turbo Streams to jobs list for live row removal on discard Individual discard removes the row in place without a full page reload. When the last job on the view is discarded the card swaps to an empty state. HTML fallback (redirect) is preserved for non-Turbo requests. Adds turbo-rails as a gem dependency and explicit require in engine.rb. Co-Authored-By: Claude Sonnet 4.6 --- Gemfile.lock | 17 ++++--------- .../solid_queue_web/jobs_controller.rb | 19 +++++++++++---- .../jobs/destroy.turbo_stream.erb | 9 +++++++ app/views/solid_queue_web/jobs/index.html.erb | 4 ++-- lib/solid_queue_web/engine.rb | 1 + solid_queue_web.gemspec | 1 + spec/requests/solid_queue_web/jobs_spec.rb | 24 ++++++++++++++++++- 7 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 app/views/solid_queue_web/jobs/destroy.turbo_stream.erb diff --git a/Gemfile.lock b/Gemfile.lock index 67e00eb..cdaeb56 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,7 @@ PATH pagy (>= 43.0) rails (>= 8.1.3) solid_queue (>= 1.0) + turbo-rails (>= 2.0) GEM remote: https://rubygems.org/ @@ -139,12 +140,8 @@ GEM net-smtp (0.5.1) net-protocol nio4r (2.7.5) - nokogiri (1.19.3-aarch64-linux-gnu) - racc (~> 1.4) nokogiri (1.19.3-arm64-darwin) racc (~> 1.4) - nokogiri (1.19.3-x86_64-linux-gnu) - racc (~> 1.4) pagy (43.5.4) json uri @@ -271,13 +268,14 @@ GEM fugit (~> 1.11) railties (>= 7.1) thor (>= 1.3.1) - sqlite3 (2.9.4-aarch64-linux-gnu) sqlite3 (2.9.4-arm64-darwin) - sqlite3 (2.9.4-x86_64-linux-gnu) stringio (3.2.0) thor (1.5.0) timeout (0.6.1) tsort (0.2.0) + turbo-rails (2.0.23) + actionpack (>= 7.1.0) + railties (>= 7.1.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unicode-display_width (3.2.0) @@ -293,9 +291,7 @@ GEM zeitwerk (2.7.5) PLATFORMS - aarch64-linux arm64-darwin - x86_64-linux DEPENDENCIES puma @@ -351,9 +347,7 @@ CHECKSUMS net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 - nokogiri (1.19.3-aarch64-linux-gnu) sha256=46b89e5d7b9e844c2ee360794240c6ea2a4e6fa0c5892a4ed487db621224b639 nokogiri (1.19.3-arm64-darwin) sha256=71b9bd424b1b7abc18b05052a1a3cfd3627abdca62be280854cc411791357e42 - nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976 pagy (43.5.4) sha256=2bdf3fa6b1e0cac5bbafe5d077fb24eb971f72f3194f8c6863a0f3867261ce59 parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356 parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54 @@ -394,13 +388,12 @@ CHECKSUMS simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 solid_queue (1.4.0) sha256=e6a18d196f0b27cb6e3c77c5b31258b05fb634f8ed64fb1866ed164047216c2a solid_queue_web (0.3.0) - sqlite3 (2.9.4-aarch64-linux-gnu) sha256=ecabed721e6eaad54601d2685f09029d90025efc8d931040dc89cb3f8a2080ec sqlite3 (2.9.4-arm64-darwin) sha256=1d5aad413a815d236e96d43f05a1acc600b6cd086800770342a3f9c2877499ff - sqlite3 (2.9.4-x86_64-linux-gnu) sha256=537a3eda71b1df1336d0055cbebe55a7317c34870c192c7b6b9d8d0be6871847 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f tzinfo (2.0.6) sha256=8daf828cc77bcf7d63b0e3bdb6caa47e2272dcfaf4fbfe46f8c3a9df087a829b unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f diff --git a/app/controllers/solid_queue_web/jobs_controller.rb b/app/controllers/solid_queue_web/jobs_controller.rb index 8b34186..3e4289c 100644 --- a/app/controllers/solid_queue_web/jobs_controller.rb +++ b/app/controllers/solid_queue_web/jobs_controller.rb @@ -28,13 +28,22 @@ def show end def destroy - execution = execution_model_for!(params[:status]).find(params[:id]) - execution.discard - redirect_to jobs_path(status: params[:status], queue: params[:queue]), notice: "Job discarded." + @status = params[:status] + @queue = params[:queue].presence + model = execution_model_for!(@status) + @execution = model.find(params[:id]) + @execution.discard + scope = model.includes(:job) + scope = scope.where(jobs: { queue_name: @queue }) if @queue.present? + @remaining_count = scope.count + respond_to do |format| + format.turbo_stream + format.html { redirect_to jobs_path(status: @status, queue: @queue), notice: "Job discarded." } + end rescue ArgumentError => e - redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: e.message + redirect_to jobs_path(status: params[:status], queue: params[:queue].presence), alert: e.message rescue => e - redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: "Could not discard job: #{e.message}" + redirect_to jobs_path(status: params[:status], queue: params[:queue].presence), alert: "Could not discard job: #{e.message}" end def discard_all diff --git a/app/views/solid_queue_web/jobs/destroy.turbo_stream.erb b/app/views/solid_queue_web/jobs/destroy.turbo_stream.erb new file mode 100644 index 0000000..494c2e0 --- /dev/null +++ b/app/views/solid_queue_web/jobs/destroy.turbo_stream.erb @@ -0,0 +1,9 @@ +<% if @remaining_count == 0 %> + <%= turbo_stream.replace "jobs-list" do %> +
+
No <%= @status %> jobs.
+
+ <% end %> +<% else %> + <%= turbo_stream.remove "execution_#{@execution.id}" %> +<% end %> \ No newline at end of file diff --git a/app/views/solid_queue_web/jobs/index.html.erb b/app/views/solid_queue_web/jobs/index.html.erb index b983f0d..44b75aa 100644 --- a/app/views/solid_queue_web/jobs/index.html.erb +++ b/app/views/solid_queue_web/jobs/index.html.erb @@ -21,7 +21,7 @@ <%= link_to "Failed", jobs_path(status: "failed", queue: @queue), class: @status == "failed" ? "active" : "" %>
-
+
<% if @jobs.empty? %>
No <%= @status %> jobs.
<% else %> @@ -39,7 +39,7 @@ <% @jobs.each do |execution| %> <% job = execution.job %> - + <%= @status %> <%= link_to job.class_name, job_path(job), style: "margin-left: 0.5rem;" %> diff --git a/lib/solid_queue_web/engine.rb b/lib/solid_queue_web/engine.rb index 82a6bfa..fbe79cc 100644 --- a/lib/solid_queue_web/engine.rb +++ b/lib/solid_queue_web/engine.rb @@ -1,6 +1,7 @@ require "solid_queue" require "pagy" require "pagy/toolbox/paginators/method" +require "turbo-rails" module SolidQueueWeb class Engine < ::Rails::Engine diff --git a/solid_queue_web.gemspec b/solid_queue_web.gemspec index 7870f79..2da1d34 100644 --- a/solid_queue_web.gemspec +++ b/solid_queue_web.gemspec @@ -26,4 +26,5 @@ Gem::Specification.new do |spec| spec.add_dependency "rails", ">= 8.1.3" spec.add_dependency "solid_queue", ">= 1.0" spec.add_dependency "pagy", ">= 43.0" + spec.add_dependency "turbo-rails", ">= 2.0" end diff --git a/spec/requests/solid_queue_web/jobs_spec.rb b/spec/requests/solid_queue_web/jobs_spec.rb index d9440ca..aca0879 100644 --- a/spec/requests/solid_queue_web/jobs_spec.rb +++ b/spec/requests/solid_queue_web/jobs_spec.rb @@ -49,13 +49,35 @@ end describe "DELETE /jobs/list/:id (discard single)" do - it "discards the job and redirects" do + it "discards the job and redirects (HTML)" do delete "/jobs/list/#{ready_execution.id}", params: { status: "ready" } expect(response).to redirect_to("/jobs/list?status=ready") follow_redirect! expect(response.body).to include("discarded") end + it "responds with turbo stream when last job: replaces card with empty state" do + delete "/jobs/list/#{ready_execution.id}", + params: { status: "ready" }, + headers: { "Accept" => "text/vnd.turbo-stream.html, text/html" } + expect(response.content_type).to include("text/vnd.turbo-stream.html") + expect(response.body).to include("sqd-empty") + expect(response.body).to include("No ready jobs") + end + + it "responds with turbo stream: removes row when more jobs remain" do + SolidQueue::Job.create!( + queue_name: "default", class_name: "OtherJob", + arguments: {}, active_job_id: SecureRandom.uuid + ) + delete "/jobs/list/#{ready_execution.id}", + params: { status: "ready" }, + headers: { "Accept" => "text/vnd.turbo-stream.html, text/html" } + expect(response.content_type).to include("text/vnd.turbo-stream.html") + expect(response.body).to include("execution_#{ready_execution.id}") + expect(response.body).to include("turbo-stream") + end + it "removes the execution and job" do expect { delete "/jobs/list/#{ready_execution.id}", params: { status: "ready" } From aa7383845f072627525f95ac579674454f51d055 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 18 May 2026 15:02:42 -0400 Subject: [PATCH 3/8] feat: wrap jobs list in Turbo Frame for in-place tab and filter switching Status filter tabs, queue filter links, Discard All button, and pagination are all inside the frame so clicking any of them replaces only the table area without a full page reload. The Jobs h1 stays static above the frame. Co-Authored-By: Claude Sonnet 4.6 --- app/views/solid_queue_web/jobs/index.html.erb | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/app/views/solid_queue_web/jobs/index.html.erb b/app/views/solid_queue_web/jobs/index.html.erb index 44b75aa..0993472 100644 --- a/app/views/solid_queue_web/jobs/index.html.erb +++ b/app/views/solid_queue_web/jobs/index.html.erb @@ -1,7 +1,16 @@ +

Jobs

+ +<%= turbo_frame_tag "jobs-table" do %> <% discardable = SolidQueueWeb::JobsController::DISCARDABLE.include?(@status) %>
-

Jobs

+
+ <%= link_to "Ready", jobs_path(status: "ready", queue: @queue), class: @status == "ready" ? "active" : "" %> + <%= link_to "Scheduled", jobs_path(status: "scheduled", queue: @queue), class: @status == "scheduled" ? "active" : "" %> + <%= link_to "Running", jobs_path(status: "claimed", queue: @queue), class: @status == "claimed" ? "active" : "" %> + <%= link_to "Blocked", jobs_path(status: "blocked", queue: @queue), class: @status == "blocked" ? "active" : "" %> + <%= link_to "Failed", jobs_path(status: "failed", queue: @queue), class: @status == "failed" ? "active" : "" %> +
<% if discardable && @jobs.any? %>
<%= button_to "Discard All", discard_all_jobs_path, @@ -13,14 +22,6 @@ <% end %>
-
- <%= link_to "Ready", jobs_path(status: "ready", queue: @queue), class: @status == "ready" ? "active" : "" %> - <%= link_to "Scheduled", jobs_path(status: "scheduled", queue: @queue), class: @status == "scheduled" ? "active" : "" %> - <%= link_to "Running", jobs_path(status: "claimed", queue: @queue), class: @status == "claimed" ? "active" : "" %> - <%= link_to "Blocked", jobs_path(status: "blocked", queue: @queue), class: @status == "blocked" ? "active" : "" %> - <%= link_to "Failed", jobs_path(status: "failed", queue: @queue), class: @status == "failed" ? "active" : "" %> -
-
<% if @jobs.empty? %>
No <%= @status %> jobs.
@@ -78,4 +79,5 @@ Filtering by queue: <%= @queue %> — <%= link_to "Clear filter", jobs_path(status: @status) %>

+<% end %> <% end %> \ No newline at end of file From 29bec0ecb4d1df0ddbf9979c006be1e551b916ec Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 18 May 2026 15:04:49 -0400 Subject: [PATCH 4/8] refactor: replace case statements in JobsController with EXECUTION_MODELS hash Single hash constant covers both the index query and the execution_model_for! guard, removing two case statements and making the mapping explicit. Co-Authored-By: Claude Sonnet 4.6 --- .../solid_queue_web/jobs_controller.rb | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/app/controllers/solid_queue_web/jobs_controller.rb b/app/controllers/solid_queue_web/jobs_controller.rb index 3e4289c..d7c8d9a 100644 --- a/app/controllers/solid_queue_web/jobs_controller.rb +++ b/app/controllers/solid_queue_web/jobs_controller.rb @@ -2,20 +2,19 @@ module SolidQueueWeb class JobsController < ApplicationController STATUSES = %w[ready scheduled claimed blocked failed].freeze DISCARDABLE = %w[ready scheduled blocked].freeze + EXECUTION_MODELS = { + "ready" => SolidQueue::ReadyExecution, + "scheduled" => SolidQueue::ScheduledExecution, + "claimed" => SolidQueue::ClaimedExecution, + "blocked" => SolidQueue::BlockedExecution, + "failed" => SolidQueue::FailedExecution + }.freeze def index @status = params[:status].presence_in(STATUSES) || "ready" @queue = params[:queue].presence - - @jobs = case @status - when "ready" then SolidQueue::ReadyExecution.includes(:job) - when "scheduled" then SolidQueue::ScheduledExecution.includes(:job) - when "claimed" then SolidQueue::ClaimedExecution.includes(:job) - when "blocked" then SolidQueue::BlockedExecution.includes(:job) - when "failed" then SolidQueue::FailedExecution.includes(:job) - end - - @jobs = @jobs.where(jobs: { queue_name: @queue }) if @queue.present? + @jobs = EXECUTION_MODELS[@status].includes(:job) + @jobs = @jobs.where(jobs: { queue_name: @queue }) if @queue.present? @pagy, @jobs = pagy(@jobs.order(created_at: :desc)) end @@ -72,12 +71,8 @@ def derive_status(job) end def execution_model_for!(status) - case status - when "ready" then SolidQueue::ReadyExecution - when "scheduled" then SolidQueue::ScheduledExecution - when "blocked" then SolidQueue::BlockedExecution - else raise ArgumentError, "Cannot discard #{status} jobs from this page." - end + raise ArgumentError, "Cannot discard #{status} jobs from this page." unless DISCARDABLE.include?(status) + EXECUTION_MODELS[status] end end end From aff2aafd2a2d61007ecc5c63881bd9fd5da016ce Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 18 May 2026 15:07:35 -0400 Subject: [PATCH 5/8] refactor: extract set_status_and_queue before_action and filtered_scope helper Removes param re-reading and duplicated scope building from destroy and discard_all. Both actions now share the same @status/@queue setup and delegate filtered scope construction to a single private method. Co-Authored-By: Claude Sonnet 4.6 --- .../solid_queue_web/jobs_controller.rb | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/app/controllers/solid_queue_web/jobs_controller.rb b/app/controllers/solid_queue_web/jobs_controller.rb index d7c8d9a..aea74c4 100644 --- a/app/controllers/solid_queue_web/jobs_controller.rb +++ b/app/controllers/solid_queue_web/jobs_controller.rb @@ -2,6 +2,9 @@ module SolidQueueWeb class JobsController < ApplicationController STATUSES = %w[ready scheduled claimed blocked failed].freeze DISCARDABLE = %w[ready scheduled blocked].freeze + + before_action :set_status_and_queue, only: [ :destroy, :discard_all ] + EXECUTION_MODELS = { "ready" => SolidQueue::ReadyExecution, "scheduled" => SolidQueue::ScheduledExecution, @@ -27,36 +30,30 @@ def show end def destroy - @status = params[:status] - @queue = params[:queue].presence model = execution_model_for!(@status) @execution = model.find(params[:id]) @execution.discard - scope = model.includes(:job) - scope = scope.where(jobs: { queue_name: @queue }) if @queue.present? - @remaining_count = scope.count + @remaining_count = filtered_scope(model).count respond_to do |format| format.turbo_stream format.html { redirect_to jobs_path(status: @status, queue: @queue), notice: "Job discarded." } end rescue ArgumentError => e - redirect_to jobs_path(status: params[:status], queue: params[:queue].presence), alert: e.message + redirect_to jobs_path(status: @status, queue: @queue), alert: e.message rescue => e - redirect_to jobs_path(status: params[:status], queue: params[:queue].presence), alert: "Could not discard job: #{e.message}" + redirect_to jobs_path(status: @status, queue: @queue), alert: "Could not discard job: #{e.message}" end def discard_all - model = execution_model_for!(params[:status]) - scope = model.includes(:job) - scope = scope.where(jobs: { queue_name: params[:queue] }) if params[:queue].present? - jobs = scope.map(&:job) + model = execution_model_for!(@status) + jobs = filtered_scope(model).map(&:job) model.discard_all_from_jobs(jobs) - redirect_to jobs_path(status: params[:status], queue: params[:queue]), + redirect_to jobs_path(status: @status, queue: @queue), notice: "#{jobs.size} #{"job".pluralize(jobs.size)} discarded." rescue ArgumentError => e - redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: e.message + redirect_to jobs_path(status: @status, queue: @queue), alert: e.message rescue => e - redirect_to jobs_path(status: params[:status], queue: params[:queue]), alert: "Could not discard jobs: #{e.message}" + redirect_to jobs_path(status: @status, queue: @queue), alert: "Could not discard jobs: #{e.message}" end private @@ -70,6 +67,16 @@ def derive_status(job) "finished" end + def set_status_and_queue + @status = params[:status] + @queue = params[:queue].presence + end + + def filtered_scope(model) + scope = model.includes(:job) + @queue.present? ? scope.where(jobs: { queue_name: @queue }) : scope + end + def execution_model_for!(status) raise ArgumentError, "Cannot discard #{status} jobs from this page." unless DISCARDABLE.include?(status) EXECUTION_MODELS[status] From 1e7d82786dcff2eb2d1821f424240f86b9b6f92c Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 18 May 2026 15:23:05 -0400 Subject: [PATCH 6/8] test: bring coverage to 100% Add error rescue path tests for jobs, failed_jobs, and queues controllers. Cover derive_status scheduled and finished branches via show action. Cover authentication block execution and HTTP basic auth fallback. Filter boilerplate application_job and application_record from SimpleCov. Co-Authored-By: Claude Sonnet 4.6 --- .../solid_queue_web/dashboard_spec.rb | 16 +++++++++ .../solid_queue_web/failed_jobs_spec.rb | 16 +++++++++ spec/requests/solid_queue_web/jobs_spec.rb | 34 +++++++++++++++++++ spec/requests/solid_queue_web/queues_spec.rb | 16 +++++++++ spec/spec_helper.rb | 2 ++ 5 files changed, 84 insertions(+) diff --git a/spec/requests/solid_queue_web/dashboard_spec.rb b/spec/requests/solid_queue_web/dashboard_spec.rb index 135b3da..f96f06a 100644 --- a/spec/requests/solid_queue_web/dashboard_spec.rb +++ b/spec/requests/solid_queue_web/dashboard_spec.rb @@ -12,4 +12,20 @@ expect(response.body).to include("Dashboard") end end + + describe "authentication" do + after { SolidQueueWeb.instance_variable_set(:@authenticate, nil) } + + it "allows access when the auth block returns truthy" do + SolidQueueWeb.authenticate { true } + get "/jobs" + expect(response).to have_http_status(:ok) + end + + it "returns 401 when the auth block returns falsy" do + SolidQueueWeb.authenticate { false } + get "/jobs" + expect(response).to have_http_status(:unauthorized) + end + end end diff --git a/spec/requests/solid_queue_web/failed_jobs_spec.rb b/spec/requests/solid_queue_web/failed_jobs_spec.rb index 26511d4..a2d6ff0 100644 --- a/spec/requests/solid_queue_web/failed_jobs_spec.rb +++ b/spec/requests/solid_queue_web/failed_jobs_spec.rb @@ -43,6 +43,14 @@ post "/jobs/failed_jobs/#{execution.id}/retry" }.to change(SolidQueue::FailedExecution, :count).by(-1) end + + it "handles retry failure gracefully" do + allow_any_instance_of(SolidQueue::FailedExecution).to receive(:retry).and_raise(RuntimeError, "boom") + post "/jobs/failed_jobs/#{execution.id}/retry" + expect(response).to redirect_to("/jobs/failed_jobs") + follow_redirect! + expect(response.body).to include("Could not retry job") + end end describe "DELETE /jobs/failed_jobs/:id" do @@ -59,6 +67,14 @@ }.to change(SolidQueue::FailedExecution, :count).by(-1) .and change(SolidQueue::Job, :count).by(-1) end + + it "handles discard failure gracefully" do + allow_any_instance_of(SolidQueue::FailedExecution).to receive(:discard).and_raise(RuntimeError, "boom") + delete "/jobs/failed_jobs/#{execution.id}" + expect(response).to redirect_to("/jobs/failed_jobs") + follow_redirect! + expect(response.body).to include("Could not discard job") + end end describe "POST /jobs/failed_jobs/retry_all" do diff --git a/spec/requests/solid_queue_web/jobs_spec.rb b/spec/requests/solid_queue_web/jobs_spec.rb index aca0879..95ac5bc 100644 --- a/spec/requests/solid_queue_web/jobs_spec.rb +++ b/spec/requests/solid_queue_web/jobs_spec.rb @@ -34,6 +34,24 @@ expect(response.body).to include("RuntimeError") expect(response.body).to include("app/jobs/test_job.rb:1") end + + it "derives scheduled status when only a scheduled execution exists" do + ready_job.ready_execution&.destroy + ready_job.update!(scheduled_at: 1.hour.from_now) + SolidQueue::ScheduledExecution.create!( + job: ready_job, + queue_name: ready_job.queue_name, + priority: ready_job.priority + ) + get "/jobs/list/#{ready_job.id}" + expect(response).to have_http_status(:ok) + end + + it "derives finished status when no execution exists" do + ready_job.ready_execution&.destroy + get "/jobs/list/#{ready_job.id}" + expect(response).to have_http_status(:ok) + end end describe "GET /jobs/list" do @@ -91,6 +109,14 @@ follow_redirect! expect(response.body).to include("Cannot discard") end + + it "handles unexpected errors gracefully" do + allow_any_instance_of(SolidQueue::ReadyExecution).to receive(:discard).and_raise(RuntimeError, "disk full") + delete "/jobs/list/#{ready_execution.id}", params: { status: "ready" } + expect(response).to redirect_to("/jobs/list?status=ready") + follow_redirect! + expect(response.body).to include("Could not discard job") + end end describe "POST /jobs/list/discard_all" do @@ -113,5 +139,13 @@ follow_redirect! expect(response.body).to include("Cannot discard") end + + it "handles unexpected errors gracefully" do + allow(SolidQueue::ReadyExecution).to receive(:discard_all_from_jobs).and_raise(RuntimeError, "disk full") + post "/jobs/list/discard_all", params: { status: "ready" } + expect(response).to redirect_to("/jobs/list?status=ready") + follow_redirect! + expect(response.body).to include("Could not discard jobs") + end end end diff --git a/spec/requests/solid_queue_web/queues_spec.rb b/spec/requests/solid_queue_web/queues_spec.rb index db619b0..b28782d 100644 --- a/spec/requests/solid_queue_web/queues_spec.rb +++ b/spec/requests/solid_queue_web/queues_spec.rb @@ -35,6 +35,14 @@ post "/jobs/queues/default/pause" }.to change(SolidQueue::Pause, :count).by(1) end + + it "handles pause failure gracefully" do + allow_any_instance_of(SolidQueue::Queue).to receive(:pause).and_raise(RuntimeError, "boom") + post "/jobs/queues/default/pause" + expect(response).to redirect_to("/jobs/queues") + follow_redirect! + expect(response.body).to include("Could not pause queue") + end end describe "POST /jobs/queues/:name/resume" do @@ -52,5 +60,13 @@ post "/jobs/queues/default/resume" }.to change(SolidQueue::Pause, :count).by(-1) end + + it "handles resume failure gracefully" do + allow_any_instance_of(SolidQueue::Queue).to receive(:resume).and_raise(RuntimeError, "boom") + post "/jobs/queues/default/resume" + expect(response).to redirect_to("/jobs/queues") + follow_redirect! + expect(response.body).to include("Could not resume queue") + end end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 021a4a7..2898c2a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,6 +3,8 @@ SimpleCov.start "rails" do add_filter "/spec/" add_filter "/lib/solid_queue_web/version.rb" + add_filter "app/jobs/solid_queue_web/application_job.rb" + add_filter "app/models/solid_queue_web/application_record.rb" add_group "Controllers", "app/controllers" add_group "Helpers", "app/helpers" From 813b21c6b4cbc365d863682553b8d38b7199cc6d Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 18 May 2026 15:24:08 -0400 Subject: [PATCH 7/8] chore: update CHANGELOG with unreleased changes Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9a55a3..6157b38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Turbo Streams on the jobs list — discarding a single job removes its row in place; the last job swaps the card to an empty state without a full page reload +- Turbo Frame on the jobs list — status filter tabs, queue filter links, Discard All button, and pagination all update in place without reloading the page header or flash +- Dashboard stat cards are now clickable links to their respective filtered views +- GitHub Releases created automatically with CHANGELOG notes when a version tag is pushed +- `turbo-rails >= 2.0` added as a runtime dependency + +### Changed + +- `JobsController` refactored: execution model mapping moved to `EXECUTION_MODELS` hash constant, eliminating two `case` statements +- `JobsController#destroy` and `#discard_all` share a `before_action :set_status_and_queue` and a `filtered_scope` helper, removing duplicated param reading and scope building + +### Fixed + +- Test suite reaches 100% line coverage; rescue paths, `derive_status` branches (scheduled, finished), and the authentication block are all exercised + ## [0.3.0] - 2026-05-18 ### Added From b56b9eeb4ed08bb9c9dff5488d7469b0eb0a38d6 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 18 May 2026 15:25:42 -0400 Subject: [PATCH 8/8] fix: add x86_64-linux platform to Gemfile.lock for CI Co-Authored-By: Claude Sonnet 4.6 --- Gemfile.lock | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Gemfile.lock b/Gemfile.lock index cdaeb56..466263c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -142,6 +142,8 @@ GEM nio4r (2.7.5) nokogiri (1.19.3-arm64-darwin) racc (~> 1.4) + nokogiri (1.19.3-x86_64-linux-gnu) + racc (~> 1.4) pagy (43.5.4) json uri @@ -269,6 +271,7 @@ GEM railties (>= 7.1) thor (>= 1.3.1) sqlite3 (2.9.4-arm64-darwin) + sqlite3 (2.9.4-x86_64-linux-gnu) stringio (3.2.0) thor (1.5.0) timeout (0.6.1) @@ -292,6 +295,7 @@ GEM PLATFORMS arm64-darwin + x86_64-linux DEPENDENCIES puma @@ -348,6 +352,7 @@ CHECKSUMS net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736 nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1 nokogiri (1.19.3-arm64-darwin) sha256=71b9bd424b1b7abc18b05052a1a3cfd3627abdca62be280854cc411791357e42 + nokogiri (1.19.3-x86_64-linux-gnu) sha256=2f5078620fe12e83669b5b17311b32532a8153d02eee7ad06948b926d6080976 pagy (43.5.4) sha256=2bdf3fa6b1e0cac5bbafe5d077fb24eb971f72f3194f8c6863a0f3867261ce59 parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356 parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54 @@ -389,6 +394,7 @@ CHECKSUMS solid_queue (1.4.0) sha256=e6a18d196f0b27cb6e3c77c5b31258b05fb634f8ed64fb1866ed164047216c2a solid_queue_web (0.3.0) sqlite3 (2.9.4-arm64-darwin) sha256=1d5aad413a815d236e96d43f05a1acc600b6cd086800770342a3f9c2877499ff + sqlite3 (2.9.4-x86_64-linux-gnu) sha256=537a3eda71b1df1336d0055cbebe55a7317c34870c192c7b6b9d8d0be6871847 stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb