solid_queue_web 1.5.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +179 -5
  3. data/app/assets/stylesheets/solid_queue_web/_04_table.css +28 -0
  4. data/app/controllers/solid_queue_web/application_controller.rb +10 -0
  5. data/app/controllers/solid_queue_web/blocked_jobs_controller.rb +2 -2
  6. data/app/controllers/solid_queue_web/failed_jobs/arguments_controller.rb +3 -3
  7. data/app/controllers/solid_queue_web/failed_jobs/selections_controller.rb +4 -4
  8. data/app/controllers/solid_queue_web/failed_jobs_controller.rb +2 -2
  9. data/app/controllers/solid_queue_web/jobs/selections_controller.rb +3 -3
  10. data/app/controllers/solid_queue_web/jobs_controller.rb +4 -3
  11. data/app/controllers/solid_queue_web/queues/jobs_controller.rb +5 -5
  12. data/app/controllers/solid_queue_web/queues/pauses_controller.rb +4 -4
  13. data/app/controllers/solid_queue_web/recurring_tasks/runs_controller.rb +4 -4
  14. data/app/controllers/solid_queue_web/retry_failed_jobs_controller.rb +4 -5
  15. data/app/controllers/solid_queue_web/scheduled_jobs_controller.rb +5 -5
  16. data/app/views/layouts/solid_queue_web/application.html.erb +20 -17
  17. data/app/views/solid_queue_web/audit/index.html.erb +15 -15
  18. data/app/views/solid_queue_web/dashboard/index.html.erb +67 -46
  19. data/app/views/solid_queue_web/failed_jobs/errors/index.html.erb +7 -7
  20. data/app/views/solid_queue_web/failed_jobs/index.html.erb +31 -31
  21. data/app/views/solid_queue_web/history/index.html.erb +14 -14
  22. data/app/views/solid_queue_web/jobs/index.html.erb +42 -42
  23. data/app/views/solid_queue_web/jobs/show.html.erb +20 -20
  24. data/app/views/solid_queue_web/performance/index.html.erb +16 -14
  25. data/app/views/solid_queue_web/processes/index.html.erb +16 -16
  26. data/app/views/solid_queue_web/queues/index.html.erb +16 -16
  27. data/app/views/solid_queue_web/queues/jobs/index.html.erb +21 -21
  28. data/app/views/solid_queue_web/recurring_tasks/index.html.erb +15 -15
  29. data/app/views/solid_queue_web/search/index.html.erb +13 -13
  30. data/config/locales/en.yml +330 -0
  31. data/lib/solid_queue_web/engine.rb +1 -0
  32. data/lib/solid_queue_web/version.rb +1 -1
  33. data/lib/solid_queue_web.rb +13 -1
  34. metadata +2 -1
