pgbus 0.2.4 → 0.2.6

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -1
  3. data/app/controllers/pgbus/application_controller.rb +13 -4
  4. data/app/helpers/pgbus/application_helper.rb +10 -0
  5. data/app/models/pgbus/application_record.rb +4 -1
  6. data/app/models/pgbus/batch_entry.rb +1 -1
  7. data/app/models/pgbus/blocked_execution.rb +1 -1
  8. data/app/models/pgbus/job_lock.rb +1 -1
  9. data/app/models/pgbus/job_stat.rb +1 -1
  10. data/app/models/pgbus/outbox_entry.rb +1 -1
  11. data/app/models/pgbus/process_entry.rb +1 -1
  12. data/app/models/pgbus/processed_event.rb +1 -1
  13. data/app/models/pgbus/queue_state.rb +1 -1
  14. data/app/models/pgbus/recurring_execution.rb +1 -1
  15. data/app/models/pgbus/recurring_task.rb +1 -1
  16. data/app/models/pgbus/semaphore.rb +1 -1
  17. data/app/views/layouts/pgbus/application.html.erb +104 -15
  18. data/app/views/pgbus/dashboard/_processes_table.html.erb +5 -5
  19. data/app/views/pgbus/dashboard/_queues_table.html.erb +6 -6
  20. data/app/views/pgbus/dashboard/_recent_failures.html.erb +4 -4
  21. data/app/views/pgbus/dead_letter/_messages_table.html.erb +1 -1
  22. data/app/views/pgbus/events/index.html.erb +8 -8
  23. data/app/views/pgbus/insights/show.html.erb +31 -29
  24. data/app/views/pgbus/jobs/_enqueued_table.html.erb +1 -1
  25. data/app/views/pgbus/jobs/_failed_table.html.erb +7 -7
  26. data/app/views/pgbus/locks/index.html.erb +7 -7
  27. data/app/views/pgbus/outbox/index.html.erb +7 -7
  28. data/app/views/pgbus/processes/_processes_table.html.erb +7 -7
  29. data/app/views/pgbus/queues/_queues_list.html.erb +8 -8
  30. data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +8 -8
  31. data/config/locales/da.yml +2 -0
  32. data/config/locales/de.yml +2 -0
  33. data/config/locales/en.yml +2 -0
  34. data/config/locales/es.yml +2 -0
  35. data/config/locales/fi.yml +2 -0
  36. data/config/locales/fr.yml +2 -0
  37. data/config/locales/it.yml +2 -0
  38. data/config/locales/ja.yml +2 -0
  39. data/config/locales/nb.yml +2 -0
  40. data/config/locales/nl.yml +2 -0
  41. data/config/locales/pt.yml +2 -0
  42. data/config/locales/sv.yml +2 -0
  43. data/lib/pgbus/bus_record.rb +16 -0
  44. data/lib/pgbus/configuration.rb +4 -2
  45. data/lib/pgbus/engine.rb +1 -1
  46. data/lib/pgbus/process/consumer_priority.rb +1 -1
  47. data/lib/pgbus/process/dispatcher.rb +1 -1
  48. data/lib/pgbus/process/queue_lock.rb +1 -1
  49. data/lib/pgbus/process/worker.rb +1 -1
  50. data/lib/pgbus/version.rb +1 -1
  51. data/lib/pgbus/web/data_source.rb +1 -1
  52. data/lib/pgbus.rb +4 -0
  53. data/lib/tasks/pgbus_pgmq.rake +1 -1
  54. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: daac3606ba66624f54b5b6cf8db0830827c2ce0492b7d6d38ce347d1e0f3aeb2
4
- data.tar.gz: ad7eb6ea7beae1e07a813cac5cac37980671f8106f2ac7444a127a81d6f7a2e3
3
+ metadata.gz: 0d12150a0ea8aea034f08e626bfa2c553cf75349eb502d7a0fe0a50a867aa11b
4
+ data.tar.gz: 990216a4e6b7db7be81e888d90fe2d56ea536fa1e64a2507206dcd5524a255c5
5
5
  SHA512:
6
- metadata.gz: f7a513fd3e2f7aac5a3c0db040a3e7b6b11b6c4c0f619f6f0d37ebbf3e2cd6336737af20e4a7a00da19a1937d5be2b8e24d666483bf661005422c04bbac1805c
7
- data.tar.gz: 255c0382fbc4f4b797577757ebe5bfa9611f350456292d78e41678d6ebe834c52a5588e2f902159b0c5467becb076f0b724b8dc782b21dfb0d423404dc31d178
6
+ metadata.gz: f7e706c2824a4f00401027bd08df2c663502586a0a48fdd2e4150341a1e50fac520c50a503b2a6cff2d48d482d069ac66a605b9ce73a14513d9e65537da45f88
7
+ data.tar.gz: 64ad1a0978e42ddd271d50c224540bb3200e291912e3fde1bee72fe30eb74470b60833580a0a211785aa0830eb640dde463546c3bd748a33e9de0f903e79abad
data/README.md CHANGED
@@ -211,7 +211,7 @@ mount Pgbus::Engine => "/pgbus"
211
211
 
212
212
  The dashboard shows queues, jobs, processes, failures, dead letter messages, and event subscribers. It auto-refreshes via Turbo Frames with no WebSocket dependency.
213
213
 
214
- Protect it in production:
214
+ Protect it in production with a simple auth lambda:
215
215
 
216
216
  ```ruby
217
217
  Pgbus.configure do |config|
@@ -221,6 +221,24 @@ Pgbus.configure do |config|
221
221
  end
222
222
  ```
223
223
 
224
+ Or inherit from your own authenticated controller (like mission_control-jobs):
225
+
226
+ ```ruby
227
+ Pgbus.configure do |config|
228
+ config.base_controller_class = "Admin::BaseController"
229
+ end
230
+ ```
231
+
232
+ When `base_controller_class` is set, all dashboard controllers inherit from that class instead of `ActionController::Base`. This is the recommended approach when mounting the dashboard inside an authenticated namespace -- your base controller's `before_action` filters, helper methods, and authentication logic apply automatically without monkey-patching.
233
+
234
+ Add a "back to app" button in the dashboard nav to return to your main application:
235
+
236
+ ```ruby
237
+ Pgbus.configure do |config|
238
+ config.return_to_app_url = "/admin"
239
+ end
240
+ ```
241
+
224
242
  ## Concurrency controls
225
243
 
226
244
  Limit how many jobs with the same key can run concurrently:
@@ -624,6 +642,8 @@ The dispatcher runs archive compaction as part of its maintenance loop, deleting
624
642
  | `outbox_batch_size` | `100` | Max entries per outbox poll cycle |
625
643
  | `outbox_retention` | `86400` | Seconds to keep published outbox entries (1 day) |
626
644
  | `idempotency_ttl` | `604800` | Seconds to keep processed event records (7 days, cleaned hourly) |
645
+ | `base_controller_class` | `"::ActionController::Base"` | Base class for dashboard controllers (string, constantized at load time) |
646
+ | `return_to_app_url` | `nil` | URL for "back to app" button in dashboard nav (nil hides the button) |
627
647
  | `web_auth` | `nil` | Lambda for dashboard authentication |
628
648
  | `web_refresh_interval` | `5000` | Dashboard auto-refresh interval in milliseconds |
629
649
  | `web_live_updates` | `true` | Enable Turbo Frames auto-refresh on dashboard |
@@ -1,7 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class ApplicationController < ActionController::Base
4
+ class ApplicationController < Pgbus.configuration.base_controller_class.constantize
5
+ # Ensure core ActionController modules are available even when the
6
+ # base controller is a slim subclass that may not include them all.
7
+ ActionController::Base::MODULES.each do |mod|
8
+ include mod unless self < mod
9
+ end
10
+
5
11
  include Web::Authentication
6
12
 
7
13
  protect_from_forgery with: :exception
