Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
5 changes: 4 additions & 1 deletion app/assets/stylesheets/solid_queue_web/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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; }
Expand Down
3 changes: 2 additions & 1 deletion app/controllers/solid_queue_web/dashboard_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions app/controllers/solid_queue_web/recurring_tasks_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module SolidQueueWeb
class RecurringTasksController < ApplicationController
def index
@recurring_tasks = SolidQueue::RecurringTask.includes(:recurring_executions).order(:key)
end
end
end
1 change: 1 addition & 0 deletions app/views/layouts/solid_queue_web/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<li><%= link_to "Queues", queues_path, class: current_page?(queues_path) ? "active" : "", aria: { current: current_page?(queues_path) ? "page" : nil } %></li>
<li><%= link_to "Jobs", jobs_path, class: current_page?(jobs_path) ? "active" : "", aria: { current: current_page?(jobs_path) ? "page" : nil } %></li>
<li><%= link_to "Failed", failed_jobs_path, class: current_page?(failed_jobs_path) ? "active" : "", aria: { current: current_page?(failed_jobs_path) ? "page" : nil } %></li>
<li><%= link_to "Recurring", recurring_tasks_path, class: current_page?(recurring_tasks_path) ? "active" : "", aria: { current: current_page?(recurring_tasks_path) ? "page" : nil } %></li>
<li><%= link_to "Processes", processes_path, class: current_page?(processes_path) ? "active" : "", aria: { current: current_page?(processes_path) ? "page" : nil } %></li>
</ul>
</nav>
Expand Down
5 changes: 5 additions & 0 deletions app/views/solid_queue_web/dashboard/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
<div class="sqd-stat__value"><%= @stats[:queues] %></div>
<div class="sqd-stat__label">Queues</div>
<% end %>
<%= link_to recurring_tasks_path, class: "sqd-stat sqd-stat--recurring sqd-stat--link" do %>
<div class="sqd-stat__value"><%= @stats[:recurring] %></div>
<div class="sqd-stat__label">Recurring</div>
<% end %>
<%= link_to processes_path, class: "sqd-stat sqd-stat--processes sqd-stat--link" do %>
<div class="sqd-stat__value"><%= @stats[:processes] %></div>
<div class="sqd-stat__label">Processes</div>
Expand All @@ -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" %>
</div>
</div>

Expand Down
68 changes: 68 additions & 0 deletions app/views/solid_queue_web/recurring_tasks/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<h1 class="sqd-page-title">Recurring Tasks</h1>

<div class="sqd-card" style="margin-top: 1.5rem;">
<% if @recurring_tasks.empty? %>
<div class="sqd-empty">No recurring tasks configured.</div>
<% else %>
<table>
<thead>
<tr>
<th scope="col">Key</th>
<th scope="col">Schedule</th>
<th scope="col">Job / Command</th>
<th scope="col">Queue</th>
<th scope="col">Next Run</th>
<th scope="col">Last Run</th>
<th scope="col">Type</th>
</tr>
</thead>
<tbody>
<% @recurring_tasks.each do |task| %>
<tr>
<td class="sqd-mono"><%= task.key %></td>
<td class="sqd-mono"><%= task.schedule %></td>
<td>
<% if task.class_name.present? %>
<%= task.class_name %>
<% if task.arguments.present? %>
<div class="sqd-muted-text sqd-mono" style="font-size: 11px;">
<%= task.arguments.inspect %>
</div>
<% end %>
<% else %>
<span class="sqd-mono sqd-muted-text"><%= task.command %></span>
<% end %>
<% if task.description.present? %>
<div class="sqd-muted-text" style="font-size: 12px; margin-top: 0.2rem;">
<%= task.description %>
</div>
<% end %>
</td>
<td class="sqd-mono"><%= task.queue_name.presence || "default" %></td>
<td class="sqd-mono">
<%
next_run = begin
task.next_time.strftime("%Y-%m-%d %H:%M %Z")
rescue
nil
end
%>
<%= next_run || "—" %>
</td>
<td class="sqd-mono">
<% last_run = task.last_enqueued_time %>
<%= last_run ? last_run.strftime("%Y-%m-%d %H:%M %Z") : "—" %>
</td>
<td>
<% if task.static? %>
<span class="sqd-badge sqd-badge--static">Static</span>
<% else %>
<span class="sqd-badge sqd-badge--dynamic">Dynamic</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
</div>
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -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
Expand Down
7 changes: 7 additions & 0 deletions spec/requests/solid_queue_web/dashboard_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions spec/requests/solid_queue_web/recurring_tasks_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading