From 0553211c450b831c89c20739c5211714560046c5 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 16:57:02 -0400 Subject: [PATCH 1/8] feat: add Stimulus via importmap-rails; refactor bulk selection to use selection controller Add importmap-rails as a dependency and wire it into the engine initializer so the host app's importmap picks up the engine's JS automatically. Add a selection_controller.js (ported from solid_queue_dashboard) that manages checkbox state, select-all toggling, and form injection at submit time. Update the jobs index to use Stimulus data-targets instead of the inline onclick select-all hack; the discard form is now external to the table and the controller injects checked IDs at submit time. Co-Authored-By: Claude Sonnet 4.6 --- app/javascript/solid_stack_web/application.js | 6 +++ .../solid_stack_web/selection_controller.js | 42 +++++++++++++++++++ .../solid_stack_web/application.html.erb | 2 +- app/views/solid_stack_web/jobs/index.html.erb | 40 ++++++++++-------- config/importmap.rb | 2 + lib/solid_stack_web/engine.rb | 14 +++++++ solid_stack_web.gemspec | 1 + 7 files changed, 89 insertions(+), 18 deletions(-) create mode 100644 app/javascript/solid_stack_web/application.js create mode 100644 app/javascript/solid_stack_web/selection_controller.js create mode 100644 config/importmap.rb diff --git a/app/javascript/solid_stack_web/application.js b/app/javascript/solid_stack_web/application.js new file mode 100644 index 0000000..e4dbd2f --- /dev/null +++ b/app/javascript/solid_stack_web/application.js @@ -0,0 +1,6 @@ +import "@hotwired/turbo" +import { Application } from "@hotwired/stimulus" +import SelectionController from "solid_stack_web/selection_controller" + +const application = Application.start() +application.register("selection", SelectionController) \ No newline at end of file diff --git a/app/javascript/solid_stack_web/selection_controller.js b/app/javascript/solid_stack_web/selection_controller.js new file mode 100644 index 0000000..d82f05b --- /dev/null +++ b/app/javascript/solid_stack_web/selection_controller.js @@ -0,0 +1,42 @@ +import { Controller } from "@hotwired/stimulus" + +export default class extends Controller { + static targets = ["checkbox", "selectAll", "bar", "count"] + + toggle() { + this._update() + } + + selectAll({ target }) { + this.checkboxTargets.forEach(cb => cb.checked = target.checked) + this._update() + } + + submit({ params: { formId } }) { + const form = document.getElementById(formId) + if (!form) return + form.querySelectorAll("[data-injected-id]").forEach(el => el.remove()) + this.checkboxTargets + .filter(cb => cb.checked) + .forEach(cb => { + const input = document.createElement("input") + input.type = "hidden" + input.name = "job_ids[]" + input.value = cb.value + input.dataset.injectedId = true + form.appendChild(input) + }) + form.requestSubmit() + } + + _update() { + const checked = this.checkboxTargets.filter(cb => cb.checked).length + const total = this.checkboxTargets.length + if (this.hasBarTarget) this.barTarget.style.display = checked > 0 ? "" : "none" + if (this.hasCountTarget) this.countTarget.textContent = checked + if (this.hasSelectAllTarget) { + this.selectAllTarget.indeterminate = checked > 0 && checked < total + this.selectAllTarget.checked = total > 0 && checked === total + } + } +} \ No newline at end of file diff --git a/app/views/layouts/solid_stack_web/application.html.erb b/app/views/layouts/solid_stack_web/application.html.erb index b34694c..3e1f833 100644 --- a/app/views/layouts/solid_stack_web/application.html.erb +++ b/app/views/layouts/solid_stack_web/application.html.erb @@ -7,7 +7,7 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= inline_styles %> - + <%= javascript_importmap_tags "solid_stack_web" %>
diff --git a/app/views/solid_stack_web/jobs/index.html.erb b/app/views/solid_stack_web/jobs/index.html.erb index f0f73e6..b8ac58f 100644 --- a/app/views/solid_stack_web/jobs/index.html.erb +++ b/app/views/solid_stack_web/jobs/index.html.erb @@ -61,20 +61,22 @@
<% if @executions.any? %> - <%= form_with url: job_selection_path, - method: :delete, - data: { turbo_frame: "_top" } do |f| %> - <%= f.hidden_field :status, value: @status %> - <%= f.hidden_field :q, value: @search %> - <%= f.hidden_field :queue, value: @queue %> - <%= f.hidden_field :period, value: @period %> - <%= f.hidden_field :priority, value: @priority %> - +
"> <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %> -
- <%= f.submit "Discard Selected", - class: "sqw-btn sqw-btn--danger sqw-btn--sm", - data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } %> + <%= form_with url: job_selection_path, method: :delete, id: "job-selection-form", + data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } do |f| %> + <%= f.hidden_field :status, value: @status %> + <%= f.hidden_field :q, value: @search %> + <%= f.hidden_field :queue, value: @queue %> + <%= f.hidden_field :period, value: @period %> + <%= f.hidden_field :priority, value: @priority %> + <% end %> + + <% end %> @@ -83,7 +85,8 @@ <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %> + data-selection-target="selectAll" + data-action="change->selection#selectAll"> <% end %> Job Class Queue @@ -97,8 +100,11 @@ <% @executions.each do |execution| %> <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %> - + <% end %> <%= link_to execution.job.class_name, job_path(execution.id, status: @status), data: { turbo_frame: "_top" } %> <%= execution.job.queue_name %> @@ -119,7 +125,7 @@ <%== pagy_nav(@pagy) if @pagy.pages > 1 %> - <% end %> +
<% else %> <%= render "empty" %> <% end %> diff --git a/config/importmap.rb b/config/importmap.rb new file mode 100644 index 0000000..b80db06 --- /dev/null +++ b/config/importmap.rb @@ -0,0 +1,2 @@ +pin "solid_stack_web", to: "solid_stack_web/application.js" +pin "solid_stack_web/selection_controller", to: "solid_stack_web/selection_controller.js" diff --git a/lib/solid_stack_web/engine.rb b/lib/solid_stack_web/engine.rb index 60c8fad..cf709cd 100644 --- a/lib/solid_stack_web/engine.rb +++ b/lib/solid_stack_web/engine.rb @@ -4,6 +4,7 @@ require "solid_cache" require "solid_cable" require "turbo-rails" +require "importmap-rails" module SolidStackWeb class Engine < ::Rails::Engine @@ -11,6 +12,19 @@ class Engine < ::Rails::Engine config.i18n.load_path += Gem.find_files("pagy/locales/en.yml") + initializer "solid_stack_web.assets" do |app| + if app.config.respond_to?(:assets) + app.config.assets.paths << root.join("app/javascript") + end + end + + initializer "solid_stack_web.importmap", before: "importmap" do |app| + if app.config.respond_to?(:importmap) + app.config.importmap.paths << root.join("config/importmap.rb") + app.config.importmap.cache_sweepers << root.join("app/javascript") + end + end + initializer "solid_stack_web.pagy" do |app| app.config.after_initialize do Pagy::OPTIONS[:limit] = SolidStackWeb.page_size diff --git a/solid_stack_web.gemspec b/solid_stack_web.gemspec index 3f12988..59de4e7 100644 --- a/solid_stack_web.gemspec +++ b/solid_stack_web.gemspec @@ -28,5 +28,6 @@ Gem::Specification.new do |spec| spec.add_dependency "solid_cache", ">= 1.0" spec.add_dependency "solid_cable", ">= 1.0" spec.add_dependency "turbo-rails", ">= 2.0" + spec.add_dependency "importmap-rails", ">= 1.2" spec.add_dependency "csv", ">= 3.0" end From 0abe9e2abc9f38c2492946a7e904ae08461152c9 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 16:59:30 -0400 Subject: [PATCH 2/8] feat: bulk retry and discard for failed jobs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add FailedJobs::SelectionsController with create (bulk retry) and destroy (bulk discard) actions, routed as DELETE/POST /failed_jobs/selection. The failed jobs index now uses the Stimulus selection controller with two separate forms — one for retry, one for discard — so the same checkbox selection drives both actions from a single selection bar. Co-Authored-By: Claude Sonnet 4.6 --- .../failed_jobs/selections_controller.rb | 22 ++++ .../failed_jobs/index.html.erb | 85 ++++++++----- config/routes.rb | 3 +- .../failed_job_selections_spec.rb | 118 ++++++++++++++++++ 4 files changed, 198 insertions(+), 30 deletions(-) create mode 100644 app/controllers/solid_stack_web/failed_jobs/selections_controller.rb create mode 100644 spec/requests/solid_stack_web/failed_job_selections_spec.rb diff --git a/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb b/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb new file mode 100644 index 0000000..006de67 --- /dev/null +++ b/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb @@ -0,0 +1,22 @@ +module SolidStackWeb + module FailedJobs + class SelectionsController < ApplicationController + def create + ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?) + SolidQueue::FailedExecution.where(id: ids).each(&:retry) + redirect_to failed_jobs_path + rescue => e + redirect_to failed_jobs_path, alert: "Could not retry jobs: #{e.message}" + end + + def destroy + ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?) + job_ids = SolidQueue::FailedExecution.where(id: ids).pluck(:job_id) + SolidQueue::Job.where(id: job_ids).destroy_all + redirect_to failed_jobs_path + rescue => e + redirect_to failed_jobs_path, alert: "Could not discard jobs: #{e.message}" + end + end + end +end diff --git a/app/views/solid_stack_web/failed_jobs/index.html.erb b/app/views/solid_stack_web/failed_jobs/index.html.erb index cd8e220..2b77d0e 100644 --- a/app/views/solid_stack_web/failed_jobs/index.html.erb +++ b/app/views/solid_stack_web/failed_jobs/index.html.erb @@ -8,38 +8,65 @@
<% if @executions.any? %> - - - - - - - - - - - - <% @executions.each do |execution| %> - - - - - - +
+ <%= form_with url: failed_job_selection_path, method: :post, id: "retry-selection-form" do |f| %> + <% end %> + + <%= form_with url: failed_job_selection_path, method: :delete, id: "discard-selection-form", + data: { turbo_confirm: "Discard selected failed jobs? This cannot be undone." } do |f| %> + <% end %> + + + +
Job ClassQueueErrorFailed At
<%= execution.job.class_name %><%= execution.job.queue_name %><%= execution.exception_class %><%= execution.created_at.strftime("%b %d %H:%M") %> - <%= button_to "Retry", retry_failed_job_path(execution), - method: :post, class: "sqw-btn sqw-btn--sm" %> - <%= button_to "Discard", failed_job_path(execution), - method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm", - data: { turbo_confirm: "Discard this job?" } %> -
+ + + + + + + + - <% end %> - -
Job ClassQueueErrorFailed At
- <%== pagy_nav(@pagy) if @pagy.pages > 1 %> + + + <% @executions.each do |execution| %> + + + <%= execution.job.class_name %> + <%= execution.job.queue_name %> + <%= execution.exception_class %> + <%= execution.created_at.strftime("%b %d %H:%M") %> + + <%= button_to "Retry", retry_failed_job_path(execution), + method: :post, class: "sqw-btn sqw-btn--sm" %> + <%= button_to "Discard", failed_job_path(execution), + method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm", + data: { turbo_confirm: "Discard this job?" } %> + + + <% end %> + + + <%== pagy_nav(@pagy) if @pagy.pages > 1 %> +
<% else %>

