pgbus 0.2.5 → 0.2.7

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +33 -3
  3. data/app/controllers/pgbus/application_controller.rb +8 -2
  4. data/app/controllers/pgbus/queues_controller.rb +6 -0
  5. data/app/helpers/pgbus/application_helper.rb +10 -0
  6. data/app/views/layouts/pgbus/application.html.erb +212 -25
  7. data/app/views/pgbus/dashboard/_processes_table.html.erb +5 -5
  8. data/app/views/pgbus/dashboard/_queues_table.html.erb +6 -6
  9. data/app/views/pgbus/dashboard/_recent_failures.html.erb +4 -4
  10. data/app/views/pgbus/dead_letter/_messages_table.html.erb +1 -1
  11. data/app/views/pgbus/events/index.html.erb +8 -8
  12. data/app/views/pgbus/insights/show.html.erb +31 -29
  13. data/app/views/pgbus/jobs/_enqueued_table.html.erb +1 -1
  14. data/app/views/pgbus/jobs/_failed_table.html.erb +7 -7
  15. data/app/views/pgbus/locks/index.html.erb +7 -7
  16. data/app/views/pgbus/outbox/index.html.erb +7 -7
  17. data/app/views/pgbus/processes/_processes_table.html.erb +7 -7
  18. data/app/views/pgbus/queues/_queues_list.html.erb +12 -8
  19. data/app/views/pgbus/queues/show.html.erb +20 -1
  20. data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +8 -8
  21. data/config/locales/da.yml +18 -0
  22. data/config/locales/de.yml +18 -0
  23. data/config/locales/en.yml +18 -0
  24. data/config/locales/es.yml +18 -0
  25. data/config/locales/fi.yml +18 -0
  26. data/config/locales/fr.yml +18 -0
  27. data/config/locales/it.yml +18 -0
  28. data/config/locales/ja.yml +18 -0
  29. data/config/locales/nb.yml +18 -0
  30. data/config/locales/nl.yml +18 -0
  31. data/config/locales/pt.yml +18 -0
  32. data/config/locales/sv.yml +18 -0
  33. data/config/routes.rb +1 -1
  34. data/lib/pgbus/client.rb +10 -2
  35. data/lib/pgbus/configuration.rb +3 -1
  36. data/lib/pgbus/version.rb +1 -1
  37. data/lib/pgbus/web/data_source.rb +5 -1
  38. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cfd2ea5ccec3e3715dbcf2dd409a652527cf1d9e18c60fe4d5797fd5cf5be6ec
4
- data.tar.gz: 6983d4977aeb792ba0857884752a8b662a15164cde8cd6ee3bb5406d95d7f225
3
+ metadata.gz: 3e16e68e83d8c1963b60a9c2137d3fbd737512b7e0fed1566b5f1320ef33c470
4
+ data.tar.gz: 03d54ebcc0357a61b71bbb3f11fcafa662114245c3fedc33b58a83d40718ffc6
5
5
  SHA512:
6
- metadata.gz: c6f3e19f1e3fe2d5eae02fe52dcd2878f5bbcb13cd7387a41b3902ad91cb8c20ce2d1e3c606488bdce1c901126d1192f7a34747e8746c397d016d0c2541a8828
7
- data.tar.gz: da5ed5a1a2012d757547e3e2f1f48586a84e7d096df91a793d9ba18f76da881e52dab650d4fb3c605c4da6de06b1e8b58b0d11c99bd90e30ef64b3c368c44093
6
+ metadata.gz: 5579b34e30ee64cfff273975174d4023b921f8f64a98c1baa09df0673ccb5ba6a5db73566452a35feead0bb4584d8a969b5513efb34ada4614e2945fbc85790d
7
+ data.tar.gz: ea716b54c14023d91d440fb37db87358330ced5671365fd808e0e092a86963d5c2587519d7493e185c51c106a070eb5e49e36d79d441af37c5f2098528aea26e
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 |
@@ -687,7 +707,7 @@ pgbus help # Show help
687
707
  The dashboard is a mountable Rails engine at `/pgbus` with:
688
708
 
689
709
  - **Overview** -- queue depths, enqueued count, active processes, failure count, throughput rate
690
- - **Queues** -- per-queue metrics, purge/pause/resume actions
710
+ - **Queues** -- per-queue metrics, purge/pause/resume/delete actions
691
711
  - **Jobs** -- enqueued and failed jobs, retry/discard actions
692
712
  - **Dead letter** -- DLQ messages with retry/discard, bulk actions
693
713
  - **Processes** -- active workers/dispatcher/consumers with heartbeat status
