pgbus 0.2.0 → 0.2.2

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/pgbus/api/insights_controller.rb +5 -4
  3. data/app/controllers/pgbus/application_controller.rb +40 -0
  4. data/app/controllers/pgbus/insights_controller.rb +3 -2
  5. data/app/controllers/pgbus/locale_controller.rb +15 -0
  6. data/app/helpers/pgbus/application_helper.rb +67 -7
  7. data/app/views/layouts/pgbus/application.html.erb +50 -14
  8. data/app/views/pgbus/dashboard/_processes_table.html.erb +6 -6
  9. data/app/views/pgbus/dashboard/_queues_table.html.erb +8 -8
  10. data/app/views/pgbus/dashboard/_recent_failures.html.erb +6 -6
  11. data/app/views/pgbus/dashboard/_stats_cards.html.erb +9 -9
  12. data/app/views/pgbus/dashboard/show.html.erb +1 -1
  13. data/app/views/pgbus/dead_letter/_messages_table.html.erb +19 -19
  14. data/app/views/pgbus/dead_letter/index.html.erb +5 -5
  15. data/app/views/pgbus/dead_letter/show.html.erb +12 -12
  16. data/app/views/pgbus/events/index.html.erb +11 -11
  17. data/app/views/pgbus/events/show.html.erb +6 -6
  18. data/app/views/pgbus/insights/show.html.erb +41 -21
  19. data/app/views/pgbus/jobs/_enqueued_table.html.erb +19 -19
  20. data/app/views/pgbus/jobs/_failed_table.html.erb +11 -11
  21. data/app/views/pgbus/jobs/index.html.erb +5 -5
  22. data/app/views/pgbus/jobs/show.html.erb +13 -13
  23. data/app/views/pgbus/locks/index.html.erb +11 -11
  24. data/app/views/pgbus/outbox/index.html.erb +15 -15
  25. data/app/views/pgbus/processes/_processes_table.html.erb +7 -7
  26. data/app/views/pgbus/processes/index.html.erb +1 -1
  27. data/app/views/pgbus/queues/_queues_list.html.erb +14 -14
  28. data/app/views/pgbus/queues/index.html.erb +1 -1
  29. data/app/views/pgbus/queues/show.html.erb +11 -11
  30. data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +14 -14
  31. data/app/views/pgbus/recurring_tasks/index.html.erb +2 -2
  32. data/app/views/pgbus/recurring_tasks/show.html.erb +21 -21
  33. data/config/i18n-tasks.yml +41 -0
  34. data/config/locales/da.yml +348 -0
  35. data/config/locales/de.yml +348 -0
  36. data/config/locales/en.yml +348 -0
  37. data/config/locales/es.yml +348 -0
  38. data/config/locales/fi.yml +348 -0
  39. data/config/locales/fr.yml +348 -0
  40. data/config/locales/it.yml +348 -0
  41. data/config/locales/ja.yml +348 -0
  42. data/config/locales/nb.yml +348 -0
  43. data/config/locales/nl.yml +348 -0
  44. data/config/locales/pt.yml +348 -0
  45. data/config/locales/sv.yml +348 -0
  46. data/config/routes.rb +2 -0
  47. data/lib/pgbus/active_job/executor.rb +1 -1
  48. data/lib/pgbus/client.rb +60 -32
  49. data/lib/pgbus/configuration.rb +8 -2
  50. data/lib/pgbus/engine.rb +4 -0
  51. data/lib/pgbus/event_bus/publisher.rb +1 -7
  52. data/lib/pgbus/outbox/poller.rb +58 -20
  53. data/lib/pgbus/process/consumer.rb +16 -3
  54. data/lib/pgbus/process/worker.rb +15 -5
  55. data/lib/pgbus/version.rb +1 -1
  56. data/lib/pgbus/web/data_source.rb +11 -9
  57. metadata +15 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e06c29b37491ad78b26d4799f11baea645e4380b2ebb6950bec74ad7be339f13
4
- data.tar.gz: 712a67117a92021b45ab361476068c9885b03a8215ff02d8327e58f6b9a5abd2
3
+ metadata.gz: 11fc5e1aa1196d1332b426e72aab0364cc548925fe960ff6626a6cd8090b2de5
4
+ data.tar.gz: 52fd690b0a3afda834850f5fc0eb3a111fd18062af6b3ca7097fadc085abf1b9
5
5
  SHA512:
