pgbus 0.2.1 → 0.2.3

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +88 -25
  3. data/app/controllers/pgbus/api/insights_controller.rb +5 -4
  4. data/app/controllers/pgbus/application_controller.rb +40 -0
  5. data/app/controllers/pgbus/insights_controller.rb +3 -2
  6. data/app/controllers/pgbus/locale_controller.rb +15 -0
  7. data/app/helpers/pgbus/application_helper.rb +67 -7
  8. data/app/views/layouts/pgbus/application.html.erb +51 -16
  9. data/app/views/pgbus/dashboard/_processes_table.html.erb +7 -7
  10. data/app/views/pgbus/dashboard/_queues_table.html.erb +9 -9
  11. data/app/views/pgbus/dashboard/_recent_failures.html.erb +7 -7
  12. data/app/views/pgbus/dashboard/_stats_cards.html.erb +10 -10
  13. data/app/views/pgbus/dashboard/show.html.erb +1 -1
  14. data/app/views/pgbus/dead_letter/_messages_table.html.erb +20 -20
  15. data/app/views/pgbus/dead_letter/index.html.erb +5 -5
  16. data/app/views/pgbus/dead_letter/show.html.erb +12 -12
  17. data/app/views/pgbus/events/index.html.erb +11 -11
  18. data/app/views/pgbus/events/show.html.erb +6 -6
  19. data/app/views/pgbus/insights/show.html.erb +41 -21
  20. data/app/views/pgbus/jobs/_enqueued_table.html.erb +20 -20
  21. data/app/views/pgbus/jobs/_failed_table.html.erb +12 -12
  22. data/app/views/pgbus/jobs/index.html.erb +5 -5
  23. data/app/views/pgbus/jobs/show.html.erb +13 -13
  24. data/app/views/pgbus/locks/index.html.erb +11 -11
  25. data/app/views/pgbus/outbox/index.html.erb +15 -15
  26. data/app/views/pgbus/processes/_processes_table.html.erb +8 -8
  27. data/app/views/pgbus/processes/index.html.erb +1 -1
  28. data/app/views/pgbus/queues/_queues_list.html.erb +15 -15
  29. data/app/views/pgbus/queues/index.html.erb +1 -1
  30. data/app/views/pgbus/queues/show.html.erb +11 -11
  31. data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +15 -15
  32. data/app/views/pgbus/recurring_tasks/index.html.erb +2 -2
  33. data/app/views/pgbus/recurring_tasks/show.html.erb +21 -21
  34. data/config/i18n-tasks.yml +41 -0
  35. data/config/locales/da.yml +348 -0
  36. data/config/locales/de.yml +348 -0
  37. data/config/locales/en.yml +348 -0
  38. data/config/locales/es.yml +348 -0
  39. data/config/locales/fi.yml +348 -0
  40. data/config/locales/fr.yml +348 -0
  41. data/config/locales/it.yml +348 -0
  42. data/config/locales/ja.yml +348 -0
  43. data/config/locales/nb.yml +348 -0
  44. data/config/locales/nl.yml +348 -0
  45. data/config/locales/pt.yml +348 -0
  46. data/config/locales/sv.yml +348 -0
  47. data/config/routes.rb +2 -0
  48. data/lib/pgbus/configuration.rb +8 -2
  49. data/lib/pgbus/engine.rb +4 -0
  50. data/lib/pgbus/version.rb +1 -1
  51. metadata +15 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35bd03aa1d30dde2aea609829f027d9a1355282fb049e22d4f3a20119039828a
4
- data.tar.gz: 341150d6777d9747f19b8446692ffd262403eee0b2e687461176cce820af8177
3
+ metadata.gz: 85f2e8748cf70bb74cb736b23e3e0288be5ca0b946b0eb80c08e1d28650e98c0
4
+ data.tar.gz: d1fd675bb9d9a5beb4607f8e0da684c606d096a835948abf60034bd601cdd7f2
5
5
  SHA512:
6
- metadata.gz: dafeed2ede684fedb7a95b76ac3504c0987624ba49bdd5b3c9ea18c67a51f241ce626069b3d62f0dff617d9582da83490e8b527e600f396a334785d56010c405
7
- data.tar.gz: 55a9207ec4e8bbe56ce2e8d7322f0bf242daf6b30f89ac87f4cd37dc4dad2565704503076e24b994a3c52aec31e822abfbdafe72ee11ef239227a703d823f875
6
+ metadata.gz: 99b168d4276ef5149effdbf7d18955c542b9f8507a67d022381a61c7c36cacdf58ab414e166e528495e244313ef12d10e054119361bbf77ca2e6cc260f6d1982
7
+ data.tar.gz: 606324c33ddd072b24135c91314258fc2c162e54b6610ec813457cae2b4df3ba5c0391fc5dbd8a1ff6e13f14032968320b41b9a1d9450d0b5fbe2d66686f2c9e
data/Rakefile CHANGED
@@ -50,15 +50,24 @@ task :build do
50
50
  sh("rm -rf /tmp/gem-verify #{gem_file}")
