solid_queue_web 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +220 -4
  3. data/app/assets/stylesheets/solid_queue_web/_04_table.css +28 -0
  4. data/app/controllers/solid_queue_web/application_controller.rb +30 -0
  5. data/app/controllers/solid_queue_web/audit_controller.rb +43 -0
  6. data/app/controllers/solid_queue_web/blocked_jobs_controller.rb +2 -2
  7. data/app/controllers/solid_queue_web/failed_jobs/arguments_controller.rb +3 -3
  8. data/app/controllers/solid_queue_web/failed_jobs/selections_controller.rb +6 -4
  9. data/app/controllers/solid_queue_web/failed_jobs_controller.rb +4 -2
  10. data/app/controllers/solid_queue_web/jobs/selections_controller.rb +4 -3
  11. data/app/controllers/solid_queue_web/jobs_controller.rb +7 -3
  12. data/app/controllers/solid_queue_web/queues/jobs_controller.rb +5 -5
  13. data/app/controllers/solid_queue_web/queues/pauses_controller.rb +6 -4
  14. data/app/controllers/solid_queue_web/recurring_tasks/runs_controller.rb +4 -4
  15. data/app/controllers/solid_queue_web/retry_failed_jobs_controller.rb +6 -5
  16. data/app/controllers/solid_queue_web/scheduled_jobs_controller.rb +5 -5
  17. data/app/models/solid_queue_web/audit_event.rb +17 -0
  18. data/app/views/layouts/solid_queue_web/application.html.erb +20 -16
  19. data/app/views/solid_queue_web/audit/index.html.erb +78 -0
  20. data/app/views/solid_queue_web/dashboard/index.html.erb +67 -46
  21. data/app/views/solid_queue_web/failed_jobs/errors/index.html.erb +7 -7
  22. data/app/views/solid_queue_web/failed_jobs/index.html.erb +31 -31
  23. data/app/views/solid_queue_web/history/index.html.erb +14 -14
  24. data/app/views/solid_queue_web/jobs/index.html.erb +42 -42
  25. data/app/views/solid_queue_web/jobs/show.html.erb +20 -20
  26. data/app/views/solid_queue_web/performance/index.html.erb +16 -14
  27. data/app/views/solid_queue_web/processes/index.html.erb +16 -16
  28. data/app/views/solid_queue_web/queues/index.html.erb +16 -16
  29. data/app/views/solid_queue_web/queues/jobs/index.html.erb +21 -21
  30. data/app/views/solid_queue_web/recurring_tasks/index.html.erb +15 -15
  31. data/app/views/solid_queue_web/search/index.html.erb +13 -13
  32. data/config/locales/en.yml +330 -0
  33. data/config/routes.rb +1 -0
  34. data/db/migrate/01_create_solid_queue_web_audit_events.rb +16 -0
  35. data/lib/generators/solid_queue_web/install/migrations_generator.rb +24 -0
  36. data/lib/generators/solid_queue_web/install/templates/create_solid_queue_web_audit_events.rb.tt +16 -0
  37. data/lib/solid_queue_web/engine.rb +1 -0
  38. data/lib/solid_queue_web/version.rb +1 -1
  39. data/lib/solid_queue_web.rb +18 -1
  40. metadata +8 -1
@@ -4,17 +4,19 @@ module SolidQueueWeb
4
4
  def create
5
5
  queue = SolidQueue::Queue.find_by_name(params[:queue_name])
6
6
  queue.pause
7
- redirect_to queues_path, notice: "Queue \"#{queue.name}\" paused."
7
+ record_audit("queue_paused", queue_name: queue.name)
8
+ redirect_to queues_path, notice: t("solid_queue_web.flash.queue_paused", name: queue.name)
8
9
  rescue => e
9
- redirect_to queues_path, alert: "Could not pause queue: #{e.message}"
10
+ redirect_to queues_path, alert: t("solid_queue_web.flash.cannot_pause_queue", error: e.message)
10
11
  end
11
12
 
12
13
  def destroy
13
14
  queue = SolidQueue::Queue.find_by_name(params[:queue_name])
14
15
  queue.resume
15
- redirect_to queues_path, notice: "Queue \"#{queue.name}\" resumed."
16
+ record_audit("queue_resumed", queue_name: queue.name)
17
+ redirect_to queues_path, notice: t("solid_queue_web.flash.queue_resumed", name: queue.name)
16
18
  rescue => e
17
- redirect_to queues_path, alert: "Could not resume queue: #{e.message}"
19
+ redirect_to queues_path, alert: t("solid_queue_web.flash.cannot_resume_queue", error: e.message)
18
20
  end
19
21
  end
20
22
  end
@@ -5,14 +5,14 @@ module SolidQueueWeb
5
5
  result = task.enqueue(at: Time.current)
6
6
 
7
7
  if result
8
- redirect_to recurring_tasks_path, notice: "\"#{task.key}\" queued for immediate execution."
8
+ redirect_to recurring_tasks_path, notice: t("solid_queue_web.flash.task_queued", key: task.key)
9
9
  else
10
- redirect_to recurring_tasks_path, alert: "Could not enqueue \"#{task.key}\" — it may have just run."
10
+ redirect_to recurring_tasks_path, alert: t("solid_queue_web.flash.cannot_enqueue_task", key: task.key)
11
11
  end