6
- metadata.gz: f3e083cd40e0a1d02577c015283d83e3aeb8a1205bde8bb21e5e4c5e6df8b70f031f894fb1bfc93771caaeb4d26b5e1c7cca08cfa01dfad0d31a2b946db997f4
7
- data.tar.gz: fe1d70f3a39bb849981a025b5d9fee6e2c447a0fc641ce771e606e42f2da8892a40e7cb68ecaa57aac47fe970c819cdd0c60f8c439d92fe1306662da309f1dfd
6
+ metadata.gz: f232df26f7a0122db40119b43bf0d03290ea583eaf889ac58c95bb81850c5f745603fa81b848be437f4298ffff5ed65c82c64b890da64bc663ef600a4d05fe0c
7
+ data.tar.gz: c2d5f381fa2e4cf4943fe9bd27815459faaf9f92c7bf40d97bb2ea93b5622dcb5a197d8ccdb63249fd2ee681d4c10961070e7016093bacda2ba81010939cd7aa
@@ -4,11 +4,12 @@ module Pgbus
4
4
  module Api
5
5
  class InsightsController < ApplicationController
6
6
  def show
7
+ minutes = insights_minutes
7
8
  render json: {
8
- summary: data_source.job_stats_summary,
9
- throughput: data_source.job_throughput,
10
- status_counts: data_source.job_status_counts,
11
- slowest: data_source.slowest_job_classes
9
+ summary: data_source.job_stats_summary(minutes: minutes),
10
+ throughput: data_source.job_throughput(minutes: minutes),
11
+ status_counts: data_source.job_status_counts(minutes: minutes),
12
+ slowest: data_source.slowest_job_classes(minutes: minutes)
12
13
  }
13
14
  end
14
15
  end
@@ -5,6 +5,7 @@ module Pgbus
5
5
  include Web::Authentication
6
6
 
7
7
  protect_from_forgery with: :exception
8
+ before_action :set_locale
8
9
 
9
10
  layout "pgbus/application"
10
11
 
@@ -20,6 +21,37 @@ module Pgbus
20
21
 
21
22
  private
22
23
 
24
+ def set_locale
25
+ I18n.locale = extract_locale || I18n.default_locale
26
+ end
27
+
28
+ def extract_locale
29
+ locale = params[:locale] || cookies[:pgbus_locale] || preferred_locale_from_header
30
+ locale if locale && available_locales.include?(locale.to_sym)
31
+ end
32
+
33
+ def preferred_locale_from_header
34
+ return unless request.env["HTTP_ACCEPT_LANGUAGE"]
35
+
36
+ request.env["HTTP_ACCEPT_LANGUAGE"]
37
+ .scan(/([a-z]{2}(?:-[A-Z]{2})?)\s*;?\s*(?:q=([0-9.]+))?/i)
38
+ .sort_by { |_, q| -(q&.to_f || 1.0) }
39
+ .each do |lang, _|
40
+ code = lang.downcase.to_sym
41
+ return code.to_s if available_locales.include?(code)
42
+
43
+ # Try base language (e.g., "de" from "de-AT")
44
+ base = lang.split("-").first.downcase.to_sym
45
+ return base.to_s if available_locales.include?(base)
46
+ end
47
+ nil
48
+ end
49
+
50
+ def available_locales
51
+ @available_locales ||= Dir[Pgbus::Engine.root.join("config", "locales", "*.yml")]
52
+ .map { |f| File.basename(f, ".yml").to_sym }
53
+ end
54
+
23
55
  def data_source
24
56
  @data_source ||= Pgbus.configuration.web_data_source || Web::DataSource.new
25
57
  end
@@ -32,6 +64,14 @@ module Pgbus
32
64
  Pgbus.configuration.web_per_page
33
65
  end
34
66
 
67
+ def insights_minutes
68
+ config = Pgbus.configuration
69
+ default = config.insights_default_minutes.to_i
70
+ max = config.stats_retention.to_i / 60
71
+ value = (params[:minutes] || default).to_i
72
+ value.clamp(1, [max, 1].max)
73
+ end
74
+
35
75
  def turbo_frame_request?
36
76
  request.headers["Turbo-Frame"].present? || params[:frame].present?
37
77
  end
@@ -3,8 +3,9 @@
3
3
  module Pgbus
4
4
  class InsightsController < ApplicationController
5
5
  def show
