diff --git a/CHANGELOG.md b/CHANGELOG.md index fc0aa1c..28e72c6 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 +- Per-queue job browser — queue names and sizes on the Queues index are now links to `GET /queues/:id`, which shows a paginated list of ready jobs for that queue with job class, priority, and enqueued-at; individual "Discard" buttons remove a single job; a "Discard All Ready (N)" header button discards every ready job in the queue in one request; pause/resume controls are present on the show page so operators never need to leave the queue context - Recurring task list — `GET /recurring_tasks` enumerates all `SolidQueue::RecurringTask` records with key, cron schedule, job class or command, queue, next-run time, last-run time, and static/dynamic badge; each row has a "Run Now" button that immediately enqueues the task via `RecurringTasks::RunsController`; "Recurring" link added to the queue subnav - Job history view — `GET /history` lists all finished jobs with class name, queue, duration, and finished-at time; filterable by queue, class substring, and time period (1h / 24h / 7d); clicking a queue badge filters the history to that queue; CSV export respects active filters; "History" link added to the queue subnav - Scheduled job management — "Run Now" and offset buttons (+1h / +24h / +7d) on each scheduled job row; Turbo Stream removes the row on run-now and updates the scheduled-at cell on offset reschedule; "Run All Now (N)" header button back-dates all matching scheduled executions; backed by `ScheduledJobsController` using standard CRUD (`update` for single, `create` for bulk via `run_all_now` collection route) diff --git a/README.md b/README.md index 94c753a..7e5e8fc 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 / 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 +- **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; **Per-queue browser** — click any queue name or size to drill into its ready jobs with per-row and bulk discard - **Job history view** — paginated list of all finished jobs with class name, queue, duration, and finished-at time; filterable by queue (click a badge), class substring, and time period; CSV export respects active filters - **Scheduled job management** — "Run Now" and offset buttons (+1h / +24h / +7d) per row update the scheduled time inline via Turbo Stream; "Run All Now (N)" in the header back-dates all matching executions at once - **Recurring task list** — enumerates all `SolidQueue::RecurringTask` records with cron schedule, job class or command, queue, next-run and last-run times, and a static/dynamic badge; each row has a "Run Now" button that immediately enqueues the task diff --git a/ROADMAP.md b/ROADMAP.md index f3f64e5..1f5874a 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 > _Close the remaining Solid Queue feature gaps._ ### Added -- **Per-queue job browser** — drill into any queue from the Queues list to see its ready jobs and discard them - **Blocked job bulk discard** — "Discard all blocked" action on the blocked jobs view --- diff --git a/app/assets/stylesheets/solid_stack_web/_02_layout.css b/app/assets/stylesheets/solid_stack_web/_02_layout.css index e206515..9971954 100644 --- a/app/assets/stylesheets/solid_stack_web/_02_layout.css +++ b/app/assets/stylesheets/solid_stack_web/_02_layout.css @@ -72,6 +72,7 @@ .sqw-page-header { margin-bottom: 1.25rem; } .sqw-page-title { font-size: 20px; font-weight: 600; } +.sqw-page-title-row { display: flex; align-items: center; gap: 0.5rem; } @keyframes sqw-flash-dismiss { 0%, 86% { opacity: 1; max-height: 200px; margin-bottom: 1rem; } diff --git a/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb b/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb index 006de67..9d8e8a3 100644 --- a/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb +++ b/app/controllers/solid_stack_web/failed_jobs/selections_controller.rb @@ -1,22 +1,28 @@ module SolidStackWeb module FailedJobs class SelectionsController < ApplicationController + before_action :set_ids + def create - ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?) - SolidQueue::FailedExecution.where(id: ids).each(&:retry) + 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) + 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 + + private + + def set_ids + @ids = Array(params[:job_ids]).map(&:to_i).reject(&:zero?) + end end end end diff --git a/app/controllers/solid_stack_web/queues/pauses_controller.rb b/app/controllers/solid_stack_web/queues/pauses_controller.rb new file mode 100644 index 0000000..67d3028 --- /dev/null +++ b/app/controllers/solid_stack_web/queues/pauses_controller.rb @@ -0,0 +1,13 @@ +module SolidStackWeb + class Queues::PausesController < ApplicationController + def create + ::SolidQueue::Pause.find_or_create_by!(queue_name: params[:queue_id]) + redirect_back_or_to queues_path + end + + def destroy + ::SolidQueue::Pause.find_by(queue_name: params[:queue_id])&.destroy + redirect_back_or_to queues_path + end + end +end diff --git a/app/controllers/solid_stack_web/queues_controller.rb b/app/controllers/solid_stack_web/queues_controller.rb index 4f2a582..4721cff 100644 --- a/app/controllers/solid_stack_web/queues_controller.rb +++ b/app/controllers/solid_stack_web/queues_controller.rb @@ -13,14 +13,15 @@ def index end end - def pause - ::SolidQueue::Pause.find_or_create_by!(queue_name: params[:id]) - redirect_to queues_path - end - - def resume - ::SolidQueue::Pause.find_by(queue_name: params[:id])&.destroy - redirect_to queues_path + def show + @queue_name = params[:id] + @paused = ::SolidQueue::Pause.exists?(queue_name: @queue_name) + @pagy, @executions = pagy( + ::SolidQueue::ReadyExecution + .where(queue_name: @queue_name) + .includes(:job) + .order(created_at: :desc) + ) end end end diff --git a/app/views/solid_stack_web/queues/index.html.erb b/app/views/solid_stack_web/queues/index.html.erb index ca239f9..1b8b403 100644 --- a/app/views/solid_stack_web/queues/index.html.erb +++ b/app/views/solid_stack_web/queues/index.html.erb @@ -15,8 +15,8 @@
<% @queues.each do |queue| %>| Job Class | +Priority | +Enqueued At | ++ |
|---|---|---|---|
| + <%= link_to execution.job.class_name, job_path(execution.id, status: "ready"), + data: { turbo_frame: "_top" } %> + | +<%= execution.job.priority %> | +<%= execution.created_at.strftime("%b %d %H:%M") %> | ++ <%= button_to "Discard", job_path(execution, status: "ready", queue: @queue_name), + method: :delete, class: "sqw-btn sqw-btn--danger sqw-btn--sm", + data: { turbo_confirm: "Discard this job?" } %> + | +
No ready jobs in <%= @queue_name %>.
+