12
12
  rescue ActiveRecord::RecordNotFound
13
- redirect_to recurring_tasks_path, alert: "Recurring task not found."
13
+ redirect_to recurring_tasks_path, alert: t("solid_queue_web.flash.task_not_found")
14
14
  rescue => e
15
- redirect_to recurring_tasks_path, alert: "Could not run task: #{e.message}"
15
+ redirect_to recurring_tasks_path, alert: t("solid_queue_web.flash.cannot_run_task", error: e.message)
16
16
  end
17
17
  end
18
18
  end
@@ -8,7 +8,7 @@ module SolidQueueWeb
8
8
 
9
9
  if params[:stagger].present? && executions.size > 1
10
10
  interval = STAGGER_INTERVALS[params[:stagger]]
11
- raise ArgumentError, "Invalid stagger interval." unless interval
11
+ raise ArgumentError, t("solid_queue_web.flash.invalid_stagger") unless interval
12
12
  executions.each_with_index do |execution, i|
13
13
  execution.job.update!(scheduled_at: i.zero? ? nil : Time.current + (i * interval))
14
14
  execution.retry
@@ -16,22 +16,23 @@ module SolidQueueWeb
16
16
  else
17
17
  SolidQueue::FailedExecution.retry_all(jobs)
18
18
  end
19
+ action = params[:id] ? "failed_job_retried" : "failed_jobs_retried"
20
+ record_audit(action, job_class: jobs.first&.class_name, queue_name: jobs.first&.queue_name, item_count: jobs.size)
19
21
  redirect_to failed_jobs_path(queue: @queue, q: @search, period: @period),
20
22
  notice: retry_notice(jobs.size)
21
23
  rescue ArgumentError => e
22
24
  redirect_to failed_jobs_path, alert: e.message
23
25
  rescue => e
24
- redirect_to failed_jobs_path, alert: "Could not retry job: #{e.message}"
26
+ redirect_to failed_jobs_path, alert: t("solid_queue_web.flash.cannot_retry_job", error: e.message)
25
27
  end
26
28
 
27
29
  private
28
30
 
29
31
  def retry_notice(count)
30
- label = "#{count} #{"job".pluralize(count)}"
31
32
  if params[:stagger].present? && count > 1
32
- "#{label} queued for retry, staggered #{params[:stagger]} apart."
33
+ t("solid_queue_web.flash.jobs_retried_staggered", count: count, stagger: params[:stagger])
33
34
  else
34
- "#{label} queued for retry."
35
+ t("solid_queue_web.flash.jobs_retried", count: count)
35
36
  end
36
37
  end
37
38
 
@@ -8,10 +8,10 @@ module SolidQueueWeb
8
8
  SolidQueue::Job.where(id: job_ids).update_all(scheduled_at: 1.second.ago)
9
9
 
10
10
  redirect_to jobs_path(status: "scheduled", period: @period),
11
- notice: "#{job_ids.size} #{"job".pluralize(job_ids.size)} scheduled to run immediately."
11
+ notice: t("solid_queue_web.flash.jobs_run_immediately", count: job_ids.size)
12
12
  rescue => e
13
13
  redirect_to jobs_path(status: "scheduled", period: @period),
14
- alert: "Could not run jobs: #{e.message}"
14
+ alert: t("solid_queue_web.flash.cannot_run_jobs", error: e.message)
15
15
  end
16
16
 
17
17
  def update
@@ -26,14 +26,14 @@ module SolidQueueWeb
26
26
  respond_to do |format|
27
27
  format.turbo_stream
28
28
  format.html do
29
- notice = @run_now ? "Job scheduled to run immediately." : "Job rescheduled by +#{params[:offset]}."
29
+ notice = @run_now ? t("solid_queue_web.flash.job_run_immediately") : t("solid_queue_web.flash.job_rescheduled", offset: params[:offset])
30
30
  redirect_to jobs_path(status: "scheduled", period: @period), notice: notice
31
31
  end
32
32
  end
33
33
  rescue ArgumentError => e
34
34
  redirect_to jobs_path(status: "scheduled"), alert: e.message
35
35
  rescue => e
36
- redirect_to jobs_path(status: "scheduled"), alert: "Could not reschedule job: #{e.message}"
36
+ redirect_to jobs_path(status: "scheduled"), alert: t("solid_queue_web.flash.cannot_reschedule_job", error: e.message)
37
37
  end
38
38
 
39
39
  private
@@ -46,7 +46,7 @@ module SolidQueueWeb
46
46
 
47
47
  def resolve_new_time(execution, offset)
48
48
  return 1.second.ago if offset == "now"
49
- raise ArgumentError, "Invalid offset." unless PERIOD_DURATIONS.key?(offset)
49
+ raise ArgumentError, t("solid_queue_web.flash.invalid_offset") unless PERIOD_DURATIONS.key?(offset)
50
50
 
51
51
  execution.scheduled_at + PERIOD_DURATIONS[offset]
52
52
  end