6
- @summary = data_source.job_stats_summary
7
- @slowest = data_source.slowest_job_classes
6
+ @minutes = insights_minutes
7
+ @summary = data_source.job_stats_summary(minutes: @minutes)
8
+ @slowest = data_source.slowest_job_classes(minutes: @minutes)
8
9
  end
9
10
  end
10
11
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgbus
4
+ class LocaleController < ApplicationController
5
+ def update
6
+ locale = params[:locale].to_s
7
+ if available_locales.include?(locale.to_sym)
8
+ cookies[:pgbus_locale] = { value: locale, expires: 1.year.from_now, path: "/" }
9
+ I18n.locale = locale
10
+ end
11
+
12
+ redirect_back fallback_location: pgbus.root_path
13
+ end
14
+ end
15
+ end
@@ -31,17 +31,21 @@ module Pgbus
31
31
 
32
32
  def pgbus_status_badge(healthy)
33
33
  if healthy
34
- tag.span("Healthy", class: "inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800")
34
+ tag.span(I18n.t("pgbus.helpers.status_badge.healthy"),
35
+ class: "inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800")
35
36
  else
36
- tag.span("Stale", class: "inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800")
37
+ tag.span(I18n.t("pgbus.helpers.status_badge.stale"),
38
+ class: "inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800")
37
39
  end
38
40
  end
39
41
 
40
42
  def pgbus_queue_badge(name)
41
43
  if name.to_s.end_with?("_dlq")
42
- tag.span("DLQ", class: "inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700")
44
+ tag.span(I18n.t("pgbus.helpers.queue_badge.dlq"),
45
+ class: "inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700")
43
46
  else
44
- tag.span("Queue", class: "inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700")
47
+ tag.span(I18n.t("pgbus.helpers.queue_badge.queue"),
48
+ class: "inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700")
45
49
  end
46
50
  end
47
51
 
@@ -76,7 +80,8 @@ module Pgbus
76
80
  def pgbus_paused_badge(paused)
77
81
  return unless paused
78
82
 
79
- tag.span("Paused", class: "inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800")
83
+ tag.span(I18n.t("pgbus.helpers.paused_badge"),
84
+ class: "inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-800")
80
85
  end
81
86
 
82
87
  def pgbus_parse_message(message)
@@ -123,14 +128,26 @@ module Pgbus
123
128
 
124
129
  def pgbus_recurring_health_badge(task)
125
130
  if task[:last_run_at].nil?
126
- tag.span("Pending",
131
+ tag.span(I18n.t("pgbus.helpers.recurring_health.pending"),
127
132
  class: "inline-flex items-center rounded-full bg-yellow-100 px-2.5 py-0.5 text-xs font-medium text-yellow-800")
128
133
  else
129
- tag.span("Active",
134
+ tag.span(I18n.t("pgbus.helpers.recurring_health.active"),
130
135
  class: "inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800")
131
136
  end
132
137
  end
133
138
 
139
+ def pgbus_time_range_label(minutes)
140
+ minutes = [minutes.to_i, 1].max
141
+
142
+ if minutes > 1440 && (minutes % 1440).zero?
143
+ pgbus_pluralize_unit(minutes / 1440, "day")
144
+ elsif minutes >= 60 && (minutes % 60).zero?
145
+ pgbus_pluralize_unit(minutes / 60, "hour")
146
+ else
147
+ pgbus_pluralize_unit(minutes, "minute")
148
+ end
149
+ end
150
+
134
151
  def pgbus_nav_link(label, path)
135
152
  active = request.path == path || (path != pgbus.root_path && request.path.start_with?(path))
136
153
  css = if active
@@ -140,5 +157,48 @@ module Pgbus
140
157
  end
141
158
  link_to label, path, class: css
142
159
  end
160
+
161
+ LOCALE_NAMES = {
162
+ da: "Dansk",
163
+ de: "Deutsch",
164
+ en: "English",
165
+ es: "Espa\u00f1ol",
166
+ fi: "Suomi",
167
+ fr: "Fran\u00e7ais",
168
+ it: "Italiano",
169
+ ja: "\u65E5\u672C\u8A9E",
170
+ nb: "Norsk",
171
+ nl: "Nederlands",
172
+ pt: "Portugu\u00eas",
173
+ sv: "Svenska"
174
+ }.freeze
175
+
176
+ def pgbus_locale_name(code)
177
+ LOCALE_NAMES[code.to_sym] || code.to_s.upcase
178
+ end
179
+
180
+ def pgbus_locale_flag(code)
181
+ case code.to_sym
182
+ when :da then "\u{1F1E9}\u{1F1F0}"
183
+ when :de then "\u{1F1E9}\u{1F1EA}"
184
+ when :en then "\u{1F1EC}\u{1F1E7}"
185
+ when :es then "\u{1F1EA}\u{1F1F8}"
186
+ when :fi then "\u{1F1EB}\u{1F1EE}"
187
+ when :fr then "\u{1F1EB}\u{1F1F7}"
188
+ when :it then "\u{1F1EE}\u{1F1F9}"
189
+ when :ja then "\u{1F1EF}\u{1F1F5}"
190
+ when :nb then "\u{1F1F3}\u{1F1F4}"
191
+ when :nl then "\u{1F1F3}\u{1F1F1}"
192
+ when :pt then "\u{1F1F5}\u{1F1F9}"
193
+ when :sv then "\u{1F1F8}\u{1F1EA}"
194
+ else "\u{1F310}"
195
+ end
196
+ end
197
+
198
+ private
199
+
200
+ def pgbus_pluralize_unit(count, unit)
201
+ count == 1 ? "1 #{unit}" : "#{count} #{unit}s"
202
+ end
143
203
  end
144
204
  end
@@ -1,9 +1,9 @@
1
1
  <!DOCTYPE html>
2
- <html lang="en" class="h-full">
2
+ <html lang="<%= I18n.locale %>" class="h-full">
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Pgbus Dashboard</title>
6
+ <title><%= t("pgbus.layout.title") %></title>
7
7
  <script src="https://cdn.tailwindcss.com"></script>
8
8
  <script>
9
9
  tailwind.config = { darkMode: 'class' };
@@ -17,6 +17,21 @@
17
17
  var isDark = document.documentElement.classList.toggle('dark');
18
18
  localStorage.setItem('pgbus-dark', isDark);
19
19
  }
20
+ // Locale dropdown toggle
21
+ document.addEventListener('click', function(e) {
22
+ var btn = e.target.closest('[data-action="click->dropdown#toggle"]');
23
+ var menu = btn && btn.parentElement.querySelector('[data-dropdown-target="menu"]');
24
+ // Close all menus first
25
+ document.querySelectorAll('[data-dropdown-target="menu"]').forEach(function(m) {
26
+ if (m !== menu) m.classList.add('hidden');
27
+ });
28
+ if (menu) { menu.classList.toggle('hidden'); e.stopPropagation(); }
29
+ });
30
+ document.addEventListener('click', function() {
31
+ document.querySelectorAll('[data-dropdown-target="menu"]').forEach(function(m) {
32
+ m.classList.add('hidden');
33
+ });
34
+ });
20
35
  </script>
21
36
  <script type="module">
22
37
  import * as Turbo from "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8/dist/turbo.es2017.esm.js";
@@ -46,25 +61,45 @@
46
61
  <div class="flex h-14 items-center justify-between">
47
62
  <div class="flex items-center space-x-8">
48
63
  <%= link_to pgbus.root_path, class: "flex items-center space-x-2" do %>
49
- <span class="text-lg font-bold text-white">Pgbus</span>
64
+ <span class="text-lg font-bold text-white"><%= t("pgbus.layout.brand") %></span>
50
65
  <span class="rounded bg-gray-700 px-1.5 py-0.5 text-xs text-gray-300"><%= Pgbus::VERSION %></span>
51
66
  <% end %>
52
67
 
53
68
  <div class="flex space-x-1">
54
- <%= pgbus_nav_link "Dashboard", pgbus.root_path %>
55
- <%= pgbus_nav_link "Queues", pgbus.queues_path %>
56
- <%= pgbus_nav_link "Jobs", pgbus.jobs_path %>
57
- <%= pgbus_nav_link "Recurring", pgbus.recurring_tasks_path %>
58
- <%= pgbus_nav_link "Processes", pgbus.processes_path %>
59
- <%= pgbus_nav_link "Events", pgbus.events_path %>
60
- <%= pgbus_nav_link "DLQ", pgbus.dead_letter_index_path %>
61
- <%= pgbus_nav_link "Outbox", pgbus.outbox_index_path %>
62
- <%= pgbus_nav_link "Locks", pgbus.locks_path %>
63
- <%= pgbus_nav_link "Insights", pgbus.insights_path %>
69
+ <%= pgbus_nav_link t("pgbus.layout.nav.dashboard"), pgbus.root_path %>
70
+ <%= pgbus_nav_link t("pgbus.layout.nav.queues"), pgbus.queues_path %>
71
+ <%= pgbus_nav_link t("pgbus.layout.nav.jobs"), pgbus.jobs_path %>
72
+ <%= pgbus_nav_link t("pgbus.layout.nav.recurring"), pgbus.recurring_tasks_path %>
73
+ <%= pgbus_nav_link t("pgbus.layout.nav.processes"), pgbus.processes_path %>
74
+ <%= pgbus_nav_link t("pgbus.layout.nav.events"), pgbus.events_path %>
75
+ <%= pgbus_nav_link t("pgbus.layout.nav.dlq"), pgbus.dead_letter_index_path %>
76
+ <%= pgbus_nav_link t("pgbus.layout.nav.outbox"), pgbus.outbox_index_path %>
77
+ <%= pgbus_nav_link t("pgbus.layout.nav.locks"), pgbus.locks_path %>
78
+ <%= pgbus_nav_link t("pgbus.layout.nav.insights"), pgbus.insights_path %>
64
79
  </div>
65
80
  </div>
66
81
 
67
- <button onclick="toggleDarkMode()" class="rounded-md p-2 text-gray-400 hover:text-white focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-900" aria-label="Toggle dark mode">
82
+ <div class="flex items-center space-x-2">
83
+ <!-- Locale switcher -->
84
+ <div class="relative" data-controller="dropdown">
85
+ <button type="button" data-action="click->dropdown#toggle" class="rounded-md px-2 py-1.5 text-sm text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500">
86
+ <%= pgbus_locale_flag(I18n.locale) %> <%= I18n.locale.to_s.upcase %>
87
+ </button>
88
+ <div data-dropdown-target="menu" class="hidden absolute right-0 z-50 mt-1 w-44 origin-top-right rounded-md bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black/5 dark:ring-white/10">
89
+ <div class="py-1" role="menu">
90
+ <% controller.send(:available_locales).sort.each do |loc| %>
91
+ <%= form_tag pgbus.set_locale_path, method: :post, class: "contents" do %>
92
+ <%= hidden_field_tag :locale, loc %>
93
+ <button type="submit" role="menuitem" class="w-full text-left px-3 py-1.5 text-sm <%= loc == I18n.locale ? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700' %>">
94
+ <%= pgbus_locale_flag(loc) %> <%= pgbus_locale_name(loc) %>
95
+ </button>
96
+ <% end %>
97
+ <% end %>
98
+ </div>
99
+ </div>
100
+ </div>
101
+
102
+ <button onclick="toggleDarkMode()" class="rounded-md p-2 text-gray-400 hover:text-white focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-900" aria-label="<%= t("pgbus.layout.toggle_dark_mode") %>">
68
103
  <svg class="h-5 w-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
69
104
  <path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/>
70
105
  </svg>
@@ -72,6 +107,7 @@
72
107
  <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
73
108
  </svg>
74
109
  </button>
110
+ </div>
75
111
  </div>
76
112
  </div>
77
113
  </nav>
@@ -1,14 +1,14 @@
1
1
  <turbo-frame id="dashboard-processes" data-auto-refresh src="<%= pgbus.root_path(frame: 'processes') %>">
2
2
  <div>
3
- <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3">Active Processes</h2>
3
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3"><%= t("pgbus.dashboard.processes_table.title") %></h2>
4
4
  <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
5
5
  <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
6
6
  <thead class="bg-gray-50 dark:bg-gray-900">
7
7
  <tr>
8
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Kind</th>
9
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Host</th>
10
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">PID</th>
11
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Status</th>
8
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.processes_table.headers.kind") %></th>
9
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.processes_table.headers.host") %></th>
10
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.processes_table.headers.pid") %></th>
11
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.processes_table.headers.status") %></th>
12
12
  </tr>
13
13
  </thead>
14
14
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
@@ -21,7 +21,7 @@
21
21
  </tr>
22
22
  <% end %>
23
23
  <% if @processes.empty? %>
24
- <tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-400">No processes running</td></tr>
24
+ <tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.dashboard.processes_table.empty") %></td></tr>
25
25
  <% end %>
26
26
  </tbody>
27
27
  </table>
@@ -1,19 +1,19 @@
1
1
  <turbo-frame id="dashboard-queues" data-auto-refresh src="<%= pgbus.root_path(frame: 'queues') %>">
2
2
  <div class="mb-8">
3
3
  <div class="flex items-center justify-between mb-3">
4
- <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Queues</h2>
5
- <%= link_to "View all", pgbus.queues_path, class: "text-sm text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
4
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white"><%= t("pgbus.dashboard.queues_table.title") %></h2>
5
+ <%= link_to t("pgbus.dashboard.queues_table.view_all"), pgbus.queues_path, class: "text-sm text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
6
6
  </div>
7
7
 
8
8
  <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
9
9
  <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
10
10
  <thead class="bg-gray-50 dark:bg-gray-900">
11
11
  <tr>
12
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Queue</th>
13
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Depth</th>
14
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Visible</th>
15
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Oldest (s)</th>
16
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Total</th>
12
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.queue") %></th>
13
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.depth") %></th>
14
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.visible") %></th>
15
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.oldest") %></th>
16
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.total") %></th>
17
17
  </tr>
18
18
  </thead>
19
19
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
@@ -30,7 +30,7 @@
30
30
  </tr>
31
31
  <% end %>
32
32
  <% if @queues.empty? %>
33
- <tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400">No queues found</td></tr>
33
+ <tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.dashboard.queues_table.empty") %></td></tr>
34
34
  <% end %>
35
35
  </tbody>
36
36
  </table>
@@ -1,18 +1,18 @@
1
1
  <turbo-frame id="dashboard-failures" data-auto-refresh src="<%= pgbus.root_path(frame: 'failures') %>">
2
2
  <div>
3
3
  <div class="flex items-center justify-between mb-3">
4
- <h2 class="text-lg font-semibold text-gray-900 dark:text-white">Recent Failures</h2>
4
+ <h2 class="text-lg font-semibold text-gray-900 dark:text-white"><%= t("pgbus.dashboard.recent_failures.title") %></h2>
5
5
  <% if @recent_failures.any? %>
6
- <%= link_to "View all", pgbus.jobs_path(status: "failed"), class: "text-sm text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
6
+ <%= link_to t("pgbus.dashboard.recent_failures.view_all"), pgbus.jobs_path(status: "failed"), class: "text-sm text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
7
7
  <% end %>
8
8
  </div>
9
9
  <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
10
10
  <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
11
11
  <thead class="bg-gray-50 dark:bg-gray-900">
12
12
  <tr>
13
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Queue</th>
14
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Error</th>
15
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">When</th>
13
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.recent_failures.headers.queue") %></th>
14
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.recent_failures.headers.error") %></th>
15
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.recent_failures.headers.when") %></th>
16
16
  </tr>
17
17
  </thead>
18
18
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
@@ -24,7 +24,7 @@
24
24
  </tr>
25
25
  <% end %>
26
26
  <% if @recent_failures.empty? %>
27
- <tr><td colspan="3" class="px-4 py-8 text-center text-sm text-gray-400">No failures</td></tr>
27
+ <tr><td colspan="3" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.dashboard.recent_failures.empty") %></td></tr>
28
28
  <% end %>
29
29
  </tbody>
30
30
  </table>
@@ -1,42 +1,42 @@
1
1
  <turbo-frame id="dashboard-stats" data-auto-refresh src="<%= pgbus.root_path(frame: 'stats') %>">
2
2
  <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-6 mb-8">
3
3
  <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
4
- <p class="text-sm font-medium text-gray-500 dark:text-gray-400">Queues</p>
4
+ <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.queues") %></p>
5
5
  <p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:total_queues] %></p>
6
6
  </div>
7
7
 
8
8
  <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
9
- <p class="text-sm font-medium text-gray-500 dark:text-gray-400">Enqueued</p>
9
+ <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.enqueued") %></p>
10
10
  <p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@stats[:total_depth]) %></p>
11
- <p class="text-xs text-gray-400 dark:text-gray-500"><%= pgbus_number(@stats[:total_visible]) %> visible</p>
11
+ <p class="text-xs text-gray-400 dark:text-gray-500"><%= pgbus_number(@stats[:total_visible]) %> <%= t("pgbus.dashboard.stats_cards.visible") %></p>
12
12
  </div>
13
13
 
14
14
  <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
15
- <p class="text-sm font-medium text-gray-500 dark:text-gray-400">Processes</p>
15
+ <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.processes") %></p>
16
16
  <p class="mt-1 text-3xl font-semibold <%= @stats[:active_processes] > 0 ? 'text-green-600 dark:text-green-400' : 'text-gray-400' %>">
17
17
  <%= @stats[:active_processes] %>
18
18
  </p>
19
19
  </div>
20
20
 
21
21
  <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
22
- <p class="text-sm font-medium text-gray-500 dark:text-gray-400">Recurring</p>
22
+ <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.recurring") %></p>
23
23
  <p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:recurring_count] %></p>
24
24
  <p class="text-xs text-gray-400">
25
- <%= link_to "View tasks", pgbus.recurring_tasks_path, class: "text-blue-500 hover:text-blue-700 dark:text-blue-400" %>
25
+ <%= link_to t("pgbus.dashboard.stats_cards.view_tasks"), pgbus.recurring_tasks_path, class: "text-blue-500 hover:text-blue-700 dark:text-blue-400" %>
26
26
  </p>
27
27
  </div>
28
28
 
29
29
  <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
30
- <p class="text-sm font-medium text-gray-500 dark:text-gray-400">Failed / DLQ</p>
30
+ <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.failed_dlq") %></p>
31
31
  <p class="mt-1 text-3xl font-semibold <%= (@stats[:failed_count] + @stats[:dlq_depth]) > 0 ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-white' %>">
32
32
  <%= @stats[:failed_count] %> / <%= @stats[:dlq_depth] %>
33
33
  </p>
34
34
  </div>
35
35
 
36
36
  <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
37
- <p class="text-sm font-medium text-gray-500 dark:text-gray-400">Throughput</p>
37
+ <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.throughput") %></p>
38
38
  <p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:throughput_rate] %></p>
39
- <p class="text-xs text-gray-400">msgs/s</p>
39
+ <p class="text-xs text-gray-400"><%= t("pgbus.dashboard.stats_cards.throughput_unit") %></p>
40
40
  </div>
41
41
  </div>
42
42
  </turbo-frame>
@@ -1,4 +1,4 @@
1
- <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-6">Dashboard</h1>
1
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-6"><%= t("pgbus.dashboard.show.title") %></h1>
2
2
 
3
3
  <%= render "pgbus/dashboard/stats_cards" %>
4
4
 
@@ -3,11 +3,11 @@
3
3
  <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
4
4
  <thead class="bg-gray-50 dark:bg-gray-900">
5
5
  <tr>
6
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">ID</th>
7
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Job Class</th>
8
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Source Queue</th>
9
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Enqueued</th>
10
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Reads</th>
6
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dead_letter.messages_table.headers.id") %></th>
7
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dead_letter.messages_table.headers.job_class") %></th>
8
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dead_letter.messages_table.headers.source_queue") %></th>
9
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dead_letter.messages_table.headers.enqueued") %></th>
10
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dead_letter.messages_table.headers.reads") %></th>
11
11
  </tr>
12
12
  </thead>
13
13
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
@@ -27,39 +27,39 @@
27
27
  </summary>
28
28
  <div class="px-4 pb-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-100">
29
29
  <div class="flex items-center justify-between mt-3 mb-3">
30
- <span class="text-xs font-mono text-gray-400">Job ID: <%= payload["job_id"] %></span>
30
+ <span class="text-xs font-mono text-gray-400"><%= t("pgbus.dead_letter.messages_table.job_id") %> <%= payload["job_id"] %></span>
31
31
  <div class="flex space-x-2">
32
- <%= button_to "Retry", pgbus.retry_dead_letter_path(m[:msg_id], queue_name: m[:queue_name]), method: :post,
32
+ <%= button_to t("pgbus.dead_letter.messages_table.retry"), pgbus.retry_dead_letter_path(m[:msg_id], queue_name: m[:queue_name]), method: :post,
33
33
  class: "rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500",
34
34
  data: { turbo_frame: "_top" } %>
35
- <%= button_to "Discard", pgbus.discard_dead_letter_path(m[:msg_id], queue_name: m[:queue_name]), method: :post,
35
+ <%= button_to t("pgbus.dead_letter.messages_table.discard"), pgbus.discard_dead_letter_path(m[:msg_id], queue_name: m[:queue_name]), method: :post,
36
36
  class: "rounded-md bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-500",
37
- data: { turbo_confirm: "Permanently discard?", turbo_frame: "_top" } %>
37
+ data: { turbo_confirm: t("pgbus.dead_letter.messages_table.discard_confirm"), turbo_frame: "_top" } %>
38
38
  </div>
39
39
  </div>
40
40
  <div class="grid grid-cols-2 gap-4 mb-3">