@@ -10,7 +16,7 @@ module Pgbus
10
16
 
11
17
  layout "pgbus/application"
12
18
 
13
- helper Pgbus::ApplicationHelper
19
+ helper Pgbus::ApplicationHelper unless self < Pgbus::ApplicationHelper
14
20
 
15
21
  # Make `pgbus` route proxy available in views (e.g. pgbus.root_path).
16
22
  # With isolate_namespace, the non-prefixed helpers (root_path) work inside
@@ -57,8 +63,11 @@ module Pgbus
57
63
  end
58
64
 
59
65
  def available_locales
60
- @available_locales ||= Dir[Pgbus::Engine.root.join("config", "locales", "*.yml")]
61
- .map { |f| File.basename(f, ".yml").to_sym }
66
+ @available_locales ||= begin
67
+ pgbus_locales = Dir[Pgbus::Engine.root.join("config", "locales", "*.yml")]
68
+ .map { |f| File.basename(f, ".yml").to_sym }
69
+ pgbus_locales & I18n.available_locales
70
+ end
62
71
  end
63
72
  helper_method :available_locales
64
73
 
@@ -158,6 +158,16 @@ module Pgbus
158
158
  link_to label, path, class: css
159
159
  end
160
160
 
161
+ def pgbus_mobile_nav_link(label, path)
162
+ active = request.path == path || (path != pgbus.root_path && request.path.start_with?(path))
163
+ css = if active
164
+ "block rounded-md px-3 py-2 text-base font-medium text-white bg-gray-800"
165
+ else
166
+ "block rounded-md px-3 py-2 text-base font-medium text-gray-300 hover:text-white hover:bg-gray-700"
167
+ end
168
+ link_to label, path, class: css
169
+ end
170
+
161
171
  LOCALE_NAMES = {
162
172
  da: "Dansk",
163
173
  de: "Deutsch",
@@ -1,7 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class ApplicationRecord < ActiveRecord::Base
4
+ # Backward-compatible alias — host apps that subclassed
5
+ # Pgbus::ApplicationRecord will continue to work.
6
+ # New code should inherit from Pgbus::BusRecord directly.
7
+ class ApplicationRecord < BusRecord
5
8
  self.abstract_class = true
6
9
  end
7
10
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class BatchEntry < ApplicationRecord
4
+ class BatchEntry < BusRecord
5
5
  self.table_name = "pgbus_batches"
6
6
 
7
7
  COUNTER_COLUMNS = %w[completed_jobs discarded_jobs].freeze
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class BlockedExecution < ApplicationRecord
4
+ class BlockedExecution < BusRecord
5
5
  self.table_name = "pgbus_blocked_executions"
6
6
 
7
7
  scope :for_key, ->(key) { where(concurrency_key: key) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class JobLock < Pgbus::ApplicationRecord
4
+ class JobLock < BusRecord
5
5
  self.table_name = "pgbus_job_locks"
6
6
 
7
7
  # States:
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class JobStat < Pgbus::ApplicationRecord
4
+ class JobStat < BusRecord
5
5
  self.table_name = "pgbus_job_stats"
6
6
 
7
7
  scope :since, ->(time) { where("created_at >= ?", time) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class OutboxEntry < Pgbus::ApplicationRecord
4
+ class OutboxEntry < BusRecord
5
5
  self.table_name = "pgbus_outbox_entries"
6
6
 
7
7
  scope :unpublished, -> { where(published_at: nil) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class ProcessEntry < ApplicationRecord
4
+ class ProcessEntry < BusRecord
5
5
  self.table_name = "pgbus_processes"
6
6
 
7
7
  scope :stale, ->(threshold) { where("last_heartbeat_at < ?", threshold) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class ProcessedEvent < ApplicationRecord
4
+ class ProcessedEvent < BusRecord
5
5
  self.table_name = "pgbus_processed_events"
6
6
 
7
7
  scope :expired, ->(before) { where("processed_at < ?", before) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class QueueState < Pgbus::ApplicationRecord
4
+ class QueueState < BusRecord
5
5
  self.table_name = "pgbus_queue_states"
6
6
 
7
7
  scope :paused, -> { where(paused: true) }
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class RecurringExecution < ApplicationRecord
4
+ class RecurringExecution < BusRecord
5
5
  self.table_name = "pgbus_recurring_executions"
6
6
 
7
7
  validates :task_key, presence: true
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class RecurringTask < ApplicationRecord
4
+ class RecurringTask < BusRecord
5
5
  self.table_name = "pgbus_recurring_tasks"
6
6
 
7
7
  validates :key, presence: true, uniqueness: true
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- class Semaphore < ApplicationRecord
4
+ class Semaphore < BusRecord
5
5
  self.table_name = "pgbus_semaphores"
6
6
 
7
7
  scope :expired, ->(now = Time.current) { where("expires_at < ? OR value <= 0", now) }
@@ -9,6 +9,43 @@
9
9
  Applied before Tailwind CDN loads so the background is correct immediately. */
10
10
  html.dark { background-color: #030712; } /* gray-950 */
11
11
  html.dark body { background-color: #030712; }
12
+
13
+ /* Responsive tables: stack rows as cards on small screens */
14
+ @media (max-width: 1023px) {
15
+ .pgbus-table thead { display: none; }
16
+ .pgbus-table tbody tr {
17
+ display: block;
18
+ margin-bottom: 0.75rem;
19
+ border-radius: 0.5rem;
20
+ padding: 0.75rem;
21
+ border: 1px solid #e5e7eb;
22
+ }
23
+ html.dark .pgbus-table tbody tr { border-color: #374151; }
24
+ .pgbus-table tbody td {
25
+ display: flex;
26
+ justify-content: space-between;
27
+ align-items: baseline;
28
+ padding: 0.25rem 0;
29
+ border: none;
30
+ text-align: right;
31
+ }
32
+ .pgbus-table tbody td::before {
33
+ content: attr(data-label);
34
+ font-weight: 600;
35
+ font-size: 0.75rem;
36
+ text-transform: uppercase;
37
+ color: #6b7280;
38
+ text-align: left;
39
+ margin-right: 1rem;
40
+ flex-shrink: 0;
41
+ }
42
+ html.dark .pgbus-table tbody td::before { color: #9ca3af; }
43
+ .pgbus-table tbody td[colspan] {
44
+ display: block;
45
+ text-align: center;
46
+ }
47
+ .pgbus-table tbody td[colspan]::before { display: none; }
48
+ }
12
49
  </style>
13
50
  <script src="https://cdn.tailwindcss.com"></script>
14
51
  <script>
@@ -23,6 +60,15 @@
23
60
  var isDark = document.documentElement.classList.toggle('dark');
24
61
  localStorage.setItem('pgbus-dark', isDark);
25
62
  }
63
+ // Mobile menu toggle
64
+ function toggleMobileMenu() {
65
+ var menu = document.getElementById('pgbus-mobile-menu');
66
+ var openIcon = document.getElementById('pgbus-menu-open');
67
+ var closeIcon = document.getElementById('pgbus-menu-close');
68
+ menu.classList.toggle('hidden');
69
+ openIcon.classList.toggle('hidden');
70
+ closeIcon.classList.toggle('hidden');
71
+ }
26
72
  // Close locale dropdown when clicking outside
27
73
  document.addEventListener('click', function(e) {
28
74
  var switcher = document.getElementById('pgbus-locale-switcher');
@@ -43,8 +89,10 @@
43
89
  if (document.hidden) return;
44
90
  document.querySelectorAll("turbo-frame[data-auto-refresh]")
45
91
  .forEach(frame => {
46
- if (!frame.src && frame.dataset.src) frame.src = frame.dataset.src;
47
- if (frame.src) frame.reload();
92
+ try {
93
+ if (!frame.src && frame.dataset.src) frame.src = frame.dataset.src;
94
+ if (frame.src) frame.reload();
95
+ } catch (_) { /* Turbo may abort in-flight fetches during navigation */ }
48
96
  });
49
97
  }
50
98
  function start() { timer = setInterval(refreshFrames, interval); }
@@ -61,13 +109,24 @@
61
109
  <nav class="bg-gray-900 dark:bg-gray-950 border-b border-gray-800">
62
110
  <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
63
111
  <div class="flex h-14 items-center justify-between">
112
+ <!-- Left: brand + desktop nav -->
64
113
  <div class="flex items-center space-x-8">
65
- <%= link_to pgbus.root_path, class: "flex items-center space-x-2" do %>
66
- <span class="text-lg font-bold text-white"><%= t("pgbus.layout.brand") %></span>
67
- <span class="rounded bg-gray-700 px-1.5 py-0.5 text-xs text-gray-300"><%= Pgbus::VERSION %></span>
68
- <% end %>
114
+ <div class="flex items-center space-x-3">
115
+ <% if Pgbus.configuration.return_to_app_url %>
116
+ <a href="<%= Pgbus.configuration.return_to_app_url %>" class="rounded-md p-1.5 text-gray-400 hover:text-white hover:bg-gray-700" title="<%= t("pgbus.layout.return_to_app") %>">
117
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
118
+ <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"/>
119
+ </svg>
120
+ </a>
121
+ <% end %>
122
+ <%= link_to pgbus.root_path, class: "flex items-center space-x-2" do %>
123
+ <span class="text-lg font-bold text-white"><%= t("pgbus.layout.brand") %></span>
124
+ <span class="rounded bg-gray-700 px-1.5 py-0.5 text-xs text-gray-300"><%= Pgbus::VERSION %></span>
125
+ <% end %>
126
+ </div>
69
127
 
70
- <div class="flex space-x-1">
128
+ <!-- Desktop nav links (hidden on small screens) -->
129
+ <div class="hidden lg:flex space-x-1">
71
130
  <%= pgbus_nav_link t("pgbus.layout.nav.dashboard"), pgbus.root_path %>
72
131
  <%= pgbus_nav_link t("pgbus.layout.nav.queues"), pgbus.queues_path %>
73
132
  <%= pgbus_nav_link t("pgbus.layout.nav.jobs"), pgbus.jobs_path %>
@@ -81,6 +140,7 @@
81
140
  </div>
82
141
  </div>
83
142
 
143
+ <!-- Right: locale, dark mode, hamburger -->
84
144
  <div class="flex items-center space-x-2">
85
145
  <!-- Locale switcher -->
86
146
  <div class="relative" id="pgbus-locale-switcher">
@@ -98,17 +158,46 @@
98
158
  </div>
99
159
  </div>
100
160
 
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") %>">
102
- <svg class="h-5 w-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
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"/>
104
- </svg>
105
- <svg class="h-5 w-5 block dark:hidden" fill="currentColor" viewBox="0 0 20 20">
106
- <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
107
- </svg>
108
- </button>
161
+ <!-- Dark mode toggle -->
162
+ <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") %>">
163
+ <svg class="h-5 w-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
164
+ <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"/>
165
+ </svg>
166
+ <svg class="h-5 w-5 block dark:hidden" fill="currentColor" viewBox="0 0 20 20">
167
+ <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
168
+ </svg>
169
+ </button>
170
+
171
+ <!-- Mobile menu button (hidden on large screens) -->
172
+ <button onclick="toggleMobileMenu()" class="lg:hidden rounded-md p-2 text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500" aria-label="<%= t("pgbus.layout.toggle_menu") %>">
173
+ <!-- Hamburger icon -->
174
+ <svg id="pgbus-menu-open" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
175
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>
176
+ </svg>
177
+ <!-- Close icon (hidden by default) -->
178
+ <svg id="pgbus-menu-close" class="hidden h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
179
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
180
+ </svg>
181
+ </button>
109
182
  </div>
110
183
  </div>
111
184
  </div>
185
+
186
+ <!-- Mobile menu (hidden by default, shown on small screens when toggled) -->
187
+ <div id="pgbus-mobile-menu" class="hidden lg:hidden border-t border-gray-800">
188
+ <div class="space-y-1 px-3 py-3">
189
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.dashboard"), pgbus.root_path %>
190
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.queues"), pgbus.queues_path %>
191
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.jobs"), pgbus.jobs_path %>
192
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.recurring"), pgbus.recurring_tasks_path %>
193
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.processes"), pgbus.processes_path %>
194
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.events"), pgbus.events_path %>
195
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.dlq"), pgbus.dead_letter_index_path %>
196
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.outbox"), pgbus.outbox_index_path %>
197
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.locks"), pgbus.locks_path %>
198
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.insights"), pgbus.insights_path %>
199
+ </div>
200
+ </div>
112
201
  </nav>
113
202
 
114
203
  <!-- Flash messages -->
@@ -2,7 +2,7 @@
2
2
  <div>
3
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
- <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
5
+ <table class="pgbus-table 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
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>
@@ -14,10 +14,10 @@
14
14
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
15
15
  <% @processes.each do |p| %>
16
16
  <tr>
17
- <td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white"><%= p[:kind] %></td>
18
- <td class="px-4 py-3 text-sm text-gray-500"><%= p[:hostname] %></td>
19
- <td class="px-4 py-3 text-sm text-gray-500"><%= p[:pid] %></td>
20
- <td class="px-4 py-3 text-sm"><%= pgbus_status_badge(p[:healthy]) %></td>
17
+ <td data-label="Kind" class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white"><%= p[:kind] %></td>
18
+ <td data-label="Host" class="px-4 py-3 text-sm text-gray-500"><%= p[:hostname] %></td>
19
+ <td data-label="PID" class="px-4 py-3 text-sm text-gray-500"><%= p[:pid] %></td>
20
+ <td data-label="Status" class="px-4 py-3 text-sm"><%= pgbus_status_badge(p[:healthy]) %></td>
21
21
  </tr>
22
22
  <% end %>
23
23
  <% if @processes.empty? %>
@@ -6,7 +6,7 @@
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
- <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
9
+ <table class="pgbus-table 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
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>
@@ -19,14 +19,14 @@
19
19
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
20
20
  <% @queues.each do |q| %>
21
21
  <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900">
22
- <td class="px-4 py-3 text-sm">
22
+ <td data-label="Queue" class="px-4 py-3 text-sm">
23
23
  <%= link_to q[:name], pgbus.queue_path(name: q[:name]), class: "font-medium text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
24
24
  <%= pgbus_queue_badge(q[:name]) %>
25
25
  </td>
26
- <td class="px-4 py-3 text-sm text-right text-gray-700"><%= pgbus_number(q[:queue_length]) %></td>
27
- <td class="px-4 py-3 text-sm text-right text-gray-700"><%= pgbus_number(q[:queue_visible_length]) %></td>
28
- <td class="px-4 py-3 text-sm text-right text-gray-500"><%= q[:oldest_msg_age_sec] || "—" %></td>
29
- <td class="px-4 py-3 text-sm text-right text-gray-500"><%= pgbus_number(q[:total_messages]) %></td>
26
+ <td data-label="Depth" class="px-4 py-3 text-sm text-right text-gray-700"><%= pgbus_number(q[:queue_length]) %></td>
27
+ <td data-label="Visible" class="px-4 py-3 text-sm text-right text-gray-700"><%= pgbus_number(q[:queue_visible_length]) %></td>
28
+ <td data-label="Oldest" class="px-4 py-3 text-sm text-right text-gray-500"><%= q[:oldest_msg_age_sec] || "—" %></td>
29
+ <td data-label="Total" class="px-4 py-3 text-sm text-right text-gray-500"><%= pgbus_number(q[:total_messages]) %></td>
30
30
  </tr>
31
31
  <% end %>
32
32
  <% if @queues.empty? %>
@@ -7,7 +7,7 @@
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
- <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
10
+ <table class="pgbus-table 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
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>
@@ -18,9 +18,9 @@
18
18
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
19
19
  <% @recent_failures.each do |f| %>
20
20
  <tr>
21
- <td class="px-4 py-3 text-sm text-gray-700"><%= f["queue_name"] %></td>
22
- <td class="px-4 py-3 text-sm text-red-600 truncate max-w-xs"><%= f["error_class"] %>: <%= truncate(f["error_message"].to_s, length: 60) %></td>
23
- <td class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(f["failed_at"]) %></td>
21
+ <td data-label="Queue" class="px-4 py-3 text-sm text-gray-700"><%= f["queue_name"] %></td>
22
+ <td data-label="Error" class="px-4 py-3 text-sm text-red-600 truncate max-w-xs"><%= f["error_class"] %>: <%= truncate(f["error_message"].to_s, length: 60) %></td>
23
+ <td data-label="Time" class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(f["failed_at"]) %></td>
24
24
  </tr>
25
25
  <% end %>
26
26
  <% if @recent_failures.empty? %>
@@ -1,6 +1,6 @@
1
1
  <turbo-frame id="dlq-messages" data-auto-refresh data-src="<%= pgbus.dead_letter_index_path(frame: 'list') %>">
2
2
  <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
3
- <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
3
+ <table class="pgbus-table 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
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>
@@ -4,7 +4,7 @@
4
4
  <div class="mb-8">
5
5
  <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3"><%= t("pgbus.events.index.subscribers_title") %></h2>
6
6
  <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
7
- <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
7
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
8
8
  <thead class="bg-gray-50 dark:bg-gray-900">
9
9
  <tr>
10
10
  <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.subscribers_headers.pattern") %></th>
@@ -15,9 +15,9 @@
15
15
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
16
16
  <% @subscribers.each do |s| %>
17
17
  <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900">
18
- <td class="px-4 py-3 text-sm font-mono text-indigo-600"><%= s[:pattern] %></td>
19
- <td class="px-4 py-3 text-sm text-gray-900 dark:text-white"><%= s[:handler_class] %></td>
20
- <td class="px-4 py-3 text-sm text-gray-500"><%= s[:queue_name] %></td>
18
+ <td data-label="Pattern" class="px-4 py-3 text-sm font-mono text-indigo-600"><%= s[:pattern] %></td>
19
+ <td data-label="Handler" class="px-4 py-3 text-sm text-gray-900 dark:text-white"><%= s[:handler_class] %></td>
20
+ <td data-label="Queue" class="px-4 py-3 text-sm text-gray-500"><%= s[:queue_name] %></td>
21
21
  </tr>
22
22
  <% end %>
23
23
  <% if @subscribers.empty? %>
@@ -32,7 +32,7 @@
32
32
  <div>
33
33
  <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3"><%= t("pgbus.events.index.processed_title") %></h2>
34
34
  <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
35
- <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
35
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
36
36
  <thead class="bg-gray-50 dark:bg-gray-900">
37
37
  <tr>
38
38
  <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.processed_headers.event_id") %></th>
@@ -43,9 +43,9 @@
43
43
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
44
44
  <% @events.each do |e| %>
45
45
  <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900">
46
- <td class="px-4 py-3 text-sm font-mono text-gray-900 dark:text-white"><%= e["event_id"] %></td>
47
- <td class="px-4 py-3 text-sm text-gray-700"><%= e["handler_class"] %></td>
48
- <td class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(e["processed_at"]) %></td>
46
+ <td data-label="Event ID" class="px-4 py-3 text-sm font-mono text-gray-900 dark:text-white"><%= e["event_id"] %></td>
47
+ <td data-label="Handler" class="px-4 py-3 text-sm text-gray-700"><%= e["handler_class"] %></td>
48
+ <td data-label="Processed At" class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(e["processed_at"]) %></td>
49
49
  </tr>
50
50
  <% end %>
51
51
  <% if @events.empty? %>