diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a5585a..f775c19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Recurring Tasks page (`/jobs/recurring_tasks`) showing key, cron schedule, job class or command, queue, next run time, last run time, and Static/Dynamic badge; eager loads recurring executions to avoid N+1 +- Recurring Tasks stat card on the dashboard (cyan, links to the page) +- "View recurring tasks" button in the dashboard Quick Links +- `sqd-badge--static` (green) and `sqd-badge--dynamic` (purple) badge variants - Hamburger toggle nav for viewports narrower than 576px — three-bar button opens a full-width dropdown with vertically stacked links; no JS file required - `sqd-grid-2` utility class for responsive two-column layouts (collapses to one column at ≤768px) - `.sqd-sr-only` utility class for visually-hidden text @@ -22,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Dashboard stat card order aligned with nav: Ready, Scheduled, Running, Blocked, Failed, Queues, Recurring, Processes +- Stat grid minimum cell width reduced from 150px to 128px so all 8 cards fit in one row - Navbar title and links constrained to the same max-width as page content so they align horizontally with the dashboard - Page headers stack vertically on mobile (≤640px) - Stat grid uses a smaller minimum cell width on mobile diff --git a/app/assets/stylesheets/solid_queue_web/application.css b/app/assets/stylesheets/solid_queue_web/application.css index 96342e0..78fca7e 100644 --- a/app/assets/stylesheets/solid_queue_web/application.css +++ b/app/assets/stylesheets/solid_queue_web/application.css @@ -149,7 +149,7 @@ body { /* Stat cards */ .sqd-stats { display: grid; - grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(128px, 1fr)); gap: 1rem; margin-bottom: 2rem; } @@ -183,6 +183,7 @@ body { .sqd-stat--blocked .sqd-stat__value { color: var(--warning); } .sqd-stat--queues .sqd-stat__value { color: var(--purple); } .sqd-stat--processes .sqd-stat__value { color: var(--muted); } +.sqd-stat--recurring .sqd-stat__value { color: var(--info); } .sqd-stat--link { display: block; @@ -267,6 +268,8 @@ tbody tr:hover { background: var(--bg); } .sqd-badge--claimed { background: #cfe2ff; color: #084298; } .sqd-badge--failed { background: #f8d7da; color: #842029; } .sqd-badge--blocked { background: #fff3cd; color: #664d03; } +.sqd-badge--static { background: #d1e7dd; color: #0f5132; } +.sqd-badge--dynamic { background: #e0d7f5; color: #4a2c8a; } .sqd-badge--paused { background: #e2e3e5; color: #41464b; } .sqd-badge--running { background: #d1e7dd; color: #0f5132; } .sqd-badge--supervisor { background: #e0d7f5; color: #4a2c8a; } diff --git a/app/controllers/solid_queue_web/dashboard_controller.rb b/app/controllers/solid_queue_web/dashboard_controller.rb index 2ed86e4..f92335f 100644 --- a/app/controllers/solid_queue_web/dashboard_controller.rb +++ b/app/controllers/solid_queue_web/dashboard_controller.rb @@ -8,7 +8,8 @@ def index failed: SolidQueue::FailedExecution.count, blocked: SolidQueue::BlockedExecution.count, queues: SolidQueue::Job.select(:queue_name).distinct.count, - processes: SolidQueue::Process.count + processes: SolidQueue::Process.count, + recurring: SolidQueue::RecurringTask.count } end end diff --git a/app/controllers/solid_queue_web/recurring_tasks_controller.rb b/app/controllers/solid_queue_web/recurring_tasks_controller.rb new file mode 100644 index 0000000..a719304 --- /dev/null +++ b/app/controllers/solid_queue_web/recurring_tasks_controller.rb @@ -0,0 +1,7 @@ +module SolidQueueWeb + class RecurringTasksController < ApplicationController + def index + @recurring_tasks = SolidQueue::RecurringTask.includes(:recurring_executions).order(:key) + end + end +end diff --git a/app/views/layouts/solid_queue_web/application.html.erb b/app/views/layouts/solid_queue_web/application.html.erb index d11ad2f..fdefcb9 100644 --- a/app/views/layouts/solid_queue_web/application.html.erb +++ b/app/views/layouts/solid_queue_web/application.html.erb @@ -26,6 +26,7 @@
  • <%= link_to "Queues", queues_path, class: current_page?(queues_path) ? "active" : "", aria: { current: current_page?(queues_path) ? "page" : nil } %>
  • <%= link_to "Jobs", jobs_path, class: current_page?(jobs_path) ? "active" : "", aria: { current: current_page?(jobs_path) ? "page" : nil } %>
  • <%= link_to "Failed", failed_jobs_path, class: current_page?(failed_jobs_path) ? "active" : "", aria: { current: current_page?(failed_jobs_path) ? "page" : nil } %>
  • +
  • <%= link_to "Recurring", recurring_tasks_path, class: current_page?(recurring_tasks_path) ? "active" : "", aria: { current: current_page?(recurring_tasks_path) ? "page" : nil } %>
  • <%= link_to "Processes", processes_path, class: current_page?(processes_path) ? "active" : "", aria: { current: current_page?(processes_path) ? "page" : nil } %>
  • diff --git a/app/views/solid_queue_web/dashboard/index.html.erb b/app/views/solid_queue_web/dashboard/index.html.erb index 2caf25f..56d5a38 100644 --- a/app/views/solid_queue_web/dashboard/index.html.erb +++ b/app/views/solid_queue_web/dashboard/index.html.erb @@ -25,6 +25,10 @@
    <%= @stats[:queues] %>
    Queues
    <% end %> + <%= link_to recurring_tasks_path, class: "sqd-stat sqd-stat--recurring sqd-stat--link" do %> +
    <%= @stats[:recurring] %>
    +
    Recurring
    + <% end %> <%= link_to processes_path, class: "sqd-stat sqd-stat--processes sqd-stat--link" do %>
    <%= @stats[:processes] %>
    Processes
    @@ -41,6 +45,7 @@ <%= link_to "View scheduled jobs", jobs_path(status: "scheduled"), class: "sqd-btn sqd-btn--muted" %> <%= link_to "View failed jobs", failed_jobs_path, class: "sqd-btn sqd-btn--muted" %> <%= link_to "Manage queues", queues_path, class: "sqd-btn sqd-btn--muted" %> + <%= link_to "View recurring tasks", recurring_tasks_path, class: "sqd-btn sqd-btn--muted" %> diff --git a/app/views/solid_queue_web/recurring_tasks/index.html.erb b/app/views/solid_queue_web/recurring_tasks/index.html.erb new file mode 100644 index 0000000..bd305cb --- /dev/null +++ b/app/views/solid_queue_web/recurring_tasks/index.html.erb @@ -0,0 +1,68 @@ +

    Recurring Tasks

    + +
    + <% if @recurring_tasks.empty? %> +
    No recurring tasks configured.
    + <% else %> + + + + + + + + + + + + + + <% @recurring_tasks.each do |task| %> + + + + + + + + + + <% end %> + +
    KeyScheduleJob / CommandQueueNext RunLast RunType
    <%= 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("%Y-%m-%d %H:%M %Z") + rescue + nil + end + %> + <%= next_run || "—" %> + + <% last_run = task.last_enqueued_time %> + <%= last_run ? last_run.strftime("%Y-%m-%d %H:%M %Z") : "—" %> + + <% if task.static? %> + Static + <% else %> + Dynamic + <% end %> +
    + <% end %> +
    \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index f163725..92bfa7d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ SolidQueueWeb::Engine.routes.draw do root to: "dashboard#index" + resources :recurring_tasks, only: [ :index ] resources :processes, only: [ :index ] resources :queues, only: [ :index ], param: :name do member do diff --git a/spec/requests/solid_queue_web/dashboard_spec.rb b/spec/requests/solid_queue_web/dashboard_spec.rb index f96f06a..983223d 100644 --- a/spec/requests/solid_queue_web/dashboard_spec.rb +++ b/spec/requests/solid_queue_web/dashboard_spec.rb @@ -11,6 +11,13 @@ get "/jobs" expect(response.body).to include("Dashboard") end + + it "includes a recurring tasks stat card and quick link" do + SolidQueue::RecurringTask.create!(key: "t", schedule: "* * * * *", command: "echo hi") + get "/jobs" + expect(response.body).to include("Recurring") + expect(response.body).to include("recurring_tasks") + end end describe "authentication" do diff --git a/spec/requests/solid_queue_web/recurring_tasks_spec.rb b/spec/requests/solid_queue_web/recurring_tasks_spec.rb new file mode 100644 index 0000000..bf2b35c --- /dev/null +++ b/spec/requests/solid_queue_web/recurring_tasks_spec.rb @@ -0,0 +1,57 @@ +require "rails_helper" + +RSpec.describe "RecurringTasks", type: :request do + let!(:recurring_task) do + SolidQueue::RecurringTask.create!( + key: "cleanup-task", + schedule: "0 2 * * *", + command: "CleanupJob.perform_later", + queue_name: "default", + description: "Nightly cleanup" + ) + end + + let!(:dynamic_task) do + SolidQueue::RecurringTask.create!( + key: "dynamic-report-task", + schedule: "0 8 * * 1", + command: "ReportJob.perform_later", + static: false + ) + end + + describe "GET /jobs/recurring_tasks" do + it "returns HTTP success" do + get "/jobs/recurring_tasks" + expect(response).to have_http_status(:ok) + end + + it "displays task keys" do + get "/jobs/recurring_tasks" + expect(response.body).to include("cleanup-task") + expect(response.body).to include("dynamic-report-task") + end + + it "displays the schedule" do + get "/jobs/recurring_tasks" + expect(response.body).to include("0 2 * * *") + end + + it "displays the description" do + get "/jobs/recurring_tasks" + expect(response.body).to include("Nightly cleanup") + end + + it "distinguishes static and dynamic tasks" do + get "/jobs/recurring_tasks" + expect(response.body).to include("Static") + expect(response.body).to include("Dynamic") + end + + it "shows empty state when no tasks exist" do + SolidQueue::RecurringTask.delete_all + get "/jobs/recurring_tasks" + expect(response.body).to include("No recurring tasks") + end + end +end