diff --git a/CHANGELOG.md b/CHANGELOG.md index df4e318..fc0aa1c 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 +- 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 950789f..94c753a 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ A mountable Rails engine that provides a unified web dashboard for the full [Sol - **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 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 - **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/ROADMAP.md b/ROADMAP.md index 8a537d2..f3f64e5 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 -- **Recurring task list** — enumerate tasks defined in `config/recurring.yml` with last-run time, next-run time, and a "run now" action per task - **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 b1a77ec..e206515 100644 --- a/app/assets/stylesheets/solid_stack_web/_02_layout.css +++ b/app/assets/stylesheets/solid_stack_web/_02_layout.css @@ -73,11 +73,18 @@ .sqw-page-header { margin-bottom: 1.25rem; } .sqw-page-title { font-size: 20px; font-weight: 600; } +@keyframes sqw-flash-dismiss { + 0%, 86% { opacity: 1; max-height: 200px; margin-bottom: 1rem; } + 100% { opacity: 0; max-height: 0; margin-bottom: 0; padding: 0; } +} + .sqw-flash { padding: 0.75rem 1rem; border-radius: var(--radius); margin-bottom: 1rem; font-size: 13px; + animation: sqw-flash-dismiss 7s ease forwards; + overflow: hidden; } .sqw-flash--notice { background: #d1e7dd; color: #0a3622; border: 1px solid #a3cfbb; } .sqw-flash--alert { background: #f8d7da; color: #58151c; border: 1px solid #f1aeb5; } diff --git a/app/controllers/solid_stack_web/application_controller.rb b/app/controllers/solid_stack_web/application_controller.rb index 57da95a..5110276 100644 --- a/app/controllers/solid_stack_web/application_controller.rb +++ b/app/controllers/solid_stack_web/application_controller.rb @@ -15,7 +15,7 @@ class ApplicationController < ActionController::Base def current_section case controller_name - when "jobs", "failed_jobs", "queues", "processes", "history", "scheduled_jobs" then :queue + when "jobs", "failed_jobs", "queues", "processes", "history", "scheduled_jobs", "recurring_tasks" then :queue when "cache" then :cache when "cable" then :cable else :overview diff --git a/app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb b/app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb new file mode 100644 index 0000000..a1f7d7a --- /dev/null +++ b/app/controllers/solid_stack_web/recurring_tasks/runs_controller.rb @@ -0,0 +1,18 @@ +module SolidStackWeb + class RecurringTasks::RunsController < ApplicationController + def create + task = SolidQueue::RecurringTask.find_by!(key: params[:recurring_task_key]) + result = task.enqueue(at: Time.current) + + if result + redirect_to recurring_tasks_path, notice: "\"#{task.key}\" queued for immediate execution." + else + redirect_to recurring_tasks_path, alert: "Could not enqueue \"#{task.key}\" — it may have just run." + end + rescue ActiveRecord::RecordNotFound + redirect_to recurring_tasks_path, alert: "Recurring task not found." + rescue => e + redirect_to recurring_tasks_path, alert: "Could not run task: #{e.message}" + end + end +end diff --git a/app/controllers/solid_stack_web/recurring_tasks_controller.rb b/app/controllers/solid_stack_web/recurring_tasks_controller.rb new file mode 100644 index 0000000..141ac8f --- /dev/null +++ b/app/controllers/solid_stack_web/recurring_tasks_controller.rb @@ -0,0 +1,7 @@ +module SolidStackWeb + class RecurringTasksController < ApplicationController + def index + @recurring_tasks = SolidQueue::RecurringTask.includes(:recurring_executions).order(:key) + end + end +end diff --git a/app/views/layouts/solid_stack_web/application.html.erb b/app/views/layouts/solid_stack_web/application.html.erb index 40dbdb0..672c61a 100644 --- a/app/views/layouts/solid_stack_web/application.html.erb +++ b/app/views/layouts/solid_stack_web/application.html.erb @@ -33,6 +33,8 @@ class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "failed_jobs"}" %> <%= link_to "Queues", queues_path, class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "queues"}" %> + <%= link_to "Recurring", recurring_tasks_path, + class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "recurring_tasks"}" %> <%= link_to "History", history_path, class: "sqw-subnav__link#{" sqw-subnav__link--active" if controller_name == "history"}" %> <%= link_to "Processes", processes_path, diff --git a/app/views/solid_stack_web/recurring_tasks/index.html.erb b/app/views/solid_stack_web/recurring_tasks/index.html.erb new file mode 100644 index 0000000..cce359f --- /dev/null +++ b/app/views/solid_stack_web/recurring_tasks/index.html.erb @@ -0,0 +1,67 @@ +
| Key | +Schedule | +Job / Command | +Queue | +Next Run | +Last Run | +Type | ++ |
|---|---|---|---|---|---|---|---|
| <%= task.key %> | +<%= task.schedule %> | +
+ <% if task.class_name.present? %>
+ <%= task.class_name %>
+ <% if task.arguments.present? %>
+ <%= task.arguments.inspect %>
+ <% end %>
+ <% else %>
+ <%= task.command %>
+ <% end %>
+ <% if task.description.present? %>
+ <%= task.description %>
+ <% end %>
+ |
+ <%= task.queue_name.presence || "default" %> | ++ <% next_run = begin; task.next_time.strftime("%b %d %H:%M"); rescue; nil; end %> + <%= next_run || "—" %> + | ++ <% last_run = task.last_enqueued_time %> + <%= last_run ? last_run.strftime("%b %d %H:%M") : "—" %> + | ++ <% if task.static? %> + Static + <% else %> + Dynamic + <% end %> + | ++ <%= button_to "Run Now", recurring_task_run_path(task.key), + method: :post, + class: "sqw-btn sqw-btn--sm", + data: { turbo_confirm: "Run \"#{task.key}\" immediately?" } %> + | +
No recurring tasks configured.
+