From 9fb5380811113d6cb6150d1e263a98c9fa8edf47 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 13:19:32 -0400 Subject: [PATCH 1/7] feat: job detail page (jobs/:id show view) Add a show action and view for individual job executions, surfacing 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 names in the list are now links to the detail page with data-turbo-frame="_top" to break out of the filter Turbo Frame. Discard button available on the detail page for ready, scheduled, and blocked jobs. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + .../solid_stack_web/_09_detail.css | 61 +++++++++++++++++++ .../solid_stack_web/jobs_controller.rb | 8 +++ app/views/solid_stack_web/jobs/index.html.erb | 2 +- app/views/solid_stack_web/jobs/show.html.erb | 56 +++++++++++++++++ config/routes.rb | 2 +- spec/requests/solid_stack_web/jobs_spec.rb | 57 +++++++++++++++++ 7 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 app/assets/stylesheets/solid_stack_web/_09_detail.css create mode 100644 app/views/solid_stack_web/jobs/show.html.erb diff --git a/CHANGELOG.md b/CHANGELOG.md index f5ca589..e8c6b6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- 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 diff --git a/app/assets/stylesheets/solid_stack_web/_09_detail.css b/app/assets/stylesheets/solid_stack_web/_09_detail.css new file mode 100644 index 0000000..86fbf29 --- /dev/null +++ b/app/assets/stylesheets/solid_stack_web/_09_detail.css @@ -0,0 +1,61 @@ +.sqw-back-link { + display: inline-block; + font-size: 13px; + color: var(--muted); + margin-bottom: 0.4rem; +} +.sqw-back-link:hover { color: var(--text); text-decoration: none; } + +.sqw-detail-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 1.25rem 1.5rem; + margin-bottom: 1.25rem; +} + +.sqw-detail-grid { + display: grid; + grid-template-columns: 160px 1fr; + gap: 0.75rem 1rem; +} + +.sqw-detail-grid dt { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--muted); + padding-top: 3px; +} + +.sqw-detail-section { + margin-bottom: 1.25rem; +} + +.sqw-section-title { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: .05em; + color: var(--muted); + margin-bottom: 0.5rem; +} + +.sqw-code-block { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 1rem 1.25rem; + font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; + font-size: 13px; + line-height: 1.6; + overflow-x: auto; + white-space: pre; +} + +.sqw-detail-actions { + display: flex; + gap: 0.75rem; +} \ No newline at end of file diff --git a/app/controllers/solid_stack_web/jobs_controller.rb b/app/controllers/solid_stack_web/jobs_controller.rb index c86fb25..782dad1 100644 --- a/app/controllers/solid_stack_web/jobs_controller.rb +++ b/app/controllers/solid_stack_web/jobs_controller.rb @@ -11,6 +11,14 @@ def index @pagy, @executions = pagy(filtered_scope) end + def show + @execution = Job::EXECUTION_MODELS[@status].includes(:job).find(params[:id]) + @job = @execution.job + @arguments = JSON.parse(@job.arguments) if @job.arguments.present? + rescue JSON::ParserError + @arguments = nil + end + def destroy execution = Job::EXECUTION_MODELS[@status].find(params[:id]) execution.job.destroy! diff --git a/app/views/solid_stack_web/jobs/index.html.erb b/app/views/solid_stack_web/jobs/index.html.erb index e61a5f6..3795e9d 100644 --- a/app/views/solid_stack_web/jobs/index.html.erb +++ b/app/views/solid_stack_web/jobs/index.html.erb @@ -63,7 +63,7 @@ <% @executions.each do |execution| %> - <%= execution.job.class_name %> + <%= link_to execution.job.class_name, job_path(execution.id, status: @status), data: { turbo_frame: "_top" } %> <%= execution.job.queue_name %> <%= execution.job.priority %> <%= execution.created_at.strftime("%b %d %H:%M") %> diff --git a/app/views/solid_stack_web/jobs/show.html.erb b/app/views/solid_stack_web/jobs/show.html.erb new file mode 100644 index 0000000..e20cba1 --- /dev/null +++ b/app/views/solid_stack_web/jobs/show.html.erb @@ -0,0 +1,56 @@ +
+ <%= link_to "← Jobs", jobs_path(status: params[:status]), class: "sqw-back-link" %> +

<%= @job.class_name %>

