From 024a5d21ad2adceba7459e61a8411590867b448e Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Wed, 27 May 2026 17:35:53 -0400 Subject: [PATCH 1/2] feat: configurable display timezone Add `SolidQueueWeb.time_zone` config option. When set, all dashboard timestamps are rendered via `in_time_zone` so operators in non-UTC zones see local times without needing to mentally convert. Defaults to nil (UTC). A centralised `format_timestamp` helper replaces all bare `.strftime` calls across jobs, failed jobs, history, search, queues, recurring tasks, and the scheduled-job turbo stream update. Closes #58 Co-Authored-By: Claude Sonnet 4.6 --- .../solid_queue_web/application_helper.rb | 8 +++++++ .../failed_jobs/index.html.erb | 2 +- .../solid_queue_web/history/index.html.erb | 2 +- app/views/solid_queue_web/jobs/index.html.erb | 8 +++---- .../solid_queue_web/queues/index.html.erb | 2 +- .../queues/jobs/index.html.erb | 4 ++-- .../recurring_tasks/index.html.erb | 4 ++-- .../scheduled_jobs/update.turbo_stream.erb | 2 +- .../solid_queue_web/search/index.html.erb | 2 +- lib/solid_queue_web.rb | 6 ++++- .../application_helper_spec.rb | 23 +++++++++++++++++++ 11 files changed, 49 insertions(+), 14 deletions(-) diff --git a/app/helpers/solid_queue_web/application_helper.rb b/app/helpers/solid_queue_web/application_helper.rb index e4e1f2b..f8ac0d9 100644 --- a/app/helpers/solid_queue_web/application_helper.rb +++ b/app/helpers/solid_queue_web/application_helper.rb @@ -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 diff --git a/app/views/solid_queue_web/failed_jobs/index.html.erb b/app/views/solid_queue_web/failed_jobs/index.html.erb index 3ebf2c6..3040e5d 100644 --- a/app/views/solid_queue_web/failed_jobs/index.html.erb +++ b/app/views/solid_queue_web/failed_jobs/index.html.erb @@ -120,7 +120,7 @@ <% end %> - <%= execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %> + <%= format_timestamp(execution.created_at) %> <%= button_to "Retry", retry_failed_job_path(execution), method: :post, class: "sqd-btn sqd-btn--primary sqd-btn--sm" %> diff --git a/app/views/solid_queue_web/history/index.html.erb b/app/views/solid_queue_web/history/index.html.erb index 306f46c..bb5085b 100644 --- a/app/views/solid_queue_web/history/index.html.erb +++ b/app/views/solid_queue_web/history/index.html.erb @@ -58,7 +58,7 @@ class: "sqd-mono", style: "color: inherit;" %> <%= format_duration(job.finished_at - job.created_at) %> - <%= job.finished_at.strftime("%Y-%m-%d %H:%M:%S") %> + <%= format_timestamp(job.finished_at) %> <% end %> diff --git a/app/views/solid_queue_web/jobs/index.html.erb b/app/views/solid_queue_web/jobs/index.html.erb index 7390be3..a754eff 100644 --- a/app/views/solid_queue_web/jobs/index.html.erb +++ b/app/views/solid_queue_web/jobs/index.html.erb @@ -116,9 +116,9 @@ <%= job.priority %> - <%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %> + <%= format_timestamp(job.scheduled_at) %> - <%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %> + <%= format_timestamp(job.created_at) %> <% if @status == "scheduled" %> <%= button_to "Run Now", scheduled_job_path(execution), @@ -183,9 +183,9 @@ <%= job.priority %> - <%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %> + <%= format_timestamp(job.scheduled_at) %> - <%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %> + <%= format_timestamp(job.created_at) %> <% if @status == "claimed" %> "> <%= time_ago_in_words(execution.created_at) %> diff --git a/app/views/solid_queue_web/queues/index.html.erb b/app/views/solid_queue_web/queues/index.html.erb index c2c620b..3abc8d8 100644 --- a/app/views/solid_queue_web/queues/index.html.erb +++ b/app/views/solid_queue_web/queues/index.html.erb @@ -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" %> - "> + <%= format_duration(age) %> <% else %> diff --git a/app/views/solid_queue_web/queues/jobs/index.html.erb b/app/views/solid_queue_web/queues/jobs/index.html.erb index 0fd38b9..db6d596 100644 --- a/app/views/solid_queue_web/queues/jobs/index.html.erb +++ b/app/views/solid_queue_web/queues/jobs/index.html.erb @@ -64,9 +64,9 @@ <%= job.priority %> - <%= job.scheduled_at ? job.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") : "—" %> + <%= format_timestamp(job.scheduled_at) %> - <%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %> + <%= format_timestamp(job.created_at) %> <% if discardable %> <%= button_to "Discard", queue_job_path(queue_name: @queue, id: execution), diff --git a/app/views/solid_queue_web/recurring_tasks/index.html.erb b/app/views/solid_queue_web/recurring_tasks/index.html.erb index 30d236f..abd78b2 100644 --- a/app/views/solid_queue_web/recurring_tasks/index.html.erb +++ b/app/views/solid_queue_web/recurring_tasks/index.html.erb @@ -43,7 +43,7 @@ <% 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 @@ -52,7 +52,7 @@ <% 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") %> <% if task.static? %> diff --git a/app/views/solid_queue_web/scheduled_jobs/update.turbo_stream.erb b/app/views/solid_queue_web/scheduled_jobs/update.turbo_stream.erb index bc870e6..7191a6c 100644 --- a/app/views/solid_queue_web/scheduled_jobs/update.turbo_stream.erb +++ b/app/views/solid_queue_web/scheduled_jobs/update.turbo_stream.erb @@ -3,7 +3,7 @@ <% else %> <%= turbo_stream.replace "scheduled_at_#{@execution.id}" do %> - <%= @execution.scheduled_at.strftime("%Y-%m-%d %H:%M:%S") %> + <%= format_timestamp(@execution.scheduled_at) %> <% end %> <% end %> \ No newline at end of file diff --git a/app/views/solid_queue_web/search/index.html.erb b/app/views/solid_queue_web/search/index.html.erb index 617e722..b37c71d 100644 --- a/app/views/solid_queue_web/search/index.html.erb +++ b/app/views/solid_queue_web/search/index.html.erb @@ -52,7 +52,7 @@ <%= link_to job.class_name, job_path(job), class: "sqd-table-link" %> <%= job.queue_name %> - <%= job.created_at.strftime("%Y-%m-%d %H:%M:%S") %> + <%= format_timestamp(job.created_at) %> <% end %> diff --git a/lib/solid_queue_web.rb b/lib/solid_queue_web.rb index 09c19f2..ad8f95b 100644 --- a/lib/solid_queue_web.rb +++ b/lib/solid_queue_web.rb @@ -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 @@ -48,6 +48,10 @@ def connects_to @connects_to end + def time_zone + @time_zone + end + def configure yield self end diff --git a/spec/helpers/solid_queue_web/application_helper_spec.rb b/spec/helpers/solid_queue_web/application_helper_spec.rb index 1b473bb..fd2a097 100644 --- a/spec/helpers/solid_queue_web/application_helper_spec.rb +++ b/spec/helpers/solid_queue_web/application_helper_spec.rb @@ -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") From 8a835408f6525f71c7765c1b5075cb61a57dc18c Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Wed, 27 May 2026 17:35:58 -0400 Subject: [PATCH 2/2] docs: update CHANGELOG, README, ROADMAP for configurable timezone Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 ++ README.md | 1 + ROADMAP.md | 1 - 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33882ef..dec7b91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 `` elements with `aria-sort` and direction indicators (↑/↓) ## [1.2.0] - 2026-05-27 diff --git a/README.md b/README.md index b793036..9063eed 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/ROADMAP.md b/ROADMAP.md index 4a11a0d..3466e38 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. | ---