@@ -0,0 +1,17 @@
1
+ module SolidQueueWeb
2
+ class AuditEvent < ApplicationRecord
3
+ self.table_name = "solid_queue_web_audit_events"
4
+
5
+ ACTIONS = %w[
6
+ job_discarded jobs_discarded
7
+ failed_job_retried failed_jobs_retried
8
+ failed_job_discarded failed_jobs_discarded
9
+ queue_paused queue_resumed
10
+ ].freeze
11
+
12
+ validates :action, presence: true, inclusion: { in: ACTIONS }
13
+ validates :item_count, numericality: { greater_than: 0 }
14
+
15
+ scope :recent, -> { order(created_at: :desc) }
16
+ end
17
+ end
@@ -1,9 +1,9 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en">
2
+ <html lang="<%= I18n.locale %>">
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Solid Queue Dashboard</title>
6
+ <title><%= t("solid_queue_web.layout.title") %></title>
7
7
  <%= csrf_meta_tags %>
8
8
  <%= csp_meta_tag %>
9
9
  <%= inline_styles %>
@@ -13,26 +13,30 @@
13
13
 
14
14
  <header class="sqd-header">
15
15
  <div class="sqd-header__inner">
16
- <%= link_to "Solid Queue", root_path, class: "sqd-header__title" %>
16
+ <%= link_to t("solid_queue_web.layout.logo"), root_path, class: "sqd-header__title" %>
17
17
  <div class="sqd-nav-wrapper">
18
- <nav aria-label="Main">
18
+ <nav aria-label="<%= t("solid_queue_web.layout.nav.main_navigation") %>">
19
19
  <ul class="sqd-nav">
20
- <li><%= link_to "Dashboard", root_path, class: current_page?(root_path) ? "active" : "", aria: { current: current_page?(root_path) ? "page" : nil } %></li>
21
- <li><%= link_to "Queues", queues_path, class: current_page?(queues_path) ? "active" : "", aria: { current: current_page?(queues_path) ? "page" : nil } %></li>
22
- <li><%= link_to "Jobs", jobs_path, class: current_page?(jobs_path) ? "active" : "", aria: { current: current_page?(jobs_path) ? "page" : nil } %></li>
23
- <li><%= link_to "History", history_path, class: current_page?(history_path) ? "active" : "", aria: { current: current_page?(history_path) ? "page" : nil } %></li>
24
- <li><%= link_to "Performance", performance_path, class: current_page?(performance_path) ? "active" : "", aria: { current: current_page?(performance_path) ? "page" : nil } %></li>
25
- <li><%= link_to "Failed", failed_jobs_path, class: current_page?(failed_jobs_path) ? "active" : "", aria: { current: current_page?(failed_jobs_path) ? "page" : nil } %></li>
26
- <li><%= link_to "Recurring", recurring_tasks_path, class: current_page?(recurring_tasks_path) ? "active" : "", aria: { current: current_page?(recurring_tasks_path) ? "page" : nil } %></li>
27
- <li><%= link_to "Processes", processes_path, class: current_page?(processes_path) ? "active" : "", aria: { current: current_page?(processes_path) ? "page" : nil } %></li>
28
- <li><%= link_to "Search", search_path, class: current_page?(search_path) ? "active" : "", aria: { current: current_page?(search_path) ? "page" : nil } %></li>
20
+ <li><%= link_to t("solid_queue_web.layout.nav.dashboard"), root_path, class: current_page?(root_path) ? "active" : "", aria: { current: current_page?(root_path) ? "page" : nil } %></li>
21
+ <li><%= link_to t("solid_queue_web.layout.nav.queues"), queues_path, class: current_page?(queues_path) ? "active" : "", aria: { current: current_page?(queues_path) ? "page" : nil } %></li>
22
+ <li><%= link_to t("solid_queue_web.layout.nav.jobs"), jobs_path, class: current_page?(jobs_path) ? "active" : "", aria: { current: current_page?(jobs_path) ? "page" : nil } %></li>
23
+ <li><%= link_to t("solid_queue_web.layout.nav.history"), history_path, class: current_page?(history_path) ? "active" : "", aria: { current: current_page?(history_path) ? "page" : nil } %></li>
24
+ <li><%= link_to t("solid_queue_web.layout.nav.performance"), performance_path, class: current_page?(performance_path) ? "active" : "", aria: { current: current_page?(performance_path) ? "page" : nil } %></li>
25
+ <li><%= link_to t("solid_queue_web.layout.nav.failed"), failed_jobs_path, class: current_page?(failed_jobs_path) ? "active" : "", aria: { current: current_page?(failed_jobs_path) ? "page" : nil } %></li>
26
+ <li><%= link_to t("solid_queue_web.layout.nav.recurring"), recurring_tasks_path, class: current_page?(recurring_tasks_path) ? "active" : "", aria: { current: current_page?(recurring_tasks_path) ? "page" : nil } %></li>
27
+ <li><%= link_to t("solid_queue_web.layout.nav.processes"), processes_path, class: current_page?(processes_path) ? "active" : "", aria: { current: current_page?(processes_path) ? "page" : nil } %></li>
28
+ <li><%= link_to t("solid_queue_web.layout.nav.search"), search_path, class: current_page?(search_path) ? "active" : "", aria: { current: current_page?(search_path) ? "page" : nil } %></li>
29
+ <li><%= link_to t("solid_queue_web.layout.nav.audit"), audit_path, class: current_page?(audit_path) ? "active" : "", aria: { current: current_page?(audit_path) ? "page" : nil } %></li>
30
+ <% SolidQueueWeb.nav_links.each do |link| %>
31
+ <li><%= link_to link[:label], link[:url] %></li>
32
+ <% end %>
29
33
  </ul>