+
+ +
+
+
Status
+
+ <%= SolidStackWeb::Job::TAB_LABELS[@status] %> +
+ +
Queue
+
<%= @job.queue_name %>
+ +
Priority
+
<%= @job.priority %>
+ +
Enqueued
+
<%= @job.created_at.strftime("%b %d, %Y %H:%M:%S UTC") %>
+ + <% if @job.active_job_id.present? %> +
Active Job ID
+
<%= @job.active_job_id %>
+ <% end %> + + <% if @status == "scheduled" && @job.scheduled_at %> +
Scheduled At
+
<%= @job.scheduled_at.strftime("%b %d, %Y %H:%M:%S UTC") %>
+ <% end %> + + <% if @status == "blocked" %> + <% if @job.concurrency_key.present? %> +
Concurrency Key
+
<%= @job.concurrency_key %>
+ <% end %> +
Blocked Until
+
<%= @execution.expires_at.strftime("%b %d, %Y %H:%M:%S UTC") %>
+ <% end %> +
+
+ +<% if @job.arguments.present? %> +
+

Arguments

+
<%= @arguments ? JSON.pretty_generate(@arguments) : @job.arguments %>
+
+<% end %> + +<% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %> +
+ <%= button_to "Discard Job", job_path(@execution.id, status: @status), + method: :delete, class: "sqw-btn sqw-btn--danger", + data: { turbo_confirm: "Discard this job?" } %> +
+<% end %> \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index d4f5406..4b724db 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,7 +1,7 @@ SolidStackWeb::Engine.routes.draw do root to: "dashboard#index" - resources :jobs, only: [:index, :destroy] + resources :jobs, only: [:index, :show, :destroy] resources :failed_jobs, only: [:index, :destroy] do member { post :retry } diff --git a/spec/requests/solid_stack_web/jobs_spec.rb b/spec/requests/solid_stack_web/jobs_spec.rb index 426c26e..78b3da3 100644 --- a/spec/requests/solid_stack_web/jobs_spec.rb +++ b/spec/requests/solid_stack_web/jobs_spec.rb @@ -130,6 +130,63 @@ def create_ready(class_name: "MyJob", queue_name: "default", priority: 0) end end + describe "GET /jobs/:id" do + it "returns 200 and shows the job class" do + job = create_ready(class_name: "ReportJob") + get "#{engine_root}/jobs/#{job.ready_execution.id}", params: { status: "ready" } + + expect(response).to have_http_status(:ok) + expect(response.body).to include("ReportJob") + end + + it "shows queue and priority" do + job = create_ready(queue_name: "critical", priority: 5) + get "#{engine_root}/jobs/#{job.ready_execution.id}", params: { status: "ready" } + + expect(response.body).to include("critical") + expect(response.body).to include("5") + end + + it "pretty-prints JSON arguments" do + job = SolidQueue::Job.create!(class_name: "MyJob", queue_name: "default", priority: 0, + arguments: [{ user_id: 42 }].to_json) + get "#{engine_root}/jobs/#{job.ready_execution.id}", params: { status: "ready" } + + expect(response.body).to include("user_id") + expect(response.body).to include("42") + end + + it "shows a discard button for ready jobs" do + job = create_ready + get "#{engine_root}/jobs/#{job.ready_execution.id}", params: { status: "ready" } + + expect(response.body).to include("Discard Job") + end + + it "does not show a discard button for claimed jobs" do + SolidQueue::Job.skip_callback(:create, :after, :prepare_for_execution) + job = SolidQueue::Job.create!(class_name: "WorkJob", queue_name: "default", priority: 0) + process = SolidQueue::Process.create!( + kind: "Worker", name: "worker-spec", pid: 99_999, + hostname: "test", last_heartbeat_at: Time.current + ) + execution = SolidQueue::ClaimedExecution.create!(job: job, process_id: process.id) + SolidQueue::Job.set_callback(:create, :after, :prepare_for_execution) + + get "#{engine_root}/jobs/#{execution.id}", params: { status: "claimed" } + + expect(response).to have_http_status(:ok) + expect(response.body).not_to include("Discard Job") + end + + it "shows a back link to the jobs list" do + job = create_ready + get "#{engine_root}/jobs/#{job.ready_execution.id}", params: { status: "ready" } + + expect(response.body).to include("← Jobs") + end + end + describe "combined filters" do it "applies class and queue filters together" do create_ready(class_name: "ReportJob", queue_name: "reports") From 65a87359f291f02528bb66c4ac29c72bb3b3d3ce Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 13:23:54 -0400 Subject: [PATCH 2/7] refactor: match solid_queue_dashboard two-column detail layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch show page from single-column stacked layout to side-by-side grid (1fr 1fr): Details card left, Arguments card right. Move Discard button into the page header (top right) with breadcrumb navigation replacing the back link. Show all fields unconditionally with — for empty values. Update dl to auto/1fr columns and code block to pre-wrap with max-height scroll. Co-Authored-By: Claude Sonnet 4.6 --- .../solid_stack_web/_09_detail.css | 77 ++++++++-------- app/views/solid_stack_web/jobs/show.html.erb | 87 ++++++++++--------- spec/requests/solid_stack_web/jobs_spec.rb | 5 +- 3 files changed, 90 insertions(+), 79 deletions(-) diff --git a/app/assets/stylesheets/solid_stack_web/_09_detail.css b/app/assets/stylesheets/solid_stack_web/_09_detail.css index 86fbf29..0320ddb 100644 --- a/app/assets/stylesheets/solid_stack_web/_09_detail.css +++ b/app/assets/stylesheets/solid_stack_web/_09_detail.css @@ -1,61 +1,70 @@ -.sqw-back-link { - display: inline-block; - font-size: 13px; - color: var(--muted); - margin-bottom: 0.4rem; -} -.sqw-back-link:hover { color: var(--text); text-decoration: none; } - .sqw-detail-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow); - padding: 1.25rem 1.5rem; - margin-bottom: 1.25rem; + overflow: hidden; } -.sqw-detail-grid { - display: grid; - grid-template-columns: 160px 1fr; - gap: 0.75rem 1rem; +.sqw-breadcrumb { + font-size: 12px; + color: var(--muted); + margin-bottom: 0.25rem; } +.sqw-breadcrumb a { color: var(--muted); text-decoration: none; } +.sqw-breadcrumb a:hover { color: var(--text); } -.sqw-detail-grid dt { - font-size: 11px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: .05em; - color: var(--muted); - padding-top: 3px; +.sqw-page-header--split { + display: flex; + align-items: center; + justify-content: space-between; +} + +.sqw-detail-actions { + display: flex; + gap: 0.5rem; +} + +/* Two-column detail layout: Details card | Arguments card */ +.sqw-detail-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1.5rem; } .sqw-detail-section { - margin-bottom: 1.25rem; + padding: 1.25rem; } .sqw-section-title { - font-size: 11px; + font-size: 13px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); - margin-bottom: 0.5rem; + margin-bottom: 0.75rem; +} + +/* Definition list — dt naturally sized, dd takes the rest */ +.sqw-dl { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.5rem 1.5rem; + font-size: 13px; } +.sqw-dl dt { color: var(--muted); white-space: nowrap; } +.sqw-dl dd { word-break: break-all; } .sqw-code-block { + font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; + font-size: 12px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); - padding: 1rem 1.25rem; - font-family: ui-monospace, "SFMono-Regular", Menlo, monospace; - font-size: 13px; - line-height: 1.6; + padding: 0.75rem; overflow-x: auto; - white-space: pre; -} - -.sqw-detail-actions { - display: flex; - gap: 0.75rem; + white-space: pre-wrap; + word-break: break-word; + max-height: 400px; + overflow-y: auto; } \ No newline at end of file diff --git a/app/views/solid_stack_web/jobs/show.html.erb b/app/views/solid_stack_web/jobs/show.html.erb index e20cba1..92f08dd 100644 --- a/app/views/solid_stack_web/jobs/show.html.erb +++ b/app/views/solid_stack_web/jobs/show.html.erb @@ -1,56 +1,57 @@ -
- <%= link_to "← Jobs", jobs_path(status: params[:status]), class: "sqw-back-link" %> -