51
51
  end
52
52
 
53
- desc "Release a new version (rake release[1.2.3] or rake release[pre])"
54
- task :release, [:version] do |_t, args|
53
+ desc "Release a new version (rake release[1.2.3] or rake release[pre] or rake release[1.2.3,force])"
54
+ task :release, %i[version force] do |_t, args|
55
55
  require_relative "lib/pgbus/version"
56
56
 
57
+ def info(msg) = puts "\e[34m→\e[0m #{msg}"
58
+ def success(msg) = puts "\e[32m✓\e[0m #{msg}"
59
+ def skip(msg) = puts "\e[33m⊘\e[0m #{msg} \e[33m(skipped)\e[0m"
60
+ def warn(msg) = puts "\e[33m⚠\e[0m #{msg}"
61
+ def error(msg) = puts "\e[31m✗\e[0m #{msg}"
62
+ def header(msg) = puts "\n\e[1;36m#{msg}\e[0m\n#{"─" * msg.length}"
63
+
57
64
  new_version = args[:version]
58
- abort "Usage: rake release[X.Y.Z] or rake release[pre]" unless new_version
65
+ abort "\e[31mUsage: rake release[X.Y.Z] or rake release[X.Y.Z,force]\e[0m" unless new_version
66
+
67
+ force = args[:force]&.to_s&.downcase == "force"
59
68
 
60
69
  dirty = `git status --porcelain`.strip
61
- abort "Aborting: working directory is not clean.\n#{dirty}" unless dirty.empty?
70
+ abort "\e[31mAborting: working directory is not clean.\e[0m\n#{dirty}" unless dirty.empty?
62
71
 
63
72
  current = Pgbus::VERSION
64
73
  prerelease = new_version.match?(/alpha|beta|rc|pre/) || new_version == "pre"
@@ -69,43 +78,97 @@ task :release, [:version] do |_t, args|
69
78
  end
70
79
 
71
80
  tag = "v#{new_version}"
81
+ version_file = "lib/pgbus/version.rb"
72
82
 
73
- puts "Current version: #{current}"
74
- puts "New version: #{new_version}"
75
- puts "Tag: #{tag}"
76
- puts "Pre-release: #{prerelease}"
77
- puts ""
83
+ title = "Release #{tag}"
84
+ title += " (force)" if force
85
+ header title
86
+ info "Current version: #{current}"
87
+ info "New version: #{new_version}"
88
+ info "Pre-release: #{prerelease}"
89
+
90
+ # Step 0: Force cleanup — delete existing release and tag
91
+ if force
92
+ header "Force cleanup"
93
+ if system("gh release view #{tag} >/dev/null 2>&1")
94
+ sh("gh release delete #{tag} --yes --cleanup-tag")
95
+ success "Deleted release and remote tag #{tag}"
96
+ else
97
+ skip "No release #{tag} to delete"
98
+ end
99
+
100
+ if system("git rev-parse #{tag} >/dev/null 2>&1")
101
+ sh("git tag -d #{tag}")
102
+ success "Deleted local tag #{tag}"
103
+ else
104
+ skip "No local tag #{tag} to delete"
105
+ end
106
+ end
78
107
 
79
- # Update version file if needed
80
- version_file = "lib/pgbus/version.rb"
81
- if new_version != current
108
+ # Step 1: Update version file
109
+ header "Version"
110
+ if new_version == current
111
+ skip "Version already #{new_version}"
112
+ else
82
113
  content = File.read(version_file)
83
114
  content.sub!(/VERSION = ".*"/, "VERSION = \"#{new_version}\"")
84
115
  File.write(version_file, content)
85
- puts "Updated #{version_file}"
116
+ success "Updated #{version_file}"
86
117
  end
87
118
 
88
- # Verify gem builds cleanly
119
+ # Step 2: Verify gem builds cleanly
120
+ header "Build verification"
89
121
  sh("gem build pgbus.gemspec --strict")
90
122
  sh("rm -f pgbus-*.gem")
123
+ success "Gem builds cleanly"
91
124
 