30
34
  </nav>
31
35
  </div>
32
36
  <div class="sqd-header__controls">
33
- <button class="sqd-theme-toggle" aria-label="Switch to dark mode"
37
+ <button class="sqd-theme-toggle" aria-label="<%= t("solid_queue_web.layout.theme_toggle") %>"
34
38
  data-theme-target="toggle" data-action="theme#toggle">☽</button>
35
- <button class="sqd-nav-toggle" aria-label="Toggle navigation" aria-expanded="false"
39
+ <button class="sqd-nav-toggle" aria-label="<%= t("solid_queue_web.layout.nav.toggle") %>" aria-expanded="false"
36
40
  onclick="var open=document.querySelector('.sqd-nav-wrapper').classList.toggle('sqd-nav--open');this.setAttribute('aria-expanded',open)">
37
41
  <span></span>
38
42
  <span></span>
@@ -54,4 +58,4 @@
54
58
  </main>
55
59
 
56
60
  </body>
57
- </html>
61
+ </html>
@@ -0,0 +1,78 @@
1
+ <h1 class="sqd-page-title"><%= t("solid_queue_web.audit.title") %></h1>
2
+
3
+ <div class="sqd-page-header">
4
+ <div class="sqd-filters">
5
+ <form action="<%= audit_path %>" method="get" style="display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap;">
6
+ <select name="action_filter" class="sqd-select" aria-label="<%= t("solid_queue_web.audit.aria_filter_action") %>" onchange="this.form.submit()">
7
+ <option value=""><%= t("solid_queue_web.audit.all_actions") %></option>
8
+ <% SolidQueueWeb::AuditEvent::ACTIONS.each do |a| %>
9
+ <option value="<%= a %>" <%= @action_filter == a ? "selected" : "" %>><%= a.tr("_", " ") %></option>
10
+ <% end %>
11
+ </select>
12
+ <% if @actor_filter.present? %>
13
+ <span class="sqd-badge sqd-badge--muted"><%= t("solid_queue_web.audit.actor_label") %> <%= @actor_filter %></span>
14
+ <%= link_to "×", audit_path(action_filter: @action_filter, queue: @queue_filter), class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
15
+ <% end %>
16
+ <% if @queue_filter.present? %>
17
+ <span class="sqd-badge sqd-badge--muted"><%= t("solid_queue_web.audit.queue_label") %> <%= @queue_filter %></span>
18
+ <%= link_to "×", audit_path(action_filter: @action_filter, actor: @actor_filter), class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
19
+ <% end %>
20
+ <% if @action_filter.present? || @actor_filter.present? || @queue_filter.present? %>
21
+ <%= link_to t("solid_queue_web.audit.clear_all"), audit_path, class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
22
+ <% end %>
23
+ </form>
24
+ </div>
25
+ <% if @audit_events.any? %>
26
+ <div class="sqd-actions">
27
+ <%= link_to t("solid_queue_web.audit.export_csv"), audit_path(format: :csv, action_filter: @action_filter, actor: @actor_filter, queue: @queue_filter),
28
+ class: "sqd-btn sqd-btn--muted", data: { turbo: false } %>
29
+ </div>
30
+ <% end %>
31
+ </div>
32
+
33
+ <div class="sqd-card">
34
+ <% if @audit_events.empty? %>
35
+ <div class="sqd-empty"><%= t("solid_queue_web.audit.empty") %></div>
36
+ <% else %>
37
+ <table>
38
+ <thead>
39
+ <tr>
40
+ <th scope="col"><%= t("solid_queue_web.audit.col_time") %></th>
41
+ <th scope="col"><%= t("solid_queue_web.audit.col_action") %></th>
42
+ <th scope="col"><%= t("solid_queue_web.audit.col_actor") %></th>
43
+ <th scope="col"><%= t("solid_queue_web.audit.col_job_class") %></th>
44
+ <th scope="col"><%= t("solid_queue_web.audit.col_queue") %></th>
45
+ <th scope="col"><%= t("solid_queue_web.audit.col_count") %></th>
46
+ </tr>
47
+ </thead>
48
+ <tbody>
49
+ <% @audit_events.each do |event| %>
50
+ <tr>
51
+ <td class="sqd-mono"><%= format_timestamp(event.created_at) %></td>
52
+ <td><span class="sqd-badge sqd-badge--<%= event.action.include?("discard") ? "failed" : event.action.include?("paused") || event.action.include?("resumed") ? "paused" : "ready" %>"><%= event.action.tr("_", " ") %></span></td>
53
+ <td class="sqd-mono sqd-muted-text">
54
+ <% if event.actor.present? %>
55
+ <%= link_to event.actor, audit_path(action_filter: @action_filter, queue: @queue_filter, actor: event.actor), style: "color: inherit;" %>
56
+ <% else %>
57
+ <span style="color: var(--muted)">—</span>
58
+ <% end %>
59
+ </td>
60
+ <td class="sqd-mono"><%= event.job_class || "—" %></td>
61
+ <td class="sqd-mono">
62
+ <% if event.queue_name.present? %>
63
+ <%= link_to event.queue_name, audit_path(action_filter: @action_filter, actor: @actor_filter, queue: event.queue_name), style: "color: inherit;" %>
64
+ <% else %>
65
+ <span style="color: var(--muted)">—</span>
66
+ <% end %>
67
+ </td>
68
+ <td><%= event.item_count %></td>
69
+ </tr>
70
+ <% end %>
71
+ </tbody>
72
+ </table>
73
+ <% end %>
74
+ </div>
75
+
76
+ <% if @pagy.last > 1 %>
77
+ <%= @pagy.series_nav.html_safe %>
78
+ <% end %>
@@ -1,60 +1,60 @@
1
1
  <%= turbo_frame_tag "dashboard", target: "_top", data: { controller: "refresh", refresh_interval_value: SolidQueueWeb.dashboard_refresh_interval } do %>