<%= @job.class_name %>

-
+
+
+
+ <%= link_to "Jobs", jobs_path(status: params[:status]) %> › Detail +
+

<%= @job.class_name %>

+
-
-
-
Status
-
- <%= SolidStackWeb::Job::TAB_LABELS[@status] %> -
+ <% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %> +
+ <%= button_to "Discard Job", job_path(@execution.id, status: @status), + method: :delete, class: "sqw-btn sqw-btn--danger", + data: { turbo_confirm: "Discard this job?" } %> +
+ <% end %> +
-
Queue
-
<%= @job.queue_name %>
+
+
+

Details

+
+
Status
+
<%= SolidStackWeb::Job::TAB_LABELS[@status] %>
-
Priority
-
<%= @job.priority %>
+
Queue
+
<%= @job.queue_name %>
-
Enqueued
-
<%= @job.created_at.strftime("%b %d, %Y %H:%M:%S UTC") %>
+
Priority
+
<%= @job.priority %>
- <% if @job.active_job_id.present? %>
Active Job ID
-
<%= @job.active_job_id %>
- <% end %> +
<%= @job.active_job_id.presence || "—" %>
- <% if @status == "scheduled" && @job.scheduled_at %> -
Scheduled At
-
<%= @job.scheduled_at.strftime("%b %d, %Y %H:%M:%S UTC") %>
- <% end %> +
Concurrency Key
+
<%= @job.concurrency_key.presence || "—" %>
- <% if @status == "blocked" %> - <% if @job.concurrency_key.present? %> -
Concurrency Key
-
<%= @job.concurrency_key %>
+ <% if @status == "blocked" %> +
Blocked Until
+
<%= @execution.expires_at ? @execution.expires_at.strftime("%Y-%m-%d %H:%M:%S UTC") : "—" %>
<% end %> -
Blocked Until
-
<%= @execution.expires_at.strftime("%b %d, %Y %H:%M:%S UTC") %>
- <% end %> -
-
-<% if @job.arguments.present? %> -
-