41
41
  <div>
42
- <span class="text-xs font-medium text-gray-500">Arguments</span>
42
+ <span class="text-xs font-medium text-gray-500"><%= t("pgbus.dead_letter.messages_table.arguments") %></span>
43
43
  <pre class="text-xs text-gray-700 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto max-h-40"><%= JSON.pretty_generate(payload["arguments"] || []) rescue "—" %></pre>
44
44
  </div>
45
45
  <div>
46
- <span class="text-xs font-medium text-gray-500">Metadata</span>
46
+ <span class="text-xs font-medium text-gray-500"><%= t("pgbus.dead_letter.messages_table.metadata") %></span>
47
47
  <div class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 space-y-1">
48
- <% if payload["queue_name"] %><p><strong>Queue:</strong> <%= payload["queue_name"] %></p><% end %>
49
- <% if payload["priority"] %><p><strong>Priority:</strong> <%= payload["priority"] %></p><% end %>
50
- <% if payload["executions"] %><p><strong>Executions:</strong> <%= payload["executions"] %></p><% end %>
51
- <% if m[:vt] %><p><strong>Visible at:</strong> <%= m[:vt] %></p><% end %>
52
- <% if m[:last_read_at] %><p><strong>Last read:</strong> <%= m[:last_read_at] %></p><% end %>
48
+ <% if payload["queue_name"] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.queue") %></strong> <%= payload["queue_name"] %></p><% end %>
49
+ <% if payload["priority"] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.priority") %></strong> <%= payload["priority"] %></p><% end %>
50
+ <% if payload["executions"] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.executions") %></strong> <%= payload["executions"] %></p><% end %>
51
+ <% if m[:vt] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.visible_at") %></strong> <%= m[:vt] %></p><% end %>
52
+ <% if m[:last_read_at] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.last_read") %></strong> <%= m[:last_read_at] %></p><% end %>
53
53
  </div>
54
54
  </div>
55
55
  </div>
56
56
  <details class="mt-2">
57
- <summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700">Full JSON payload</summary>
57
+ <summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700"><%= t("pgbus.dead_letter.messages_table.full_json_payload") %></summary>
58
58
  <pre class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto max-h-96"><%= JSON.pretty_generate(payload) rescue m[:message] %></pre>
59
59
  </details>
60
60
  <% if m[:headers] %>
61
61
  <details class="mt-2">
62
- <summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700">Headers</summary>
62
+ <summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700"><%= t("pgbus.dead_letter.messages_table.headers_section") %></summary>
63
63
  <pre class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto"><%= JSON.pretty_generate(JSON.parse(m[:headers])) rescue m[:headers] %></pre>
64
64
  </details>
65
65
  <% end %>
@@ -69,7 +69,7 @@
69
69
  </tr>
70
70
  <% end %>
71
71
  <% if @messages.empty? %>
72
- <tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400">Dead letter queue is empty</td></tr>
72
+ <tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.dead_letter.messages_table.empty") %></td></tr>
73
73
  <% end %>
74
74
  </tbody>
75
75
  </table>
@@ -1,13 +1,13 @@
1
1
  <div class="flex items-center justify-between mb-6">
2
- <h1 class="text-2xl font-bold text-gray-900 dark:text-white">Dead Letter Queue</h1>
2
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.dead_letter.index.title") %></h1>
3
3
  <% if @messages.any? %>
4
4
  <div class="flex space-x-2">
5
- <%= button_to "Retry All", pgbus.retry_all_dead_letter_index_path, method: :post,
5
+ <%= button_to t("pgbus.dead_letter.index.retry_all"), pgbus.retry_all_dead_letter_index_path, method: :post,
6
6
  class: "rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-500",
7
- data: { turbo_confirm: "Retry all DLQ messages?" } %>
8
- <%= button_to "Discard All", pgbus.discard_all_dead_letter_index_path, method: :post,
7
+ data: { turbo_confirm: t("pgbus.dead_letter.index.retry_all_confirm") } %>
8
+ <%= button_to t("pgbus.dead_letter.index.discard_all"), pgbus.discard_all_dead_letter_index_path, method: :post,
9
9
  class: "rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500",
10
- data: { turbo_confirm: "Permanently discard all DLQ messages?" } %>
10
+ data: { turbo_confirm: t("pgbus.dead_letter.index.discard_all_confirm") } %>
11
11
  </div>
12
12
  <% end %>
13
13
  </div>