92
- # Commit, push, and create release
93
- if new_version != current
125
+ # Step 3: Commit version bump
126
+ header "Git commit"
127
+ version_changed = !`git diff #{version_file}`.strip.empty? || !`git diff --cached #{version_file}`.strip.empty?
128
+ if version_changed
94
129
  sh("git add #{version_file}")
95
130
  sh("git commit -m 'chore: bump version to #{new_version}'")
131
+ success "Committed version bump"
132
+ else
133
+ skip "No version change to commit"
134
+ end
135
+
136
+ # Step 4: Push to origin
137
+ header "Git push"
138
+ local_sha = `git rev-parse HEAD`.strip
139
+ remote_sha = `git rev-parse origin/main 2>/dev/null`.strip
140
+ if local_sha == remote_sha
141
+ skip "origin/main already at #{local_sha[0..6]}"
142
+ else
143
+ sh("git push origin main")
144
+ success "Pushed to origin/main"
96
145
  end
97
- sh("git push origin main")
98
146
 
99
- pre_flag = prerelease ? "--prerelease" : ""
100
- sh("gh release create #{tag} --generate-notes --target main #{pre_flag}".strip)
147
+ # Step 5: Create release
148
+ header "Release"
149
+ tag_exists = system("git rev-parse #{tag} >/dev/null 2>&1")
150
+ release_exists = system("gh release view #{tag} >/dev/null 2>&1")
151
+
152
+ if release_exists
153
+ skip "Release #{tag} already exists (use force to re-create)"
154
+ elsif tag_exists
155
+ info "Tag #{tag} exists, creating release from it"
156
+ pre_flag = prerelease ? "--prerelease" : ""
157
+ sh("gh release create #{tag} --generate-notes #{pre_flag}".strip)
158
+ success "Release #{tag} created from existing tag"
159
+ else
160
+ pre_flag = prerelease ? "--prerelease" : ""
161
+ sh("gh release create #{tag} --generate-notes --target main #{pre_flag}".strip)
162
+ success "Release #{tag} created"
163
+ end
101
164
 
102
165
  puts ""
103
- puts "Release #{tag} created! CI will handle the rest:"
104
- puts " - Run tests"
105
- puts " - Build + verify gem"
106
- puts " - Sign with Sigstore"
107
- puts " - Publish to RubyGems"
108
- puts " - Upload assets to the release"
166
+ success "\e[1mRelease #{tag} complete!\e[0m CI will handle the rest:"
167
+ puts " Run tests"
168
+ puts " Build + verify gem"
169
+ puts " Sign with Sigstore"
170
+ puts " Publish to RubyGems"
171
+ puts " Upload assets to the release"
109
172
  end
110
173
 
111
174
  task default: %i[spec rubocop]
@@ -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,38 @@ 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
+ helper_method :available_locales
55
+
23
56
  def data_source
24
57
  @data_source ||= Pgbus.configuration.web_data_source || Web::DataSource.new
25
58
  end
@@ -32,6 +65,13 @@ module Pgbus
32
65
  Pgbus.configuration.web_per_page
33
66
  end
34
67
 