2
- <h1 class="sqd-page-title">Dashboard</h1>
2
+ <h1 class="sqd-page-title"><%= t("solid_queue_web.dashboard.title") %></h1>
3
3
 
4
4
  <div class="sqd-stats">
5
5
  <%= link_to jobs_path(status: "ready"), class: "sqd-stat sqd-stat--ready sqd-stat--link" do %>
6
6
  <div class="sqd-stat__value"><%= @stats.counts[:ready] %></div>
7
- <div class="sqd-stat__label">Ready</div>
7
+ <div class="sqd-stat__label"><%= t("solid_queue_web.dashboard.ready") %></div>
8
8
  <% end %>
9
9
  <%= link_to jobs_path(status: "scheduled"), class: "sqd-stat sqd-stat--scheduled sqd-stat--link" do %>
10
10
  <div class="sqd-stat__value"><%= @stats.counts[:scheduled] %></div>
11
- <div class="sqd-stat__label">Scheduled</div>
11
+ <div class="sqd-stat__label"><%= t("solid_queue_web.dashboard.scheduled") %></div>
12
12
  <% end %>
13
13
  <%= link_to jobs_path(status: "claimed"), class: "sqd-stat sqd-stat--claimed sqd-stat--link" do %>
14
14
  <div class="sqd-stat__value"><%= @stats.counts[:claimed] %></div>
15
- <div class="sqd-stat__label">Running</div>
15
+ <div class="sqd-stat__label"><%= t("solid_queue_web.dashboard.running") %></div>
16
16
  <% end %>
17
17
  <%= link_to jobs_path(status: "blocked"), class: "sqd-stat sqd-stat--blocked sqd-stat--link" do %>
18
18
  <div class="sqd-stat__value"><%= @stats.counts[:blocked] %></div>
19
- <div class="sqd-stat__label">Blocked</div>
19
+ <div class="sqd-stat__label"><%= t("solid_queue_web.dashboard.blocked") %></div>
20
20
  <% end %>
21
21
  <%= link_to failed_jobs_path, class: "sqd-stat sqd-stat--failed sqd-stat--link" do %>
22
22
  <div class="sqd-stat__value"><%= @stats.counts[:failed] %></div>
23
- <div class="sqd-stat__label">Failed</div>
23
+ <div class="sqd-stat__label"><%= t("solid_queue_web.dashboard.failed") %></div>
24
24
  <% end %>
25
25
  <%= link_to queues_path, class: "sqd-stat sqd-stat--queues sqd-stat--link" do %>
26
26
  <div class="sqd-stat__value"><%= @stats.counts[:queues] %></div>
27
- <div class="sqd-stat__label">Queues</div>
27
+ <div class="sqd-stat__label"><%= t("solid_queue_web.dashboard.queues") %></div>
28
28
  <% end %>
29
29
  <%= link_to recurring_tasks_path, class: "sqd-stat sqd-stat--recurring sqd-stat--link" do %>
30
30
  <div class="sqd-stat__value"><%= @stats.counts[:recurring] %></div>
31
- <div class="sqd-stat__label">Recurring</div>
31
+ <div class="sqd-stat__label"><%= t("solid_queue_web.dashboard.recurring") %></div>
32
32
  <% end %>
33
33
  <%= link_to processes_path, class: "sqd-stat sqd-stat--processes sqd-stat--link" do %>
34
34
  <div class="sqd-stat__value"><%= @stats.counts[:processes] %></div>
35
- <div class="sqd-stat__label">Processes</div>
35
+ <div class="sqd-stat__label"><%= t("solid_queue_web.dashboard.processes") %></div>
36
36
  <% end %>
37
37
  <%= link_to history_path(period: "1h"), class: "sqd-stat sqd-stat--done sqd-stat--link" do %>
38
38
  <div class="sqd-stat__value"><%= @stats.throughput[:completed_1h] %></div>
39
- <div class="sqd-stat__label">Done (1h)</div>
39
+ <div class="sqd-stat__label"><%= t("solid_queue_web.dashboard.done_1h") %></div>
40
40
  <% end %>
41
41
  <%= link_to history_path(period: "24h"), class: "sqd-stat sqd-stat--done sqd-stat--link" do %>
42
42
  <div class="sqd-stat__value"><%= @stats.throughput[:completed_24h] %></div>
43
- <div class="sqd-stat__label">Done (24h)</div>
43
+ <div class="sqd-stat__label"><%= t("solid_queue_web.dashboard.done_24h") %></div>
44
44
  <% end %>
45
45
  </div>
46
46
 
47
47
  <% max_val = [@stats.sparkline.max, 1].max %>
48
48
  <div class="sqd-card" style="margin-bottom: 1rem;">
49
49
  <div class="sqd-card__header">
50
- <span class="sqd-card__title">Throughput &mdash; Last 12 Hours</span>
50
+ <span class="sqd-card__title"><%= t("solid_queue_web.dashboard.throughput_label") %></span>
51
51
  <div class="sqd-throughput__summary">
52
- <span>1h: <strong><%= @stats.throughput[:completed_1h] %></strong></span>
53
- <span>24h: <strong><%= @stats.throughput[:completed_24h] %></strong></span>
52
+ <span><%= t("solid_queue_web.dashboard.throughput_1h") %> <strong><%= @stats.throughput[:completed_1h] %></strong></span>
53
+ <span><%= t("solid_queue_web.dashboard.throughput_24h") %> <strong><%= @stats.throughput[:completed_24h] %></strong></span>
54
54
  </div>
55
55
  </div>
56
56
  <% if @stats.throughput[:completed_24h] == 0 %>
57
- <div class="sqd-sparkline__empty">No completed jobs in the last 24 hours</div>
57
+ <div class="sqd-sparkline__empty"><%= t("solid_queue_web.dashboard.no_completed_jobs") %></div>
58
58
  <% else %>
59
59
  <div class="sqd-sparkline" aria-label="Jobs completed per hour over the last 12 hours">
60
60
  <% @stats.sparkline.each_with_index do |count, i| %>
@@ -67,7 +67,7 @@
67
67
  style="height: <%= [pct, 3].max %>%"
68
68
  title="<%= hour_start.strftime('%-I%p').downcase %>: <%= count %> <%= "job".pluralize(count) %>"></div>
69
69
  </div>
70
- <div class="sqd-sparkline__tick"><%= show_tick ? (i == 11 ? "now" : hour_start.strftime("%-I%p").downcase) : "" %></div>
70
+ <div class="sqd-sparkline__tick"><%= show_tick ? (i == 11 ? t("solid_queue_web.dashboard.axis_now") : hour_start.strftime("%-I%p").downcase) : "" %></div>
71
71
  </div>
72
72
  <% end %>
73
73
  </div>
@@ -78,26 +78,26 @@
78
78
  <% max_depth = [@stats.depth_sparkline.max, 1].max %>
79
79
  <div class="sqd-card" style="margin-bottom: 1rem;">
80
80
  <div class="sqd-card__header">
81
- <span class="sqd-card__title">Queue Depth &mdash; Last 12 Hours</span>
81
+ <span class="sqd-card__title"><%= t("solid_queue_web.dashboard.queue_depth_label") %></span>
82
82
  <div class="sqd-throughput__summary">
83
- <span>Now: <strong><%= current_depth %></strong></span>
83
+ <span><%= t("solid_queue_web.dashboard.queue_depth_now") %> <strong><%= current_depth %></strong></span>
84
84
  </div>
85
85
  </div>
86
86
  <% if @stats.depth_sparkline.all?(&:zero?) %>
87
- <div class="sqd-sparkline__empty">No active jobs in the last 12 hours</div>
87
+ <div class="sqd-sparkline__empty"><%= t("solid_queue_web.dashboard.no_active_jobs") %></div>
88
88
  <% else %>
89
89
  <div class="sqd-sparkline" aria-label="Queue depth over the last 12 hours">
90
90
  <% @stats.depth_sparkline.each_with_index do |depth, i| %>
91
91
  <% pct = (depth.to_f / max_depth * 100).round %>
92
- <% t = i == 11 ? Time.current : (12 - i).hours.ago %>
92
+ <% t_val = i == 11 ? Time.current : (12 - i).hours.ago %>
93
93
  <% show_tick = [0, 3, 6, 9, 11].include?(i) %>
94
94
  <div class="sqd-sparkline__col">
95
95
  <div class="sqd-sparkline__bar-wrap">
96
96
  <div class="sqd-sparkline__bar sqd-sparkline__bar--depth"
97
97
  style="height: <%= [pct, 3].max %>%"
98
- title="<%= i == 11 ? "now" : t.strftime("%-I%p").downcase %>: <%= depth %> <%= "job".pluralize(depth) %> in queue"></div>
98
+ title="<%= i == 11 ? t("solid_queue_web.dashboard.axis_now") : t_val.strftime("%-I%p").downcase %>: <%= depth %> <%= "job".pluralize(depth) %> in queue"></div>
99
99
  </div>
100
- <div class="sqd-sparkline__tick"><%= show_tick ? (i == 11 ? "now" : t.strftime("%-I%p").downcase) : "" %></div>
100
+ <div class="sqd-sparkline__tick"><%= show_tick ? (i == 11 ? t("solid_queue_web.dashboard.axis_now") : t_val.strftime("%-I%p").downcase) : "" %></div>
101
101
  </div>
102
102
  <% end %>
103
103
  </div>
@@ -107,13 +107,13 @@
107
107
  <% max_failures = [@stats.failure_sparkline.max, 1].max %>
108
108
  <div class="sqd-card" style="margin-bottom: 1rem;">
109
109
  <div class="sqd-card__header">
110
- <span class="sqd-card__title">Failures &mdash; Last 12 Hours</span>
110
+ <span class="sqd-card__title"><%= t("solid_queue_web.dashboard.failures_label") %></span>
111
111
  <div class="sqd-throughput__summary">