@@ -696,7 +716,17 @@ The dashboard is a mountable Rails engine at `/pgbus` with:
696
716
  - **Locks** -- active job uniqueness locks with state (queued/executing), owner PID@hostname, age
697
717
  - **Insights** -- throughput chart (jobs/min), status distribution donut, slowest job classes table
698
718
 
699
- All tables use Turbo Frames for periodic auto-refresh without page reloads.
719
+ All tables use Turbo Frames for periodic auto-refresh without page reloads. Destructive actions use styled confirmation dialogs (not browser `confirm()`), and flash messages appear as auto-dismissing toast notifications.
720
+
721
+ ### Queue management
722
+
723
+ The queues page lets you manage PGMQ queues directly:
724
+
725
+ - **Purge** -- removes all messages from the queue (the queue itself remains)
726
+ - **Delete** -- permanently drops the queue from PGMQ (removes the queue table and metadata)
727
+ - **Pause / Resume** -- pauses or resumes job processing for a queue
728
+
729
+ All destructive actions require confirmation. Pause/resume and delete are available on both the queue index and detail pages.
700
730
 
701
731
  ### Dark mode
702
732
 
@@ -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
@@ -10,6 +10,7 @@ module Pgbus
10
10
  @queue = data_source.queue_detail(params[:name])
11
11
  redirect_to queues_path, alert: "Queue not found." and return unless @queue
12
12
 
13
+ @paused = data_source.queue_paused?(params[:name])
13
14
  @messages = data_source.jobs(queue_name: params[:name], page: page_param, per_page: per_page)
14
15
  end
15
16
 
@@ -18,6 +19,11 @@ module Pgbus
18
19
  redirect_to queue_path(name: params[:name]), notice: "Queue purged."
19
20
  end
20
21
 
22
+ def destroy
23
+ data_source.drop_queue(params[:name])
24
+ redirect_to queues_path, notice: t("pgbus.queues.destroy.success", name: params[:name])
25
+ end
26
+
21
27
  def pause
22
28
  data_source.pause_queue(params[:name], reason: params[:reason])
23
29
  redirect_to queue_path(name: params[:name]), notice: "Queue paused."
@@ -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",
@@ -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');
@@ -35,6 +81,71 @@
35
81
  <script type="module">
36
82
  import * as Turbo from "https://esm.sh/@hotwired/turbo@8";
37
83
 
84
+ // -- Custom confirm dialog (replaces browser confirm) --
85
+ Turbo.config.forms.confirm = (message, element) => {
86
+ const dialog = document.getElementById("pgbus-confirm-dialog");
87
+ const messageEl = document.getElementById("pgbus-confirm-message");
88
+ const titleEl = document.getElementById("pgbus-confirm-title");
89
+ const confirmBtn = document.getElementById("pgbus-confirm-btn");
90
+ const iconEl = document.getElementById("pgbus-confirm-icon");
91
+
92
+ // Detect action type from the element
93
+ const turboMethod = element.getAttribute("data-turbo-method");
94
+ const isDelete = turboMethod === "delete";
95
+
96
+ // Set title based on action
97
+ titleEl.textContent = isDelete ? "<%= t("pgbus.dialogs.delete_title", default: "Delete") %>" : "<%= t("pgbus.dialogs.confirm_title", default: "Are you sure?") %>";
98
+ messageEl.textContent = message;
99
+
100
+ // Style confirm button based on action severity
101
+ confirmBtn.className = "rounded-md px-4 py-2 text-sm font-medium text-white focus:outline-none focus:ring-2";
102
+ if (isDelete) {
103
+ confirmBtn.classList.add("bg-red-600", "hover:bg-red-500", "focus:ring-red-500");
104
+ confirmBtn.textContent = "<%= t("pgbus.dialogs.delete", default: "Delete") %>";
105
+ iconEl.className = "flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-red-100 dark:bg-red-900/30";
106
+ } else {
107
+ confirmBtn.classList.add("bg-yellow-500", "hover:bg-yellow-400", "focus:ring-yellow-500");
108
+ confirmBtn.textContent = "<%= t("pgbus.dialogs.confirm", default: "Confirm") %>";
109
+ iconEl.className = "flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-yellow-100 dark:bg-yellow-900/30";
110
+ }
111
+
112
+ dialog.showModal();
113
+
114
+ return new Promise((resolve) => {
115
+ dialog.addEventListener("close", () => {
116
+ resolve(dialog.returnValue === "confirm");
117
+ }, { once: true });
118
+ });
119
+ };
120
+
121
+ // -- Toast notifications --
122
+ function showToast(message, type = "success") {
123
+ const container = document.getElementById("pgbus-toast-container");
124
+ const toast = document.createElement("div");
125
+
126
+ const colors = {
127
+ success: "bg-green-50 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-200 dark:border-green-800",
128
+ error: "bg-red-50 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-800",
129
+ info: "bg-blue-50 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 border-blue-200 dark:border-blue-800",
130
+ };
131
+
132
+ toast.className = `rounded-md border p-3 text-sm shadow-lg transition-all duration-300 ${colors[type] || colors.info}`;
133
+ toast.textContent = message;
134
+ container.appendChild(toast);
135
+
136
+ setTimeout(() => {
137
+ toast.style.opacity = "0";
138
+ toast.style.transform = "translateX(100%)";
139
+ setTimeout(() => toast.remove(), 300);
140
+ }, 5000);
141
+ }
142
+
143
+ // Render flash toasts from <template> tags
144
+ document.querySelectorAll("template[data-pgbus-toast]").forEach(tpl => {
145
+ showToast(tpl.content.textContent.trim(), tpl.dataset.pgbusToast);
146
+ tpl.remove();
147
+ });
148
+
38
149
  <% if Pgbus.configuration.web_live_updates %>