68
+ def insights_minutes
69
+ config = Pgbus.configuration
70
+ default = config.insights_default_minutes.to_i
71
+ value = (params[:minutes] || default).to_i
72
+ value.clamp(1, [default, 43_200].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,15 @@
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
+ <style>
8
+ /* Prevent white flash during navigation in dark mode.
9
+ Applied before Tailwind CDN loads so the background is correct immediately. */
10
+ html.dark { background-color: #030712; } /* gray-950 */
11
+ html.dark body { background-color: #030712; }
12
+ </style>
7
13
  <script src="https://cdn.tailwindcss.com"></script>
8
14
  <script>
9
15
  tailwind.config = { darkMode: 'class' };
@@ -17,9 +23,17 @@
17
23
  var isDark = document.documentElement.classList.toggle('dark');
18
24
  localStorage.setItem('pgbus-dark', isDark);
19
25
  }
26
+ // Close locale dropdown when clicking outside
27
+ document.addEventListener('click', function(e) {
28
+ var switcher = document.getElementById('pgbus-locale-switcher');
29
+ var menu = document.getElementById('pgbus-locale-menu');
30
+ if (menu && switcher && !switcher.contains(e.target)) {
31
+ menu.classList.add('hidden');
32
+ }
33
+ });
20
34
  </script>
21
35
  <script type="module">
22
- import * as Turbo from "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8/dist/turbo.es2017.esm.js";
36
+ import * as Turbo from "https://esm.sh/@hotwired/turbo@8";
23
37
 
24
38
  <% if Pgbus.configuration.web_live_updates %>
25
39
  const interval = <%= Pgbus.configuration.web_refresh_interval %>;
@@ -28,7 +42,10 @@
28
42
  function refreshFrames() {
29
43
  if (document.hidden) return;
30
44
  document.querySelectorAll("turbo-frame[data-auto-refresh]")
31
- .forEach(frame => frame.reload());
45
+ .forEach(frame => {
46
+ if (!frame.src && frame.dataset.src) frame.src = frame.dataset.src;
47
+ if (frame.src) frame.reload();
48
+ });
32
49
  }
33
50
  function start() { timer = setInterval(refreshFrames, interval); }
34
51
  function stop() { clearInterval(timer); }
@@ -46,25 +63,42 @@
46
63
  <div class="flex h-14 items-center justify-between">
47
64
  <div class="flex items-center space-x-8">
48
65
  <%= link_to pgbus.root_path, class: "flex items-center space-x-2" do %>
49
- <span class="text-lg font-bold text-white">Pgbus</span>
66
+ <span class="text-lg font-bold text-white"><%= t("pgbus.layout.brand") %></span>
50
67
  <span class="rounded bg-gray-700 px-1.5 py-0.5 text-xs text-gray-300"><%= Pgbus::VERSION %></span>
51
68
  <% end %>
52
69
 
53
70
  <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 %>
71
+ <%= pgbus_nav_link t("pgbus.layout.nav.dashboard"), pgbus.root_path %>
72
+ <%= pgbus_nav_link t("pgbus.layout.nav.queues"), pgbus.queues_path %>
73
+ <%= pgbus_nav_link t("pgbus.layout.nav.jobs"), pgbus.jobs_path %>
74
+ <%= pgbus_nav_link t("pgbus.layout.nav.recurring"), pgbus.recurring_tasks_path %>
75
+ <%= pgbus_nav_link t("pgbus.layout.nav.processes"), pgbus.processes_path %>
76
+ <%= pgbus_nav_link t("pgbus.layout.nav.events"), pgbus.events_path %>
77
+ <%= pgbus_nav_link t("pgbus.layout.nav.dlq"), pgbus.dead_letter_index_path %>
78
+ <%= pgbus_nav_link t("pgbus.layout.nav.outbox"), pgbus.outbox_index_path %>
79
+ <%= pgbus_nav_link t("pgbus.layout.nav.locks"), pgbus.locks_path %>
80
+ <%= pgbus_nav_link t("pgbus.layout.nav.insights"), pgbus.insights_path %>
64
81
  </div>
65
82
  </div>
66
83
 
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">
84
+ <div class="flex items-center space-x-2">
85
+ <!-- Locale switcher -->
86
+ <div class="relative" id="pgbus-locale-switcher">
87
+ <button type="button" onclick="document.getElementById('pgbus-locale-menu').classList.toggle('hidden')" class="rounded-md px-2 py-1 text-sm text-gray-300 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500">
88
+ <%= pgbus_locale_flag(I18n.locale) %> <%= I18n.locale.to_s.upcase %>
89
+ </button>
90
+ <div id="pgbus-locale-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">
91
+ <div class="py-1" role="menu">
92
+ <% available_locales.sort.each do |loc| %>
93
+ <a href="<%= pgbus.set_locale_path(locale: loc) %>" role="menuitem" class="block px-3 py-1 text-sm no-underline <%= 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
+ </a>
96
+ <% end %>
97
+ </div>
98
+ </div>
99
+ </div>
100
+
101
+ <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
102
  <svg class="h-5 w-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
69
103
  <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
104
  </svg>
@@ -72,6 +106,7 @@
72
106
  <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
73
107
  </svg>
74
108
  </button>
109
+ </div>
75
110
  </div>
76
111
  </div>
77
112
  </nav>
@@ -1,14 +1,14 @@
1
- <turbo-frame id="dashboard-processes" data-auto-refresh src="<%= pgbus.root_path(frame: 'processes') %>">
1
+ <turbo-frame id="dashboard-processes" data-auto-refresh data-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
- <turbo-frame id="dashboard-queues" data-auto-refresh src="<%= pgbus.root_path(frame: 'queues') %>">
1
+ <turbo-frame id="dashboard-queues" data-auto-refresh data-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
- <turbo-frame id="dashboard-failures" data-auto-refresh src="<%= pgbus.root_path(frame: 'failures') %>">
1
+ <turbo-frame id="dashboard-failures" data-auto-refresh data-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>