112
- <span>Total: <strong><%= @stats.failure_sparkline.sum %></strong></span>
112
+ <span><%= t("solid_queue_web.dashboard.failures_total") %> <strong><%= @stats.failure_sparkline.sum %></strong></span>
113
113
  </div>
114
114
  </div>
115
115
  <% if @stats.failure_sparkline.all?(&:zero?) %>
116
- <div class="sqd-sparkline__empty">No failures in the last 12 hours</div>
116
+ <div class="sqd-sparkline__empty"><%= t("solid_queue_web.dashboard.no_failures") %></div>
117
117
  <% else %>
118
118
  <div class="sqd-sparkline" aria-label="Failed jobs per hour over the last 12 hours">
119
119
  <% @stats.failure_sparkline.each_with_index do |count, i| %>
@@ -126,7 +126,7 @@
126
126
  style="height: <%= [pct, 3].max %>%"
127
127
  title="<%= hour_start.strftime('%-I%p').downcase %>: <%= count %> <%= "failure".pluralize(count) %>"></div>
128
128
  </div>
129
- <div class="sqd-sparkline__tick"><%= show_tick ? (i == 11 ? "now" : hour_start.strftime("%-I%p").downcase) : "" %></div>
129
+ <div class="sqd-sparkline__tick"><%= show_tick ? (i == 11 ? t("solid_queue_web.dashboard.axis_now") : hour_start.strftime("%-I%p").downcase) : "" %></div>
130
130
  </div>
131
131
  <% end %>
132
132
  </div>
@@ -136,31 +136,31 @@
136
136
  <div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem;">
137
137
  <div class="sqd-card">
138
138
  <div class="sqd-card__header">
139
- <span class="sqd-card__title">Quick Links</span>
139
+ <span class="sqd-card__title"><%= t("solid_queue_web.dashboard.quick_links") %></span>
140
140
  </div>
141
141
  <div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
142
- <%= link_to "View all ready jobs", jobs_path(status: "ready"), class: "sqd-btn sqd-btn--muted" %>
143
- <%= link_to "View scheduled jobs", jobs_path(status: "scheduled"), class: "sqd-btn sqd-btn--muted" %>
144
- <%= link_to "View failed jobs", failed_jobs_path, class: "sqd-btn sqd-btn--muted" %>
145
- <%= link_to "Manage queues", queues_path, class: "sqd-btn sqd-btn--muted" %>
146
- <%= link_to "View recurring tasks", recurring_tasks_path, class: "sqd-btn sqd-btn--muted" %>
142
+ <%= link_to t("solid_queue_web.dashboard.view_ready_jobs"), jobs_path(status: "ready"), class: "sqd-btn sqd-btn--muted" %>
143
+ <%= link_to t("solid_queue_web.dashboard.view_scheduled_jobs"), jobs_path(status: "scheduled"), class: "sqd-btn sqd-btn--muted" %>
144
+ <%= link_to t("solid_queue_web.dashboard.view_failed_jobs"), failed_jobs_path, class: "sqd-btn sqd-btn--muted" %>
145
+ <%= link_to t("solid_queue_web.dashboard.manage_queues"), queues_path, class: "sqd-btn sqd-btn--muted" %>
146
+ <%= link_to t("solid_queue_web.dashboard.view_recurring_tasks"), recurring_tasks_path, class: "sqd-btn sqd-btn--muted" %>
147
147
  </div>
148
148
  </div>
149
149
 
150
150
  <% if @stats.counts[:failed] > 0 %>
151
151
  <div class="sqd-card">
152
152
  <div class="sqd-card__header">
153
- <span class="sqd-card__title">Failed Jobs</span>
153
+ <span class="sqd-card__title"><%= t("solid_queue_web.dashboard.failed_jobs_card") %></span>
154
154
  </div>
155
155
  <div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
156
156
  <p style="color: var(--danger); font-size: 13px;">
157
- <%= pluralize(@stats.counts[:failed], "failed job") %> need attention.
157
+ <%= t("solid_queue_web.dashboard.need_attention", count: @stats.counts[:failed]) %>
158
158
  </p>
159
- <%= button_to "Retry All Failed", retry_all_failed_jobs_path,
159
+ <%= button_to t("solid_queue_web.dashboard.retry_all_failed"), retry_all_failed_jobs_path,
160
160
  method: :post,
161
161
  class: "sqd-btn sqd-btn--primary",
162
- data: { confirm: "Retry all #{@stats.counts[:failed]} failed #{"job".pluralize(@stats.counts[:failed])}?" } %>
163
- <%= link_to "Review →", failed_jobs_path, class: "sqd-btn sqd-btn--muted" %>
162
+ data: { confirm: t("solid_queue_web.dashboard.confirm_retry_all_failed", count: @stats.counts[:failed]) } %>
163
+ <%= link_to t("solid_queue_web.dashboard.review"), failed_jobs_path, class: "sqd-btn sqd-btn--muted" %>
164
164
  </div>
165
165
  </div>
166
166
  <% end %>
@@ -168,13 +168,13 @@
168
168
  <% if SolidQueueWeb.slow_job_threshold && @stats.slow_jobs_count > 0 %>
169
169
  <div class="sqd-card">
170
170
  <div class="sqd-card__header">