@@ -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,27 +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>
29
- <li><%= link_to "Audit", audit_path, class: current_page?(audit_path) ? "active" : "", aria: { current: current_page?(audit_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 %>
30
33
  </ul>
31
34
  </nav>
32
35
  </div>
33
36
  <div class="sqd-header__controls">
34
- <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") %>"
35
38
  data-theme-target="toggle" data-action="theme#toggle">☽</button>
36
- <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"
37
40
  onclick="var open=document.querySelector('.sqd-nav-wrapper').classList.toggle('sqd-nav--open');this.setAttribute('aria-expanded',open)">
38
41
  <span></span>
39
42
  <span></span>
@@ -55,4 +58,4 @@
55
58
  </main>
56
59
 
57
60
  </body>
58
- </html>
61
+ </html>
@@ -1,30 +1,30 @@
1
- <h1 class="sqd-page-title">Audit Log</h1>
1
+ <h1 class="sqd-page-title"><%= t("solid_queue_web.audit.title") %></h1>
2
2
 
3
3
  <div class="sqd-page-header">
4
4
  <div class="sqd-filters">
5
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="Filter by action" onchange="this.form.submit()">
7
- <option value="">All actions</option>
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
8
  <% SolidQueueWeb::AuditEvent::ACTIONS.each do |a| %>
9
9
  <option value="<%= a %>" <%= @action_filter == a ? "selected" : "" %>><%= a.tr("_", " ") %></option>
10
10
  <% end %>
11
11
  </select>
12
12
  <% if @actor_filter.present? %>
13
- <span class="sqd-badge sqd-badge--muted">Actor: <%= @actor_filter %></span>
13
+ <span class="sqd-badge sqd-badge--muted"><%= t("solid_queue_web.audit.actor_label") %> <%= @actor_filter %></span>
14
14
  <%= link_to "×", audit_path(action_filter: @action_filter, queue: @queue_filter), class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
15
15
  <% end %>
16
16
  <% if @queue_filter.present? %>
17
- <span class="sqd-badge sqd-badge--muted">Queue: <%= @queue_filter %></span>
17
+ <span class="sqd-badge sqd-badge--muted"><%= t("solid_queue_web.audit.queue_label") %> <%= @queue_filter %></span>
18
18
  <%= link_to "×", audit_path(action_filter: @action_filter, actor: @actor_filter), class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
19
19
  <% end %>
20
20
  <% if @action_filter.present? || @actor_filter.present? || @queue_filter.present? %>
21
- <%= link_to "Clear", audit_path, class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
21
+ <%= link_to t("solid_queue_web.audit.clear_all"), audit_path, class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
22
22
  <% end %>
23
23
  </form>
24
24
  </div>
25
25
  <% if @audit_events.any? %>
26
26
  <div class="sqd-actions">
27
- <%= link_to "Export CSV", audit_path(format: :csv, action_filter: @action_filter, actor: @actor_filter, queue: @queue_filter),
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
28
  class: "sqd-btn sqd-btn--muted", data: { turbo: false } %>
29
29
  </div>
30
30
  <% end %>
@@ -32,17 +32,17 @@
32
32
 
33
33
  <div class="sqd-card">
34
34
  <% if @audit_events.empty? %>
35
- <div class="sqd-empty">No audit events recorded.</div>
35
+ <div class="sqd-empty"><%= t("solid_queue_web.audit.empty") %></div>
36
36
  <% else %>
37
37
  <table>
38
38
  <thead>
39
39
  <tr>
40
- <th scope="col">Time</th>
41
- <th scope="col">Action</th>
42
- <th scope="col">Actor</th>
43
- <th scope="col">Job Class</th>
44
- <th scope="col">Queue</th>
45
- <th scope="col">Count</th>
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
46
  </tr>
47
47
  </thead>
48
48
  <tbody>
@@ -75,4 +75,4 @@
75
75
 
76
76
  <% if @pagy.last > 1 %>
77
77
  <%= @pagy.series_nav.html_safe %>
78
- <% end %>
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 %>
@@ -1,30 +1,30 @@
1
1
  <div class="sqd-page-header">
2
- <h1 class="sqd-page-title">Failed Jobs</h1>
2
+ <h1 class="sqd-page-title"><%= t("solid_queue_web.failed_jobs.title") %></h1>
3
3
  <% if @failed_jobs.any? %>
4
4
  <div class="sqd-actions">
5
- <%= link_to "Error Summary", failed_job_errors_path, class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
6
- <%= link_to "Export CSV", failed_jobs_path(format: :csv, queue: @queue, q: @search, period: @period),
5
+ <%= link_to t("solid_queue_web.failed_jobs.error_summary"), failed_job_errors_path, class: "sqd-btn sqd-btn--muted sqd-btn--sm" %>
6
+ <%= link_to t("solid_queue_web.failed_jobs.export_csv"), failed_jobs_path(format: :csv, queue: @queue, q: @search, period: @period),
7
7
  class: "sqd-btn sqd-btn--muted", data: { turbo: false } %>
8
- <%= button_to "Retry All", retry_all_failed_jobs_path,
8
+ <%= button_to t("solid_queue_web.failed_jobs.retry_all"), retry_all_failed_jobs_path,
9
9
  method: :post,
10
10
  params: { queue: @queue, q: @search, period: @period },
11
11
  class: "sqd-btn sqd-btn--primary",
12
- data: { confirm: "Retry all #{@failed_jobs.size} failed jobs?" } %>
12
+ data: { confirm: t("solid_queue_web.failed_jobs.confirm_retry_all", count: @failed_jobs.size) } %>
13
13
  <% if @failed_jobs.size > 1 %>
14
14
  <% %w[5s 10s 30s 1m].each do |interval| %>
15
15
  <%= button_to "+#{interval}", retry_all_failed_jobs_path,
16
16
  method: :post,
17
17
  params: { stagger: interval, queue: @queue, q: @search, period: @period },
18
18
  class: "sqd-btn sqd-btn--muted sqd-btn--sm",
19
- title: "Retry all, staggered #{interval} apart",
20
- data: { confirm: "Retry #{@failed_jobs.size} failed jobs staggered #{interval} apart?" } %>
19
+ title: t("solid_queue_web.failed_jobs.stagger_title", interval: interval),
20
+ data: { confirm: t("solid_queue_web.failed_jobs.confirm_stagger", count: @failed_jobs.size, interval: interval) } %>
21
21
  <% end %>
22
22
  <% end %>
23
- <%= button_to "Discard All", discard_all_failed_jobs_path,
23
+ <%= button_to t("solid_queue_web.failed_jobs.discard_all"), discard_all_failed_jobs_path,
24
24
  method: :post,
25
25
  params: { queue: @queue, q: @search, period: @period },
26
26
  class: "sqd-btn sqd-btn--danger",
27
- data: { confirm: "Discard all #{@failed_jobs.size} failed jobs? This cannot be undone." } %>
27
+ data: { confirm: t("solid_queue_web.failed_jobs.confirm_discard_all", count: @failed_jobs.size) } %>
28
28
  </div>
29
29
  <% end %>
30
30
  </div>
@@ -37,13 +37,13 @@
37
37
  <input type="hidden" name="sort" value="<%= @sort %>">
38
38
  <input type="hidden" name="direction" value="<%= @direction %>">
39
39
  <input class="sqd-search__input" type="search" name="q" value="<%= @search %>"
40
- placeholder="Filter by job class…" autocomplete="off" aria-label="Filter by job class"
40
+ placeholder="<%= t("solid_queue_web.jobs.placeholder_job_class") %>" autocomplete="off" aria-label="<%= t("solid_queue_web.jobs.aria_filter_job_class") %>"
41
41
  data-action="input->search#filter">
42
42
  <% if @search.present? %>
43
- <%= link_to "Clear", failed_jobs_path(queue: @queue, period: @period), class: "sqd-btn sqd-btn--muted" %>
43
+ <%= link_to t("solid_queue_web.shared.clear"), failed_jobs_path(queue: @queue, period: @period), class: "sqd-btn sqd-btn--muted" %>
44
44
  <% end %>
45
- <div class="sqd-period-filter" role="group" aria-label="Time period" data-controller="filters" data-filters-page-value="failed">
46
- <%= link_to "All", failed_jobs_path(queue: @queue, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, aria_label: "All time", data: { action: "click->filters#savePeriod", filters_period_param: "" } %>
45
+ <div class="sqd-period-filter" role="group" aria-label="<%= t("solid_queue_web.shared.period_filter.label") %>" data-controller="filters" data-filters-page-value="failed">
46
+ <%= link_to t("solid_queue_web.shared.period_filter.all"), failed_jobs_path(queue: @queue, q: @search), class: @period.nil? ? "active" : "", aria: { current: @period.nil? ? "true" : nil }, aria_label: "All time", data: { action: "click->filters#savePeriod", filters_period_param: "" } %>
47
47
  <%= link_to "1h", failed_jobs_path(queue: @queue, q: @search, period: "1h"), class: @period == "1h" ? "active" : "", aria: { current: @period == "1h" ? "true" : nil }, aria_label: "Last 1 hour", data: { action: "click->filters#savePeriod", filters_period_param: "1h" } %>
48
48
  <%= link_to "24h", failed_jobs_path(queue: @queue, q: @search, period: "24h"), class: @period == "24h" ? "active" : "", aria: { current: @period == "24h" ? "true" : nil }, aria_label: "Last 24 hours", data: { action: "click->filters#savePeriod", filters_period_param: "24h" } %>
49
49
  <%= link_to "7d", failed_jobs_path(queue: @queue, q: @search, period: "7d"), class: @period == "7d" ? "active" : "", aria: { current: @period == "7d" ? "true" : nil }, aria_label: "Last 7 days", data: { action: "click->filters#savePeriod", filters_period_param: "7d" } %>
@@ -63,20 +63,20 @@
63
63
  <% end %>
64
64
 
65
65
  <%= form_tag failed_job_selection_path, method: :delete, id: "discard-selection-form",
66
- data: { turbo_confirm: "Discard selected jobs? This cannot be undone." } do %>
66
+ data: { turbo_confirm: t("solid_queue_web.failed_jobs.confirm_discard_selected") } do %>
67
67
  <%= hidden_field_tag :queue, @queue %>
68
68
  <%= hidden_field_tag :q, @search %>
69
69
  <%= hidden_field_tag :period, @period %>
70
70
  <% end %>
71
71
 
72
72
  <div class="sqd-selection-bar" data-selection-target="bar" style="display: none;">
73
- <span class="sqd-muted-text"><span data-selection-target="count">0</span> selected</span>
73
+ <span class="sqd-muted-text"><span data-selection-target="count">0</span> <%= t("solid_queue_web.jobs.selected_label") %></span>
74
74
  <button type="button" class="sqd-btn sqd-btn--primary sqd-btn--sm"
75
75
  data-action="click->selection#submit"
76
- data-selection-form-id-param="retry-selection-form">Retry Selected</button>
76
+ data-selection-form-id-param="retry-selection-form"><%= t("solid_queue_web.failed_jobs.retry_selected") %></button>
77
77
  <button type="button" class="sqd-btn sqd-btn--danger sqd-btn--sm"
78
78
  data-action="click->selection#submit"
79
- data-selection-form-id-param="discard-selection-form">Discard Selected</button>
79
+ data-selection-form-id-param="discard-selection-form"><%= t("solid_queue_web.failed_jobs.discard_selected") %></button>
80
80
  </div>
81
81
 
82
82
  <div class="sqd-card">
@@ -86,14 +86,14 @@
86
86
  <th scope="col">
87
87
  <input type="checkbox" data-selection-target="selectAll"
88
88
  data-action="change->selection#selectAll"
89
- aria-label="Select all failed jobs">
89
+ aria-label="<%= t("solid_queue_web.failed_jobs.select_all") %>">
90
90
  </th>
91
91
  <% sort_url = ->(p) { failed_jobs_path(queue: @queue, q: @search, period: @period, **p) } %>
92
- <%= sort_header_th("Job Class", "class_name", sort_url, current_sort: @sort, current_dir: @direction) %>
93
- <%= sort_header_th("Queue", "queue_name", sort_url, current_sort: @sort, current_dir: @direction) %>
94
- <th scope="col">Error</th>
95
- <%= sort_header_th("Failed At", "created_at", sort_url, current_sort: @sort, current_dir: @direction) %>
96
- <th scope="col"><span class="sqd-sr-only">Actions</span></th>
92
+ <%= sort_header_th(t("solid_queue_web.failed_jobs.col_job_class"), "class_name", sort_url, current_sort: @sort, current_dir: @direction) %>
93
+ <%= sort_header_th(t("solid_queue_web.failed_jobs.col_queue"), "queue_name", sort_url, current_sort: @sort, current_dir: @direction) %>
94
+ <th scope="col"><%= t("solid_queue_web.failed_jobs.col_error") %></th>
95
+ <%= sort_header_th(t("solid_queue_web.failed_jobs.col_failed_at"), "created_at", sort_url, current_sort: @sort, current_dir: @direction) %>
96
+ <th scope="col"><span class="sqd-sr-only"><%= t("solid_queue_web.shared.actions") %></span></th>
97
97
  </tr>
98
98
  </thead>
99
99
  <tbody>
@@ -104,7 +104,7 @@
104
104
  <input type="checkbox" value="<%= execution.id %>"
105
105
  data-selection-target="checkbox"
106
106
  data-action="change->selection#toggle"
107
- aria-label="Select job <%= job.class_name %>">
107
+ aria-label="<%= t("solid_queue_web.failed_jobs.select_job", class_name: job.class_name) %>">
108
108
  </td>
109
109
  <td><%= link_to job.class_name, job_path(job), class: "sqd-table-link" %></td>
110
110
  <td>
@@ -122,11 +122,11 @@
122
122
  </td>
123
123
  <td class="sqd-mono"><%= format_timestamp(execution.created_at) %></td>
124
124
  <td class="sqd-row-actions">
125
- <%= button_to "Retry", retry_failed_job_path(execution), method: :post,
125
+ <%= button_to t("solid_queue_web.failed_jobs.retry"), retry_failed_job_path(execution), method: :post,
126
126
  class: "sqd-btn sqd-btn--primary sqd-btn--sm" %>
127
- <%= button_to "Discard", failed_job_path(execution), method: :delete,
127
+ <%= button_to t("solid_queue_web.failed_jobs.discard"), failed_job_path(execution), method: :delete,
128
128
  class: "sqd-btn sqd-btn--danger sqd-btn--sm",
129
- data: { confirm: "Discard this job?" } %>
129
+ data: { confirm: t("solid_queue_web.failed_jobs.confirm_discard") } %>
130
130
  </td>
131
131
  </tr>
132
132
  <% end %>
@@ -136,13 +136,13 @@
136
136
  </div>
137
137
  <% else %>
138
138
  <div class="sqd-card">
139
- <div class="sqd-empty">No failed jobs. All clear!</div>
139
+ <div class="sqd-empty"><%= t("solid_queue_web.failed_jobs.empty") %></div>
140
140
  </div>
141
141
  <% end %>
142
142
 
143
143
  <% if @queue.present? %>
144
144
  <p style="margin-top: 0.75rem; font-size: 13px; color: var(--muted);">
145
- Filtering by queue: <strong><%= @queue %></strong> &mdash;
146
- <%= link_to "Clear filter", failed_jobs_path(q: @search, period: @period) %>
145
+ <%= t("solid_queue_web.shared.filtering_by_queue") %> <strong><%= @queue %></strong> &mdash;
146
+ <%= link_to t("solid_queue_web.shared.clear_filter"), failed_jobs_path(q: @search, period: @period) %>
147
147
  </p>
148
- <% end %>
148
+ <% end %>