No failed jobs.

<% end %> -
+
\ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 2dd70bf..7317639 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,8 @@ SolidStackWeb::Engine.routes.draw do root to: "dashboard#index" - resource :job_selection, path: "jobs/selection", only: [:destroy], controller: "jobs/selections" + resource :job_selection, path: "jobs/selection", only: [:destroy], controller: "jobs/selections" + resource :failed_job_selection, path: "failed_jobs/selection", only: [:create, :destroy], controller: "failed_jobs/selections" resources :jobs, only: [:index, :show, :destroy] do collection do diff --git a/spec/requests/solid_stack_web/failed_job_selections_spec.rb b/spec/requests/solid_stack_web/failed_job_selections_spec.rb new file mode 100644 index 0000000..d82da7e --- /dev/null +++ b/spec/requests/solid_stack_web/failed_job_selections_spec.rb @@ -0,0 +1,118 @@ +require "rails_helper" + +RSpec.describe "Failed Job Selections", type: :request do + let(:engine_root) { "/solid_stack" } + + def create_failed(class_name: "FailingJob", queue_name: "default") + SolidQueue::Job.skip_callback(:create, :after, :prepare_for_execution) + job = SolidQueue::Job.create!( + class_name:, queue_name:, priority: 0, + arguments: { "executions" => 0, "exception_executions" => {} } + ) + execution = SolidQueue::FailedExecution.create!( + job: job, + error: { exception_class: "RuntimeError", message: "something went wrong", + backtrace: ["app/jobs/failing_job.rb:5"] } + ) + SolidQueue::Job.set_callback(:create, :after, :prepare_for_execution) + execution + end + + describe "POST /failed_jobs/selection (bulk retry)" do + it "retries only the selected jobs" do + exec_a = create_failed(class_name: "JobA") + exec_b = create_failed(class_name: "JobB") + + post "#{engine_root}/failed_jobs/selection", + params: { job_ids: [exec_a.id] } + + expect(SolidQueue::FailedExecution.exists?(exec_a.id)).to be false + expect(SolidQueue::FailedExecution.exists?(exec_b.id)).to be true + end + + it "redirects to the failed jobs list" do + exec_a = create_failed + + post "#{engine_root}/failed_jobs/selection", + params: { job_ids: [exec_a.id] } + + expect(response).to redirect_to("#{engine_root}/failed_jobs") + end + + it "retries multiple selected jobs" do + exec_a = create_failed(class_name: "JobA") + exec_b = create_failed(class_name: "JobB") + + post "#{engine_root}/failed_jobs/selection", + params: { job_ids: [exec_a.id, exec_b.id] } + + expect(SolidQueue::FailedExecution.count).to eq(0) + end + + it "is a no-op when no job_ids are submitted" do + create_failed + + post "#{engine_root}/failed_jobs/selection", params: { job_ids: [] } + + expect(SolidQueue::FailedExecution.count).to eq(1) + end + end + + describe "DELETE /failed_jobs/selection (bulk discard)" do + it "discards only the selected jobs" do + exec_a = create_failed(class_name: "JobA") + exec_b = create_failed(class_name: "JobB") + + delete "#{engine_root}/failed_jobs/selection", + params: { job_ids: [exec_a.id] } + + expect(SolidQueue::FailedExecution.exists?(exec_a.id)).to be false + expect(SolidQueue::FailedExecution.exists?(exec_b.id)).to be true + end + + it "redirects to the failed jobs list" do + exec_a = create_failed + + delete "#{engine_root}/failed_jobs/selection", + params: { job_ids: [exec_a.id] } + + expect(response).to redirect_to("#{engine_root}/failed_jobs") + end + + it "discards multiple selected jobs" do + exec_a = create_failed(class_name: "JobA") + exec_b = create_failed(class_name: "JobB") + + delete "#{engine_root}/failed_jobs/selection", + params: { job_ids: [exec_a.id, exec_b.id] } + + expect(SolidQueue::FailedExecution.count).to eq(0) + end + + it "is a no-op when no job_ids are submitted" do + create_failed + + delete "#{engine_root}/failed_jobs/selection", params: { job_ids: [] } + + expect(SolidQueue::FailedExecution.count).to eq(1) + end + end + + describe "GET /failed_jobs with selection UI" do + it "renders checkboxes and both selection forms when failed jobs exist" do + create_failed + + get "#{engine_root}/failed_jobs" + + expect(response.body).to include('type="checkbox"') + expect(response.body).to include('id="retry-selection-form"') + expect(response.body).to include('id="discard-selection-form"') + end + + it "does not render the selection UI when there are no failed jobs" do + get "#{engine_root}/failed_jobs" + + expect(response.body).not_to include('retry-selection-form') + end + end +end From 35e551e88638b4209c653b4034ce5c07ae9b6979 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 17:02:02 -0400 Subject: [PATCH 3/8] fix: store failed job errors as hashes in dev seeds Passing a plain string to FailedExecution error: caused SolidQueue to serialize it as a JSON string. On read-back error.with_indifferent_access raised NoMethodError since String doesn't respond to it. Restructured ERRORS as hashes with exception_class, message, and backtrace keys. Co-Authored-By: Claude Sonnet 4.6 --- spec/dummy/db/seeds.rb | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/spec/dummy/db/seeds.rb b/spec/dummy/db/seeds.rb index 4da2445..8d0d7aa 100644 --- a/spec/dummy/db/seeds.rb +++ b/spec/dummy/db/seeds.rb @@ -14,11 +14,21 @@ QUEUES = %w[default mailers critical low_priority].freeze ERRORS = [ - "Net::ReadTimeout: Net::ReadTimeout with #", - "ActiveRecord::RecordNotFound: Couldn't find User with 'id'=42", - "Faraday::ConnectionFailed: Failed to open TCP connection to api.example.com:443", - "RuntimeError: Rate limit exceeded — retry after 60s", - "JSON::ParserError: unexpected token at ''", + { exception_class: "Net::ReadTimeout", + message: "Net::ReadTimeout with #", + backtrace: ["lib/net/http.rb:1234", "app/jobs/webhook_delivery_job.rb:12"] }, + { exception_class: "ActiveRecord::RecordNotFound", + message: "Couldn't find User with 'id'=42", + backtrace: ["app/controllers/users_controller.rb:15"] }, + { exception_class: "Faraday::ConnectionFailed", + message: "Failed to open TCP connection to api.example.com:443", + backtrace: ["app/services/api_client.rb:42", "app/jobs/sync_inventory_job.rb:8"] }, + { exception_class: "RuntimeError", + message: "Rate limit exceeded — retry after 60s", + backtrace: ["app/jobs/send_digest_job.rb:31"] }, + { exception_class: "JSON::ParserError", + message: "unexpected token at ''", + backtrace: ["app/jobs/data_import_job.rb:22"] }, ].freeze # ── Solid Queue: Processes ──────────────────────────────────────────────────── From be9845ff7cd50d0e2ff42dda3efd40cf7908d0e4 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 17:05:40 -0400 Subject: [PATCH 4/8] chore: lock x86_64-linux platform for importmap-rails on 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 600a1dd..fdcb46a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,6 +3,7 @@ PATH specs: solid_stack_web (0.1.0) csv (>= 3.0) + importmap-rails (>= 1.2) pagy (>= 43.0) rails (>= 8.1.3) solid_cable (>= 1.0) @@ -113,6 +114,10 @@ GEM activesupport (>= 6.1) i18n (1.14.8) concurrent-ruby (~> 1.0) + importmap-rails (2.2.3) + actionpack (>= 6.0.0) + activesupport (>= 6.0.0) + railties (>= 6.0.0) io-console (0.8.2) irb (1.18.0) pp (>= 0.6.0) @@ -363,6 +368,7 @@ CHECKSUMS fugit (1.12.1) sha256=5898f478ede9b415f0804e42b8f3fd53f814bd85eebffceebdbc34e1107aaf68 globalid (1.3.0) sha256=05c639ad6eb4594522a0b07983022f04aa7254626ab69445a0e493aa3786ff11 i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5 + importmap-rails (2.2.3) sha256=7101be2a4dc97cf1558fb8f573a718404c5f6bcfe94f304bf1f39e444feeb16a io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 json (2.19.5) sha256=218a18553e4801d579ca7e0f5bc72bafd776d7397238a1fb4e74db5b0a812c59 From ca39fccf114ec6cdc21e742f0d4b462063de455e Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 17:06:53 -0400 Subject: [PATCH 5/8] fix: render action buttons inline in table rows button_to wraps each button in a block-level form element, causing stacked buttons. display: inline on forms inside .sqw-actions keeps Retry and Discard side by side. Co-Authored-By: Claude Sonnet 4.6 --- app/assets/stylesheets/solid_stack_web/_04_table.css | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/solid_stack_web/_04_table.css b/app/assets/stylesheets/solid_stack_web/_04_table.css index bea430e..5381d07 100644 --- a/app/assets/stylesheets/solid_stack_web/_04_table.css +++ b/app/assets/stylesheets/solid_stack_web/_04_table.css @@ -28,6 +28,7 @@ .sqw-table tbody tr:hover { background: #f9fafb; } .sqw-actions { text-align: right; white-space: nowrap; } +.sqw-actions form { display: inline; } .sqw-empty { background: var(--surface); From 4bdd64d57b908ba3f2c5c908954ee232281a3ca0 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 17:09:39 -0400 Subject: [PATCH 6/8] fix: use proper Active Job arguments format for failed job seeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Passing arguments as a JSON string caused SolidQueue's retry (reset_execution_counters) to fail — it expects a Hash with executions and exception_executions keys. Seeds now match the format that Active Job writes when enqueuing real jobs. Co-Authored-By: Claude Sonnet 4.6 --- spec/dummy/db/seeds.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/dummy/db/seeds.rb b/spec/dummy/db/seeds.rb index 8d0d7aa..e106ea5 100644 --- a/spec/dummy/db/seeds.rb +++ b/spec/dummy/db/seeds.rb @@ -137,10 +137,12 @@ puts " failed jobs..." 5.times do + class_name = JOB_CLASSES.sample job = SolidQueue::Job.create!( - class_name: JOB_CLASSES.sample, + class_name: class_name, queue_name: QUEUES.sample, - arguments: [{ record_id: rand(1..100) }].to_json, + arguments: { "job_class" => class_name, "arguments" => [{ "record_id" => rand(1..100) }], + "executions" => 1, "exception_executions" => {} }, priority: 0, active_job_id: SecureRandom.uuid ) From 1768206130d1235f280643c0037326ce17ec664c Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 17:11:17 -0400 Subject: [PATCH 7/8] docs: update CHANGELOG, ROADMAP, and README for bulk retry feature Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 4 ++++ README.md | 2 +- ROADMAP.md | 1 - 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e9cc21..3895c55 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,12 +15,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Stimulus via importmap-rails** — `importmap-rails` added as an engine dependency; a `selection_controller.js` manages checkbox state, select-all toggling, and form injection at submit time; JS is delivered via the host app's importmap with no asset pipeline coupling +- **Bulk retry and discard for failed jobs** — checkbox column on the failed jobs list; "Retry Selected" and "Discard Selected" buttons appear in a selection bar when one or more jobs are checked; backed by `FailedJobs::SelectionsController` with `POST /failed_jobs/selection` (retry) and `DELETE /failed_jobs/selection` (discard) - **Bulk selection and discard** — checkbox column on the jobs list for ready, scheduled, and blocked statuses; "Discard Selected" submits only the checked jobs via `DELETE /jobs/selection` (`Jobs::SelectionsController#destroy`); "Select All" header checkbox toggles all rows; filter state is preserved in the redirect after a bulk discard - **Discard All** — "Discard All (N)" button on the jobs index header discards every job matching the current filters (class, queue, priority, period) in one request; respects the discardable-status guard so claimed jobs cannot be bulk-discarded; route `POST /jobs/discard_all` merges into the existing `destroy` action branching on `params[:id]` - **CSV export** — "Export CSV" button on jobs and failed-jobs index pages; export respects active filters so operators download exactly what they see on screen; columns: `id, class_name, queue_name, status, priority, enqueued_at` for jobs and `id, class_name, queue_name, error_class, error_message, failed_at` for failed jobs ### Fixed +- Retry button on failed jobs raised `NoMethodError` when dev seed data stored arguments as a raw JSON string instead of a Hash; seeds now use the Active Job arguments format (`executions`, `exception_executions` keys) that `SolidQueue::Job#reset_execution_counters` requires +- Action buttons in table rows were stacking vertically because `button_to` wraps each button in a block-level `
`; fixed with `.sqw-actions form { display: inline }` - `FailedJobsController#destroy` used a local variable instead of `@execution`, making the Turbo Stream row-removal template a no-op - Failed jobs index rendered error as a string via `.lines` — SolidQueue serializes `error` as JSON; now uses `execution.exception_class` - Replaced deprecated Rack status `:unprocessable_entity` with `:unprocessable_content` diff --git a/README.md b/README.md index a931c5d..5a3ccc1 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A mountable Rails engine that provides a unified web dashboard for the full [Sol ## Features - **Overview dashboard** with live counts across all three Solid Stack components; cards are clickable and link directly to each section -- **Solid Queue** — browse jobs by status (ready, scheduled, claimed, blocked) with filtering by job class, queue name, priority, and time period; manage failed jobs (retry / discard), pause/resume queues, and inspect worker processes; **Bulk selection** checkbox-selects individual jobs for discard; **Discard All** bulk-discards every job matching the current filters in one request; **CSV export** downloads jobs or failed jobs as a CSV file respecting active filters +- **Solid Queue** — browse jobs by status (ready, scheduled, claimed, blocked) with filtering by job class, queue name, priority, and time period; manage failed jobs (retry / discard / bulk retry / bulk discard), pause/resume queues, and inspect worker processes; **Bulk selection** checkbox-selects individual jobs for discard or retry; **Discard All** bulk-discards every job matching the current filters in one request; **CSV export** downloads jobs or failed jobs as a CSV file respecting active filters - **Job detail page** — drill into any job to see full arguments (pretty-printed JSON), queue, priority, enqueued time, Active Job ID, concurrency key, scheduled/blocked-until metadata, and a Discard button - **Solid Cache** — entry count and total byte size at a glance - **Solid Cable** — active message count and distinct channel count diff --git a/ROADMAP.md b/ROADMAP.md index 6bf9991..502a838 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -11,7 +11,6 @@ The path to v1.0.0 is staged: first achieve feature parity with `solid_queue_das > _Make the job management layer genuinely useful for operators._ ### Added -- **Bulk retry (failed jobs)** — retry selected failed jobs with optional stagger interval (5 s / 10 s / 30 s / 1 m) to avoid thundering-herd restarts - **Edit arguments & retry** — inline argument editor on failed job detail; retry with modified payload --- From 329929b68255ba690b2ea8bae0210f4b33fa1456 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 17:13:20 -0400 Subject: [PATCH 8/8] docs: remove bold formatting from CHANGELOG Added entries; fix stray prefix on line 13 Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3895c55..4215a0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,17 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Stimulus via importmap-rails — `importmap-rails` added as an engine dependency; a `selection_controller.js` manages checkbox state, select-all toggling, and form injection at submit time; JS is delivered via the host app's importmap with no asset pipeline coupling +- Bulk retry and discard for failed jobs — checkbox column on the failed jobs list; "Retry Selected" and "Discard Selected" buttons appear in a selection bar when one or more jobs are checked; backed by `FailedJobs::SelectionsController` with `POST /failed_jobs/selection` (retry) and `DELETE /failed_jobs/selection` (discard) +- Bulk selection and discard — checkbox column on the jobs list for ready, scheduled, and blocked statuses; "Discard Selected" submits only the checked jobs via `DELETE /jobs/selection` (`Jobs::SelectionsController#destroy`); "Select All" header checkbox toggles all rows; filter state is preserved in the redirect after a bulk discard +- Discard All — "Discard All (N)" button on the jobs index header discards every job matching the current filters (class, queue, priority, period) in one request; respects the discardable-status guard so claimed jobs cannot be bulk-discarded; route `POST /jobs/discard_all` merges into the existing `destroy` action branching on `params[:id]` +- CSV export — "Export CSV" button on jobs and failed-jobs index pages; export respects active filters so operators download exactly what they see on screen; columns: `id, class_name, queue_name, status, priority, enqueued_at` for jobs and `id, class_name, queue_name, error_class, error_message, failed_at` for failed jobs - Job detail page — `jobs/:id` show view with full arguments (pretty-printed JSON), queue, priority, enqueued time, status badge, Active Job ID, and status-specific metadata (scheduled_at, concurrency key, blocked-until); job class in the list is now a link to the detail page; Discard button available on the detail page for ready, scheduled, and blocked jobs - Job filtering — filter the jobs list by queue name, job class (substring), priority, and time period (1h / 24h / 7d / all) via query-param driven scopes; active filters are preserved across status tabs -- Job filter Turbo Frame — filter form and results table wrapped in a `` so applying filters reloads only the table without a full page refresh; `data-turbo-action="advance"` keeps the URL in sync; Turbo JS loaded from esm.sh CDN in the engine layout - -### Added - -- **Stimulus via importmap-rails** — `importmap-rails` added as an engine dependency; a `selection_controller.js` manages checkbox state, select-all toggling, and form injection at submit time; JS is delivered via the host app's importmap with no asset pipeline coupling -- **Bulk retry and discard for failed jobs** — checkbox column on the failed jobs list; "Retry Selected" and "Discard Selected" buttons appear in a selection bar when one or more jobs are checked; backed by `FailedJobs::SelectionsController` with `POST /failed_jobs/selection` (retry) and `DELETE /failed_jobs/selection` (discard) -- **Bulk selection and discard** — checkbox column on the jobs list for ready, scheduled, and blocked statuses; "Discard Selected" submits only the checked jobs via `DELETE /jobs/selection` (`Jobs::SelectionsController#destroy`); "Select All" header checkbox toggles all rows; filter state is preserved in the redirect after a bulk discard -- **Discard All** — "Discard All (N)" button on the jobs index header discards every job matching the current filters (class, queue, priority, period) in one request; respects the discardable-status guard so claimed jobs cannot be bulk-discarded; route `POST /jobs/discard_all` merges into the existing `destroy` action branching on `params[:id]` -- **CSV export** — "Export CSV" button on jobs and failed-jobs index pages; export respects active filters so operators download exactly what they see on screen; columns: `id, class_name, queue_name, status, priority, enqueued_at` for jobs and `id, class_name, queue_name, error_class, error_message, failed_at` for failed jobs +- Job filter Turbo Frame — filter form and results table wrapped in a `` so applying filters reloads only the table without a full page refresh; `data-turbo-action="advance"` keeps the URL in sync ### Fixed