171
- <span class="sqd-card__title">Slow Jobs</span>
171
+ <span class="sqd-card__title"><%= t("solid_queue_web.dashboard.slow_jobs_card") %></span>
172
172
  </div>
173
173
  <div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
174
174
  <p style="color: var(--warning); font-size: 13px;">
175
- <%= pluralize(@stats.slow_jobs_count, "job") %> running longer than <%= distance_of_time_in_words(SolidQueueWeb.slow_job_threshold) %>.
175
+ <%= t("solid_queue_web.dashboard.slow_jobs_warning", count: @stats.slow_jobs_count, threshold: distance_of_time_in_words(SolidQueueWeb.slow_job_threshold)) %>
176
176
  </p>
177
- <%= link_to "Review →", jobs_path(status: "claimed"), class: "sqd-btn sqd-btn--muted" %>
177
+ <%= link_to t("solid_queue_web.dashboard.review"), jobs_path(status: "claimed"), class: "sqd-btn sqd-btn--muted" %>
178
178
  </div>
179
179
  </div>
180
180
  <% end %>
@@ -182,19 +182,40 @@
182
182
  <% if @stats.counts[:blocked] > 0 %>
183
183
  <div class="sqd-card">
184
184
  <div class="sqd-card__header">
185
- <span class="sqd-card__title">Blocked Jobs</span>
185
+ <span class="sqd-card__title"><%= t("solid_queue_web.dashboard.blocked_jobs_card") %></span>
186
186
  </div>
187
187
  <div style="padding: 1rem; display: flex; flex-direction: column; gap: 0.5rem;">
188
188
  <p style="color: var(--warning); font-size: 13px;">
189
- <%= pluralize(@stats.counts[:blocked], "blocked job") %>.
189
+ <%= t("solid_queue_web.dashboard.blocked_jobs_count", count: @stats.counts[:blocked]) %>
190
190
  </p>
191
- <%= button_to "Discard All Blocked", blocked_jobs_path,
191
+ <%= button_to t("solid_queue_web.dashboard.discard_all_blocked"), blocked_jobs_path,
192
192
  method: :delete,
193
193
  class: "sqd-btn sqd-btn--danger",
194
- data: { confirm: "Discard all #{@stats.counts[:blocked]} blocked #{"job".pluralize(@stats.counts[:blocked])}? This cannot be undone." } %>
195
- <%= link_to "Review →", jobs_path(status: "blocked"), class: "sqd-btn sqd-btn--muted" %>
194
+ data: { confirm: t("solid_queue_web.dashboard.confirm_discard_all_blocked", count: @stats.counts[:blocked]) } %>
195
+ <%= link_to t("solid_queue_web.dashboard.review"), jobs_path(status: "blocked"), class: "sqd-btn sqd-btn--muted" %>
196
196
  </div>
197
197
  </div>
198
198
  <% end %>
199
+
200
+ <% SolidQueueWeb.dashboard_cards.each do |card| %>
201
+ <div class="sqd-card">
202
+ <div class="sqd-card__header">
203
+ <span class="sqd-card__title"><%= card[:title] %></span>
204
+ <% if card[:link] %>
205
+ <%= link_to card[:link][:label], card[:link][:url], class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
206
+ <% end %>
207
+ </div>
208
+ <% if card[:stats] %>
209
+ <div class="sqd-card__body">
210
+ <% card[:stats].call.each do |label, value| %>
211
+ <div class="sqd-custom-stat">
212
+ <span class="sqd-custom-stat__label"><%= label %></span>
213
+ <span class="sqd-custom-stat__value"><%= value %></span>
214
+ </div>
215
+ <% end %>
216
+ </div>
217
+ <% end %>
218
+ </div>
219
+ <% end %>
199
220
  </div>
200
- <% end %>
221
+ <% end %>
@@ -1,7 +1,7 @@
1
1
  <div class="sqd-page-header">
2
- <h1 class="sqd-page-title">Error Summary</h1>
2
+ <h1 class="sqd-page-title"><%= t("solid_queue_web.failed_job_errors.title") %></h1>
3
3
  <div class="sqd-actions">
4
- <%= link_to "← Failed Jobs", failed_jobs_path, class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
4
+ <%= link_to t("solid_queue_web.failed_job_errors.back"), failed_jobs_path, class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
5
5
  </div>
6
6
  </div>
7
7
 
@@ -10,9 +10,9 @@
10
10
  <table>
11
11
  <thead>
12
12
  <tr>
13
- <th scope="col">Error Class</th>
14
- <th scope="col">Message</th>
15
- <th scope="col" style="text-align: right;">Count</th>
13
+ <th scope="col"><%= t("solid_queue_web.failed_job_errors.col_error_class") %></th>
14
+ <th scope="col"><%= t("solid_queue_web.failed_job_errors.col_message") %></th>
15
+ <th scope="col" style="text-align: right;"><%= t("solid_queue_web.failed_job_errors.col_count") %></th>
16
16
  </tr>
17
17
  </thead>
18
18
  <tbody>
@@ -39,6 +39,6 @@
39
39
  </div>
40
40
  <% else %>
41
41
  <div class="sqd-card">
42
- <div class="sqd-empty">No failed jobs. All clear!</div>
42
+ <div class="sqd-empty"><%= t("solid_queue_web.failed_job_errors.empty") %></div>
43
43
  </div>
44
- <% end %>
44
+ <% end %>