Arguments

-
<%= @arguments ? JSON.pretty_generate(@arguments) : @job.arguments %>
+
Enqueued At
+
<%= @job.created_at.strftime("%Y-%m-%d %H:%M:%S UTC") %>
+ +
Scheduled At
+
<%= @job.scheduled_at ? @job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S UTC") : "—" %>
+ +
Finished At
+
<%= @job.finished_at ? @job.finished_at.strftime("%Y-%m-%d %H:%M:%S UTC") : "—" %>
+
-<% end %> -<% if SolidStackWeb::Job::DISCARDABLE.include?(@status) %> -
- <%= button_to "Discard Job", job_path(@execution.id, status: @status), - method: :delete, class: "sqw-btn sqw-btn--danger", - data: { turbo_confirm: "Discard this job?" } %> +
+

Arguments

+
<%= @arguments ? JSON.pretty_generate(@arguments) : (@job.arguments || "—") %>
-<% end %> \ No newline at end of file +
\ No newline at end of file diff --git a/spec/requests/solid_stack_web/jobs_spec.rb b/spec/requests/solid_stack_web/jobs_spec.rb index 78b3da3..0e100e2 100644 --- a/spec/requests/solid_stack_web/jobs_spec.rb +++ b/spec/requests/solid_stack_web/jobs_spec.rb @@ -179,11 +179,12 @@ def create_ready(class_name: "MyJob", queue_name: "default", priority: 0) expect(response.body).not_to include("Discard Job") end - it "shows a back link to the jobs list" do + it "shows a breadcrumb link to the jobs list" do job = create_ready get "#{engine_root}/jobs/#{job.ready_execution.id}", params: { status: "ready" } - expect(response.body).to include("← Jobs") + expect(response.body).to include("sqw-breadcrumb") + expect(response.body).to include("Jobs") end end From 9dce06b8bfe73260f41c276974e780272a6ac5b3 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 13:26:07 -0400 Subject: [PATCH 3/7] fix: register turbo_stream MIME type in engine initializer Without turbo-rails, Rails doesn't know about text/vnd.turbo-stream.html so respond_to { format.turbo_stream } raises an unrecognised format error. Register it once at engine boot; the unless guard avoids double-registration when the host app already has turbo-rails loaded. Co-Authored-By: Claude Sonnet 4.6 --- lib/solid_stack_web/engine.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/solid_stack_web/engine.rb b/lib/solid_stack_web/engine.rb index 4cb7414..9773e7d 100644 --- a/lib/solid_stack_web/engine.rb +++ b/lib/solid_stack_web/engine.rb @@ -10,6 +10,10 @@ class Engine < ::Rails::Engine config.i18n.load_path += Gem.find_files("pagy/locales/en.yml") + initializer "solid_stack_web.mime_types" do + Mime::Type.register "text/vnd.turbo-stream.html", :turbo_stream unless Mime[:turbo_stream] + end + initializer "solid_stack_web.pagy" do |app| app.config.after_initialize do Pagy::OPTIONS[:limit] = SolidStackWeb.page_size From 1e97a4f61b3fdd6611a0785b38b3ddaf8b2c8b48 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 13:32:18 -0400 Subject: [PATCH 4/7] fix: add turbo-rails dependency for turbo_stream view helpers The turbo_stream helper (used in destroy.turbo_stream.erb) is provided by turbo-rails, not by loading Turbo from a CDN. Add turbo-rails >= 2.0 as a declared gemspec dependency and require it in the engine, matching the solid_queue_dashboard approach. Remove the now-redundant manual Mime::Type registration. Expose @execution in destroy so the template uses @execution.id rather than params[:id]. Co-Authored-By: Claude Sonnet 4.6 --- Gemfile.lock | 11 +++++------ app/controllers/solid_stack_web/jobs_controller.rb | 4 ++-- .../solid_stack_web/jobs/destroy.turbo_stream.erb | 4 ++-- lib/solid_stack_web/engine.rb | 5 +---- solid_stack_web.gemspec | 1 + 5 files changed, 11 insertions(+), 14 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index af147e3..620c9d2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,6 +7,7 @@ PATH solid_cable (>= 1.0) solid_cache (>= 1.0) solid_queue (>= 1.0) + turbo-rails (>= 2.0) GEM remote: https://rubygems.org/ @@ -146,8 +147,6 @@ 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 @@ -291,11 +290,13 @@ 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) 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) @@ -312,7 +313,6 @@ GEM PLATFORMS arm64-darwin - x86_64-linux DEPENDENCIES bundler-audit @@ -373,7 +373,6 @@ 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 @@ -419,11 +418,11 @@ CHECKSUMS solid_queue (1.4.0) sha256=e6a18d196f0b27cb6e3c77c5b31258b05fb634f8ed64fb1866ed164047216c2a solid_stack_web (0.1.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 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_stack_web/jobs_controller.rb b/app/controllers/solid_stack_web/jobs_controller.rb index 782dad1..2a4d0e6 100644 --- a/app/controllers/solid_stack_web/jobs_controller.rb +++ b/app/controllers/solid_stack_web/jobs_controller.rb @@ -20,8 +20,8 @@ def show end def destroy - execution = Job::EXECUTION_MODELS[@status].find(params[:id]) - execution.job.destroy! + @execution = Job::EXECUTION_MODELS[@status].find(params[:id]) + @execution.job.destroy! @executions_remain = Job::EXECUTION_MODELS[@status].exists? respond_to do |format| diff --git a/app/views/solid_stack_web/jobs/destroy.turbo_stream.erb b/app/views/solid_stack_web/jobs/destroy.turbo_stream.erb index 7f9e414..927b463 100644 --- a/app/views/solid_stack_web/jobs/destroy.turbo_stream.erb +++ b/app/views/solid_stack_web/jobs/destroy.turbo_stream.erb @@ -1,7 +1,7 @@ <% if @executions_remain %> - <%= turbo_stream.remove "execution_#{params[:id]}" %> + <%= turbo_stream.remove "execution_#{@execution.id}" %> <% else %> <%= turbo_stream.replace "sqw-jobs-table" do %> <%= render "empty" %> <% end %> -<% end %> +<% end %> \ No newline at end of file diff --git a/lib/solid_stack_web/engine.rb b/lib/solid_stack_web/engine.rb index 9773e7d..60c8fad 100644 --- a/lib/solid_stack_web/engine.rb +++ b/lib/solid_stack_web/engine.rb @@ -3,6 +3,7 @@ require "solid_queue" require "solid_cache" require "solid_cable" +require "turbo-rails" module SolidStackWeb class Engine < ::Rails::Engine @@ -10,10 +11,6 @@ class Engine < ::Rails::Engine config.i18n.load_path += Gem.find_files("pagy/locales/en.yml") - initializer "solid_stack_web.mime_types" do - Mime::Type.register "text/vnd.turbo-stream.html", :turbo_stream unless Mime[:turbo_stream] - 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 2548f32..2241689 100644 --- a/solid_stack_web.gemspec +++ b/solid_stack_web.gemspec @@ -27,4 +27,5 @@ Gem::Specification.new do |spec| spec.add_dependency "solid_queue", ">= 1.0" spec.add_dependency "solid_cache", ">= 1.0" spec.add_dependency "solid_cable", ">= 1.0" + spec.add_dependency "turbo-rails", ">= 2.0" end From 3aa4f54b738adf28e3e416a7ff0869f944ae123c Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 13:34:36 -0400 Subject: [PATCH 5/7] feat: make dashboard gem cards fully clickable Use CSS stretched-link pattern: sqw-gem-card__link::after covers the entire card, making the whole surface navigate to the section. Queue card stat links remain independently clickable via position: relative z-index: 1. Cards also get a hover box-shadow for visual feedback. Co-Authored-By: Claude Sonnet 4.6 --- .../stylesheets/solid_stack_web/_07_dashboard.css | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/assets/stylesheets/solid_stack_web/_07_dashboard.css b/app/assets/stylesheets/solid_stack_web/_07_dashboard.css index 98c9766..b11f821 100644 --- a/app/assets/stylesheets/solid_stack_web/_07_dashboard.css +++ b/app/assets/stylesheets/solid_stack_web/_07_dashboard.css @@ -12,7 +12,10 @@ border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; + position: relative; + transition: box-shadow 0.15s; } +.sqw-gem-card:hover { box-shadow: 0 3px 8px rgba(0,0,0,.12); } .sqw-gem-card--queue { border-top-color: var(--primary); } .sqw-gem-card--cache { border-top-color: var(--purple); } @@ -41,6 +44,16 @@ } .sqw-gem-card__link:hover { color: var(--primary); text-decoration: none; } +/* Stretch the header link to cover the whole card */ +.sqw-gem-card__link::after { + content: ""; + position: absolute; + inset: 0; +} + +/* Stat links and other interactive elements sit above the overlay */ +.sqw-inline-stat { position: relative; z-index: 1; } + .sqw-gem-card__body { display: grid; grid-template-columns: repeat(auto-fill, minmax(90px, 1fr)); From 9e5b5a7bca00cd298b8bd2991e16e241986edbd8 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 13:37:00 -0400 Subject: [PATCH 6/7] docs: update README and ROADMAP for v0.2.0 progress README: add job detail page to features list, add turbo-rails to requirements, note clickable dashboard cards. ROADMAP: remove completed job detail page entry from v0.2.0. Co-Authored-By: Claude Sonnet 4.6 --- README.md | 4 +++- ROADMAP.md | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d69649a..fa9cfb8 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,9 @@ 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 +- **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 +- **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 - **Turbo Stream** job discard — removes the row inline without a full page reload @@ -80,6 +81,7 @@ The `authenticate` block is evaluated in the context of each request's controlle - [solid_queue](https://github.com/rails/solid_queue) >= 1.0 - [solid_cache](https://github.com/rails/solid_cache) >= 1.0 - [solid_cable](https://github.com/rails/solid_cable) >= 1.0 +- [turbo-rails](https://github.com/hotwired/turbo-rails) >= 2.0 ## Contributing diff --git a/ROADMAP.md b/ROADMAP.md index 1e68d52..8abd422 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 -- **Job detail page** — `jobs/:id` show view with full arguments, queue, priority, enqueued time, and execution metadata - **Bulk selection** — checkbox-driven multi-select on the jobs and failed-jobs lists - **Bulk discard** — discard all selected jobs in a single request - **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 From 5f4f8d46b71898122904e260505a1589aa206f9b Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 May 2026 13:42:14 -0400 Subject: [PATCH 7/7] chore: 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 620c9d2..812b68d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -147,6 +147,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 @@ -290,6 +292,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) @@ -313,6 +316,7 @@ GEM PLATFORMS arm64-darwin + x86_64-linux DEPENDENCIES bundler-audit @@ -373,6 +377,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 @@ -418,6 +423,7 @@ CHECKSUMS solid_queue (1.4.0) sha256=e6a18d196f0b27cb6e3c77c5b31258b05fb634f8ed64fb1866ed164047216c2a solid_stack_web (0.1.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