diff --git a/CHANGELOG.md b/CHANGELOG.md index 42f0045..2fef07f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- 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) + ## [0.2.0] - 2026-05-25 ### Added diff --git a/README.md b/README.md index 16e9476..cda1c4f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A mountable Rails engine that provides a unified web dashboard for the full [Sol - **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 +- **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 - **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 - **Failed job detail page** — drill into any failed job to see the full error, backtrace, and an inline JSON argument editor; submit to update arguments and retry in one action - **Solid Cache** — entry count and total byte size at a glance diff --git a/app/controllers/solid_stack_web/scheduled_jobs_controller.rb b/app/controllers/solid_stack_web/scheduled_jobs_controller.rb new file mode 100644 index 0000000..9db57f5 --- /dev/null +++ b/app/controllers/solid_stack_web/scheduled_jobs_controller.rb @@ -0,0 +1,52 @@ +module SolidStackWeb + class ScheduledJobsController < ApplicationController + def create + @period = params[:period].presence_in(PERIOD_DURATIONS.keys) + job_ids = scheduled_scope.pluck("solid_queue_jobs.id") + SolidQueue::ScheduledExecution.where(job_id: job_ids).update_all(scheduled_at: 1.second.ago) + SolidQueue::Job.where(id: job_ids).update_all(scheduled_at: 1.second.ago) + redirect_to jobs_path(status: "scheduled", period: @period), + notice: "#{job_ids.size} #{"job".pluralize(job_ids.size)} scheduled to run immediately." + rescue => e + redirect_to jobs_path(status: "scheduled", period: @period), + alert: "Could not run jobs: #{e.message}" + end + + def update + @execution = SolidQueue::ScheduledExecution.find(params[:id]) + @period = params[:period].presence_in(PERIOD_DURATIONS.keys) + @run_now = params[:offset] == "now" + new_time = resolve_new_time(@execution, params[:offset]) + + @execution.update!(scheduled_at: new_time) + @execution.job.update!(scheduled_at: new_time) + + respond_to do |format| + format.turbo_stream + format.html do + notice = @run_now ? "Job scheduled to run immediately." : "Job rescheduled by +#{params[:offset]}." + redirect_to jobs_path(status: "scheduled", period: @period), notice: notice + end + end + rescue ArgumentError => e + redirect_to jobs_path(status: "scheduled"), alert: e.message + rescue => e + redirect_to jobs_path(status: "scheduled"), alert: "Could not reschedule job: #{e.message}" + end + + private + + def scheduled_scope + scope = SolidQueue::ScheduledExecution.joins(:job) + scope = scope.where("solid_queue_jobs.created_at >= ?", PERIOD_DURATIONS[@period].ago) if @period.present? + scope + end + + def resolve_new_time(execution, offset) + return 1.second.ago if offset == "now" + raise ArgumentError, "Invalid offset." unless PERIOD_DURATIONS.key?(offset) + + execution.scheduled_at + PERIOD_DURATIONS[offset] + end + end +end diff --git a/app/views/solid_stack_web/jobs/index.html.erb b/app/views/solid_stack_web/jobs/index.html.erb index b8ac58f..ce9876e 100644 --- a/app/views/solid_stack_web/jobs/index.html.erb +++ b/app/views/solid_stack_web/jobs/index.html.erb @@ -3,6 +3,14 @@