39
150
  const interval = <%= Pgbus.configuration.web_refresh_interval %>;
40
151
  if (interval > 0) {
@@ -43,8 +154,10 @@
43
154
  if (document.hidden) return;
44
155
  document.querySelectorAll("turbo-frame[data-auto-refresh]")
45
156
  .forEach(frame => {
46
- if (!frame.src && frame.dataset.src) frame.src = frame.dataset.src;
47
- if (frame.src) frame.reload();
157
+ try {
158
+ if (!frame.src && frame.dataset.src) frame.src = frame.dataset.src;
159
+ if (frame.src) frame.reload();
160
+ } catch (_) { /* Turbo may abort in-flight fetches during navigation */ }
48
161
  });
49
162
  }
50
163
  function start() { timer = setInterval(refreshFrames, interval); }
@@ -61,13 +174,24 @@
61
174
  <nav class="bg-gray-900 dark:bg-gray-950 border-b border-gray-800">
62
175
  <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
63
176
  <div class="flex h-14 items-center justify-between">
177
+ <!-- Left: brand + desktop nav -->
64
178
  <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 %>
179
+ <div class="flex items-center space-x-3">
180
+ <% if Pgbus.configuration.return_to_app_url %>
181
+ <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") %>">
182
+ <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
183
+ <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18"/>
184
+ </svg>
185
+ </a>
186
+ <% end %>
187
+ <%= link_to pgbus.root_path, class: "flex items-center space-x-2" do %>
188
+ <span class="text-lg font-bold text-white"><%= t("pgbus.layout.brand") %></span>
189
+ <span class="rounded bg-gray-700 px-1.5 py-0.5 text-xs text-gray-300"><%= Pgbus::VERSION %></span>
190
+ <% end %>
191
+ </div>
69
192
 
70
- <div class="flex space-x-1">
193
+ <!-- Desktop nav links (hidden on small screens) -->
194
+ <div class="hidden lg:flex space-x-1">
71
195
  <%= pgbus_nav_link t("pgbus.layout.nav.dashboard"), pgbus.root_path %>
72
196
  <%= pgbus_nav_link t("pgbus.layout.nav.queues"), pgbus.queues_path %>
73
197
  <%= pgbus_nav_link t("pgbus.layout.nav.jobs"), pgbus.jobs_path %>
@@ -81,6 +205,7 @@
81
205
  </div>
82
206
  </div>
83
207
 
208
+ <!-- Right: locale, dark mode, hamburger -->
84
209
  <div class="flex items-center space-x-2">
85
210
  <!-- Locale switcher -->
86
211
  <div class="relative" id="pgbus-locale-switcher">
@@ -98,34 +223,96 @@
98
223
  </div>
99
224
  </div>
100
225
 
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>
226
+ <!-- Dark mode toggle -->
227
+ <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") %>">
228
+ <svg class="h-5 w-5 hidden dark:block" fill="currentColor" viewBox="0 0 20 20">
229
+ <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"/>
230
+ </svg>
231
+ <svg class="h-5 w-5 block dark:hidden" fill="currentColor" viewBox="0 0 20 20">
232
+ <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/>
233
+ </svg>
234
+ </button>
235
+
236
+ <!-- Mobile menu button (hidden on large screens) -->
237
+ <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") %>">
238
+ <!-- Hamburger icon -->
239
+ <svg id="pgbus-menu-open" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
240
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"/>
241
+ </svg>
242
+ <!-- Close icon (hidden by default) -->
243
+ <svg id="pgbus-menu-close" class="hidden h-6 w-6" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
244
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
245
+ </svg>
246
+ </button>
109
247
  </div>
