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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Configurable display timezone — `SolidQueueWeb.configure { |c| c.time_zone = "America/New_York" }` renders all dashboard timestamps in the configured zone rather than UTC; uses `in_time_zone` from ActiveSupport; defaults to UTC when not set; a `format_timestamp` helper centralises all timestamp formatting across jobs, failed jobs, history, search, queues, recurring tasks, and the scheduled-job turbo stream update

- Sortable table columns — Jobs, Failed Jobs, and History tables now support server-side sorting via `?sort=` and `?direction=` params; click any column header to sort ascending or descending; sort state is preserved across filter changes, status tab switches, and period buttons; a `sort_header_th` helper generates accessible `<th>` elements with `aria-sort` and direction indicators (↑/↓)

## [1.2.0] - 2026-05-27
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ SolidQueueWeb.configure do |config|
config.alert_queue_thresholds = { "critical" => 50, "default" => 200 } # fire when queue depth >= threshold (default: {})
config.alert_webhook_cooldown = 1800 # seconds between repeated alerts per alert type (default: 3600)
config.connects_to = { reading: :reading, writing: :writing } # read replica (default: nil)
config.time_zone = "America/New_York" # display timezone for all timestamps (default: nil = UTC)
end

SolidQueueWeb.authenticate do
Expand Down
1 change: 0 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ Pull requests for any of these are welcome. See [Contributing](README.md#contrib

| Feature | Notes |
|---|---|
| **Configurable display timezone** | `config.time_zone = "America/New_York"` — all timestamps rendered in the configured zone rather than UTC. |
| **Sticky filter preferences** | Persist last-used status/period to `localStorage` so filters survive page reloads. |

---
Expand Down
8 changes: 8 additions & 0 deletions app/helpers/solid_queue_web/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ def inline_styles
content_tag(:style, css.html_safe)
end

def format_timestamp(time, format: "%Y-%m-%d %H:%M:%S")
return "—" unless time

tz = SolidQueueWeb.time_zone
displayed = tz ? time.in_time_zone(tz) : time.utc
displayed.strftime(format)
end

def format_duration(seconds)
s = seconds.to_i
return "< 1s" if s < 1
Expand Down
2 changes: 1 addition & 1 deletion app/views/solid_queue_web/failed_jobs/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@
<span style="color:var(--muted)">—</span>
<% end %>
</td>
<td class="sqd-mono"><%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
<td class="sqd-mono"><%= format_timestamp(execution.created_at) %></td>
<td class="sqd-row-actions">
<%= button_to "Retry", retry_failed_job_path(execution), method: :post,
class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/solid_queue_web/history/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
class: "sqd-mono", style: "color: inherit;" %>
</td>
<td class="sqd-mono"><%= format_duration(job.finished_at - job.created_at) %></td>
<td class="sqd-mono"><%= job.finished_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
<td class="sqd-mono"><%= format_timestamp(job.finished_at) %></td>
</tr>
<% end %>
</tbody>
Expand Down
8 changes: 4 additions & 4 deletions app/views/solid_queue_web/jobs/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,9 @@
</td>
<td><%= job.priority %></td>
<td id="scheduled_at_<%= execution.id %>" class="sqd-mono">
<%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
<%= format_timestamp(job.scheduled_at) %>
</td>
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
<td class="sqd-mono"><%= format_timestamp(job.created_at) %></td>
<td class="sqd-row-actions">
<% if @status == "scheduled" %>
<%= button_to "Run Now", scheduled_job_path(execution),
Expand Down Expand Up @@ -183,9 +183,9 @@
</td>
<td><%= job.priority %></td>
<td class="sqd-mono">
<%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
<%= format_timestamp(job.scheduled_at) %>
</td>
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
<td class="sqd-mono"><%= format_timestamp(job.created_at) %></td>
<% if @status == "claimed" %>
<td class="sqd-mono<%= slow ? " sqd-slow-duration" : "" %>">
<%= time_ago_in_words(execution.created_at) %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/solid_queue_web/queues/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<% if (oldest = @oldest_ready[queue.name]) %>
<% age = Time.current - oldest %>
<% latency_color = age > 86_400 ? "var(--danger)" : age > 3_600 ? "var(--warning)" : "inherit" %>
<abbr title="<%= oldest.strftime("%Y-%m-%d %H:%M:%S UTC") %>">
<abbr title="<%= format_timestamp(oldest) %>">
<span style="color: <%= latency_color %>"><%= format_duration(age) %></span>
</abbr>
<% else %>
Expand Down
4 changes: 2 additions & 2 deletions app/views/solid_queue_web/queues/jobs/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@
</td>
<td><%= job.priority %></td>
<td class="sqd-mono">
<%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %>
<%= format_timestamp(job.scheduled_at) %>
</td>
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
<td class="sqd-mono"><%= format_timestamp(job.created_at) %></td>
<% if discardable %>
<td class="sqd-row-actions">
<%= button_to "Discard", queue_job_path(queue_name: @queue, id: execution),
Expand Down
4 changes: 2 additions & 2 deletions app/views/solid_queue_web/recurring_tasks/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
<td class="sqd-mono">
<%
next_run = begin
task.next_time.strftime("%Y-%m-%d %H:%M %Z")
format_timestamp(task.next_time, format: "%Y-%m-%d %H:%M")
rescue
nil
end
Expand All @@ -52,7 +52,7 @@
</td>
<td class="sqd-mono">
<% last_run = task.last_enqueued_time %>
<%= last_run ? last_run.strftime("%Y-%m-%d %H:%M %Z") : "—" %>
<%= format_timestamp(last_run, format: "%Y-%m-%d %H:%M") %>
</td>
<td>
<% if task.static? %>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<% else %>
<%= turbo_stream.replace "scheduled_at_#{@execution.id}" do %>
<td id="scheduled_at_<%= @execution.id %>" class="sqd-mono">
<%= @execution.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") %>
<%= format_timestamp(@execution.scheduled_at) %>
</td>
<% end %>
<% end %>
2 changes: 1 addition & 1 deletion app/views/solid_queue_web/search/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
<tr>
<td><%= link_to job.class_name, job_path(job), class: "sqd-table-link" %></td>
<td class="sqd-mono"><%= job.queue_name %></td>
<td class="sqd-mono"><%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %></td>
<td class="sqd-mono"><%= format_timestamp(job.created_at) %></td>
</tr>
<% end %>
</tbody>
Expand Down
6 changes: 5 additions & 1 deletion lib/solid_queue_web.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module SolidQueueWeb
class << self
attr_writer :page_size, :dashboard_refresh_interval, :default_refresh_interval, :search_results_limit,
:slow_job_threshold, :alert_webhook_url, :alert_failure_threshold, :alert_webhook_cooldown,
:alert_queue_thresholds, :connects_to
:alert_queue_thresholds, :connects_to, :time_zone

def page_size
@page_size || 25
Expand Down Expand Up @@ -48,6 +48,10 @@ def connects_to
@connects_to
end

def time_zone
@time_zone
end

def configure
yield self
end
Expand Down
23 changes: 23 additions & 0 deletions spec/helpers/solid_queue_web/application_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,29 @@
require "rails_helper"

RSpec.describe SolidQueueWeb::ApplicationHelper, type: :helper do
describe "#format_timestamp" do
let(:time) { Time.utc(2024, 6, 15, 14, 30, 45) }

after { SolidQueueWeb.time_zone = nil }

it "returns '—' for nil" do
expect(helper.format_timestamp(nil)).to eq("—")
end

it "formats in UTC by default" do
expect(helper.format_timestamp(time)).to eq("2024-06-15 14:30:45")
end

it "formats in the configured time zone" do
SolidQueueWeb.time_zone = "America/New_York"
expect(helper.format_timestamp(time)).to eq("2024-06-15 10:30:45")
end

it "accepts a custom format string" do
expect(helper.format_timestamp(time, format: "%Y-%m-%d %H:%M")).to eq("2024-06-15 14:30")
end
end

describe "#format_duration" do
it "returns '< 1s' for sub-second durations" do
expect(helper.format_duration(0.4)).to eq("< 1s")
Expand Down