110
248
  </div>
111
249
  </div>
112
- </nav>
113
250
 
114
- <!-- Flash messages -->
115
- <% if notice %>
116
- <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 mt-4">
117
- <div class="rounded-md bg-green-50 dark:bg-green-900/30 p-3">
118
- <p class="text-sm text-green-800 dark:text-green-300"><%= notice %></p>
251
+ <!-- Mobile menu (hidden by default, shown on small screens when toggled) -->
252
+ <div id="pgbus-mobile-menu" class="hidden lg:hidden border-t border-gray-800">
253
+ <div class="space-y-1 px-3 py-3">
254
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.dashboard"), pgbus.root_path %>
255
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.queues"), pgbus.queues_path %>
256
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.jobs"), pgbus.jobs_path %>
257
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.recurring"), pgbus.recurring_tasks_path %>
258
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.processes"), pgbus.processes_path %>
259
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.events"), pgbus.events_path %>
260
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.dlq"), pgbus.dead_letter_index_path %>
261
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.outbox"), pgbus.outbox_index_path %>
262
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.locks"), pgbus.locks_path %>
263
+ <%= pgbus_mobile_nav_link t("pgbus.layout.nav.insights"), pgbus.insights_path %>
119
264
  </div>
120
265
  </div>
266
+ </nav>
267
+
268
+ <!-- Toast container (fixed top-right) -->
269
+ <div id="pgbus-toast-container" class="fixed top-4 right-4 z-[100] flex flex-col space-y-2 max-w-sm"></div>
270
+
271
+ <!-- Flash messages rendered as toasts -->
272
+ <% if notice %>
273
+ <template data-pgbus-toast="success"><%= notice %></template>
121
274
  <% end %>
122
275
  <% if alert %>
123
- <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 mt-4">
124
- <div class="rounded-md bg-red-50 dark:bg-red-900/30 p-3">
125
- <p class="text-sm text-red-800 dark:text-red-300"><%= alert %></p>
276
+ <template data-pgbus-toast="error"><%= alert %></template>
277
+ <% end %>
278
+
279
+ <!-- Confirm dialog -->
280
+ <dialog id="pgbus-confirm-dialog" class="rounded-lg shadow-xl bg-white dark:bg-gray-800 p-0 backdrop:bg-gray-900/50 max-w-md w-full">
281
+ <div class="p-6">
282
+ <div class="flex items-start space-x-4">
283
+ <div id="pgbus-confirm-icon" class="flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-red-100 dark:bg-red-900/30">
284
+ <svg class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
285
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"/>
286
+ </svg>
287
+ </div>
288
+ <div class="flex-1">
289
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-white" id="pgbus-confirm-title"></h3>
290
+ <p class="mt-2 text-sm text-gray-600 dark:text-gray-300" id="pgbus-confirm-message"></p>
291
+ </div>
126
292
  </div>
127
293
  </div>
128
- <% end %>
294
+ <div class="flex justify-end space-x-3 px-6 py-4 bg-gray-50 dark:bg-gray-900/50 rounded-b-lg">
295
+ <button value="cancel" class="rounded-md px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-indigo-500"><%= t("pgbus.dialogs.cancel", default: "Cancel") %></button>
296
+ <button value="confirm" id="pgbus-confirm-btn" class="rounded-md px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-500 focus:outline-none focus:ring-2 focus:ring-red-500"><%= t("pgbus.dialogs.confirm", default: "Confirm") %></button>
297
+ </div>
298
+ </dialog>
299
+
300
+ <!-- Alert dialog -->
301
+ <dialog id="pgbus-alert-dialog" class="rounded-lg shadow-xl bg-white dark:bg-gray-800 p-0 backdrop:bg-gray-900/50 max-w-md w-full">
302
+ <div class="p-6">
303
+ <div class="flex items-start space-x-4">
304
+ <div class="flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-blue-100 dark:bg-blue-900/30">
305
+ <svg class="h-6 w-6 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
306
+ <path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"/>
307
+ </svg>
308
+ </div>
309
+ <p class="text-sm text-gray-700 dark:text-gray-300" id="pgbus-alert-message"></p>
310
+ </div>
311
+ </div>
312
+ <div class="flex justify-end px-6 py-4 bg-gray-50 dark:bg-gray-900/50 rounded-b-lg">
313
+ <button value="ok" class="rounded-md px-4 py-2 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"><%= t("pgbus.dialogs.ok", default: "OK") %></button>
314
+ </div>
315
+ </dialog>
129
316
 
130
317
  <!-- Content -->
131
318
  <main class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
@@ -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? %>