pgbus 0.17.1 → 0.17.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 (156) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -0
  3. data/README.md +83 -13
  4. data/Rakefile +26 -0
  5. data/app/assets/javascripts/pgbus/stream_source_element.js +34 -2
  6. data/app/assets/javascripts/pgbus/stream_source_element.min.js +6 -0
  7. data/app/assets/javascripts/pgbus/stream_source_element.min.js.map +1 -0
  8. data/app/controllers/concerns/pgbus/job_listing.rb +37 -0
  9. data/app/controllers/concerns/pgbus/process_listing.rb +17 -0
  10. data/app/controllers/pgbus/application_controller.rb +5 -2
  11. data/app/controllers/pgbus/batches_controller.rb +4 -1
  12. data/app/controllers/pgbus/dashboard_controller.rb +20 -6
  13. data/app/controllers/pgbus/dead_letter_controller.rb +27 -3
  14. data/app/controllers/pgbus/events_controller.rb +52 -3
  15. data/app/controllers/pgbus/frontends_controller.rb +25 -11
  16. data/app/controllers/pgbus/insights_controller.rb +19 -1
  17. data/app/controllers/pgbus/jobs_controller.rb +45 -26
  18. data/app/controllers/pgbus/locks_controller.rb +7 -2
  19. data/app/controllers/pgbus/outbox_controller.rb +4 -1
  20. data/app/controllers/pgbus/processes_controller.rb +3 -1
  21. data/app/controllers/pgbus/queues_controller.rb +16 -4
  22. data/app/controllers/pgbus/recurring_tasks_controller.rb +5 -7
  23. data/app/frontend/pgbus/application.min.js +2 -0
  24. data/app/frontend/pgbus/application.min.js.map +1 -0
  25. data/app/frontend/pgbus/modules/charts.js +137 -77
  26. data/app/frontend/pgbus/modules/charts.min.js +2 -0
  27. data/app/frontend/pgbus/modules/charts.min.js.map +1 -0
  28. data/app/frontend/pgbus/style.css +1 -1
  29. data/app/frontend/pgbus/tailwind.css +18 -0
  30. data/app/frontend/pgbus/vendor/turbo.min.js +54 -0
  31. data/app/frontend/pgbus/vendor/turbo.min.js.map +1 -0
  32. data/app/helpers/pgbus/application_helper.rb +155 -113
  33. data/app/helpers/pgbus/button_helper.rb +88 -0
  34. data/app/helpers/pgbus/dashboard_helper.rb +157 -0
  35. data/app/helpers/pgbus/events_helper.rb +40 -0
  36. data/app/helpers/pgbus/insights_helper.rb +39 -0
  37. data/app/helpers/pgbus/processes_helper.rb +79 -0
  38. data/app/views/layouts/pgbus/application.html.erb +0 -1
  39. data/app/views/pgbus/batches/_batches_table.html.erb +16 -13
  40. data/app/views/pgbus/batches/show.html.erb +3 -4
  41. data/app/views/pgbus/dashboard/_processes_table.html.erb +11 -11
  42. data/app/views/pgbus/dashboard/_queue_health.html.erb +42 -51
  43. data/app/views/pgbus/dashboard/_queues_table.html.erb +18 -16
  44. data/app/views/pgbus/dashboard/_recent_failures.html.erb +16 -12
  45. data/app/views/pgbus/dashboard/_stats_cards.html.erb +70 -36
  46. data/app/views/pgbus/dashboard/show.html.erb +1 -1
  47. data/app/views/pgbus/dead_letter/_error.html.erb +13 -0
  48. data/app/views/pgbus/dead_letter/_messages_table.html.erb +37 -96
  49. data/app/views/pgbus/dead_letter/_row.html.erb +75 -0
  50. data/app/views/pgbus/dead_letter/index.html.erb +6 -9
  51. data/app/views/pgbus/dead_letter/show.html.erb +37 -21
  52. data/app/views/pgbus/events/_list.html.erb +55 -0
  53. data/app/views/pgbus/events/_processed_table.html.erb +45 -0
  54. data/app/views/pgbus/events/_replay.html.erb +10 -0
  55. data/app/views/pgbus/events/_row.html.erb +162 -0
  56. data/app/views/pgbus/events/_tabs.html.erb +13 -0
  57. data/app/views/pgbus/events/index.html.erb +27 -48
  58. data/app/views/pgbus/events/show.html.erb +25 -7
  59. data/app/views/pgbus/insights/_chart_card.html.erb +21 -0
  60. data/app/views/pgbus/insights/_latency_table.html.erb +20 -0
  61. data/app/views/pgbus/insights/_status_table.html.erb +19 -0
  62. data/app/views/pgbus/insights/_throughput_table.html.erb +18 -0
  63. data/app/views/pgbus/insights/show.html.erb +83 -100
  64. data/app/views/pgbus/jobs/_list.html.erb +64 -0
  65. data/app/views/pgbus/jobs/_row.html.erb +107 -0
  66. data/app/views/pgbus/jobs/_tabs.html.erb +14 -0
  67. data/app/views/pgbus/jobs/index.html.erb +10 -10
  68. data/app/views/pgbus/jobs/show.html.erb +18 -20
  69. data/app/views/pgbus/locks/_concurrency.html.erb +25 -23
  70. data/app/views/pgbus/locks/_uniqueness.html.erb +20 -22
  71. data/app/views/pgbus/outbox/index.html.erb +30 -27
  72. data/app/views/pgbus/processes/_processes_table.html.erb +6 -25
  73. data/app/views/pgbus/processes/_row.html.erb +48 -0
  74. data/app/views/pgbus/processes/_status.html.erb +4 -0
  75. data/app/views/pgbus/queues/_queues_list.html.erb +28 -32
  76. data/app/views/pgbus/queues/show.html.erb +69 -118
  77. data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +75 -76
  78. data/app/views/pgbus/recurring_tasks/index.html.erb +1 -1
  79. data/app/views/pgbus/recurring_tasks/show.html.erb +45 -51
  80. data/app/views/pgbus/shared/_empty_row.html.erb +2 -0
  81. data/app/views/pgbus/shared/_pager.html.erb +34 -0
  82. data/config/i18n-tasks.yml +11 -2
  83. data/config/locales/da.yml +405 -113
  84. data/config/locales/de.yml +405 -113
  85. data/config/locales/en.yml +405 -113
  86. data/config/locales/es.yml +404 -112
  87. data/config/locales/fi.yml +404 -112
  88. data/config/locales/fr.yml +406 -114
  89. data/config/locales/it.yml +405 -113
  90. data/config/locales/ja.yml +405 -113
  91. data/config/locales/nb.yml +405 -113
  92. data/config/locales/nl.yml +405 -113
  93. data/config/locales/pt.yml +406 -114
  94. data/config/locales/sv.yml +404 -112
  95. data/config/routes.rb +1 -1
  96. data/lib/generators/pgbus/add_presence_generator.rb +2 -1
  97. data/lib/generators/pgbus/add_presence_seats_generator.rb +45 -0
  98. data/lib/generators/pgbus/templates/add_presence.rb.erb +32 -7
  99. data/lib/generators/pgbus/templates/add_presence_seats.rb.erb +38 -0
  100. data/lib/generators/pgbus/templates/initializer.rb.erb +4 -0
  101. data/lib/pgbus/active_job/executor.rb +16 -6
  102. data/lib/pgbus/cli/dlq.rb +32 -5
  103. data/lib/pgbus/client.rb +4 -2
  104. data/lib/pgbus/configuration/presence_settings.rb +103 -0
  105. data/lib/pgbus/configuration.rb +51 -2
  106. data/lib/pgbus/dead_letter_header.rb +129 -0
  107. data/lib/pgbus/doctor.rb +8 -4
  108. data/lib/pgbus/engine.rb +13 -7
  109. data/lib/pgbus/event_bus/handler.rb +18 -11
  110. data/lib/pgbus/event_bus/registry.rb +6 -0
  111. data/lib/pgbus/failed_event_recorder.rb +34 -0
  112. data/lib/pgbus/generators/migration_detector.rb +9 -1
  113. data/lib/pgbus/instrumentation.rb +2 -1
  114. data/lib/pgbus/mcp/base_tool.rb +25 -0
  115. data/lib/pgbus/mcp/tools/concurrency_tool.rb +23 -4
  116. data/lib/pgbus/mcp/tools/dlq_detail_tool.rb +4 -2
  117. data/lib/pgbus/mcp/tools/dlq_tool.rb +10 -4
  118. data/lib/pgbus/mcp/tools/locks_tool.rb +23 -5
  119. data/lib/pgbus/mcp/tools/recurring_tool.rb +33 -4
  120. data/lib/pgbus/outbox/poller.rb +1 -1
  121. data/lib/pgbus/process/consumer.rb +53 -3
  122. data/lib/pgbus/process/dispatcher.rb +1 -1
  123. data/lib/pgbus/process/supervisor.rb +32 -11
  124. data/lib/pgbus/process/worker.rb +45 -8
  125. data/lib/pgbus/recurring/scheduler.rb +2 -1
  126. data/lib/pgbus/streams/presence.rb +317 -81
  127. data/lib/pgbus/version.rb +1 -1
  128. data/lib/pgbus/web/data_source/dashboard_signals.rb +170 -0
  129. data/lib/pgbus/web/data_source/dead_letter.rb +142 -0
  130. data/lib/pgbus/web/data_source/event_list.rb +161 -0
  131. data/lib/pgbus/web/data_source/job_list.rb +323 -0
  132. data/lib/pgbus/web/data_source/list_counts.rb +80 -0
  133. data/lib/pgbus/web/data_source/process_list.rb +69 -0
  134. data/lib/pgbus/web/data_source/queue_summary.rb +122 -0
  135. data/lib/pgbus/web/data_source.rb +132 -254
  136. data/lib/pgbus/web/dead_letter_reason.rb +81 -0
  137. data/lib/pgbus/web/event_state.rb +71 -0
  138. data/lib/pgbus/web/health_app.rb +7 -3
  139. data/lib/pgbus/web/health_verdict.rb +78 -0
  140. data/lib/pgbus/web/insights_charts.rb +84 -0
  141. data/lib/pgbus/web/job_state.rb +111 -0
  142. data/lib/pgbus/web/process_activity.rb +97 -0
  143. data/lib/pgbus/web/process_state.rb +187 -0
  144. data/lib/pgbus/web/queue_summary.rb +104 -0
  145. data/lib/pgbus/web/stream_app.rb +18 -6
  146. data/lib/pgbus/web/streamer/connection.rb +7 -1
  147. data/lib/pgbus/web/streamer/falcon_connection.rb +7 -1
  148. data/lib/pgbus/web/streamer/heartbeat.rb +9 -3
  149. data/lib/pgbus/web/streamer/instance.rb +2 -1
  150. data/lib/pgbus/web/streamer/presence_tracker.rb +155 -0
  151. data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +8 -37
  152. data/lib/pgbus/web/time_format.rb +128 -0
  153. metadata +55 -4
  154. data/app/views/pgbus/events/_pending_table.html.erb +0 -169
  155. data/app/views/pgbus/jobs/_enqueued_table.html.erb +0 -118
  156. data/app/views/pgbus/jobs/_failed_table.html.erb +0 -76
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgbus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.1
4
+ version: 0.17.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -123,6 +123,10 @@ files:
123
123
  - README.md
124
124
  - Rakefile
125
125
  - app/assets/javascripts/pgbus/stream_source_element.js
126
+ - app/assets/javascripts/pgbus/stream_source_element.min.js
127
+ - app/assets/javascripts/pgbus/stream_source_element.min.js.map
128
+ - app/controllers/concerns/pgbus/job_listing.rb
129
+ - app/controllers/concerns/pgbus/process_listing.rb
126
130
  - app/controllers/pgbus/api/insights_controller.rb
127
131
  - app/controllers/pgbus/api/metrics_controller.rb
128
132
  - app/controllers/pgbus/api/stats_controller.rb
@@ -141,12 +145,23 @@ files:
141
145
  - app/controllers/pgbus/queues_controller.rb
142
146
  - app/controllers/pgbus/recurring_tasks_controller.rb
143
147
  - app/frontend/pgbus/application.js
148
+ - app/frontend/pgbus/application.min.js
149
+ - app/frontend/pgbus/application.min.js.map
144
150
  - app/frontend/pgbus/modules/charts.js
151
+ - app/frontend/pgbus/modules/charts.min.js
152
+ - app/frontend/pgbus/modules/charts.min.js.map
145
153
  - app/frontend/pgbus/style.css
146
154
  - app/frontend/pgbus/tailwind.css
147
155
  - app/frontend/pgbus/vendor/apexcharts.js
148
156
  - app/frontend/pgbus/vendor/turbo.js
157
+ - app/frontend/pgbus/vendor/turbo.min.js
158
+ - app/frontend/pgbus/vendor/turbo.min.js.map
149
159
  - app/helpers/pgbus/application_helper.rb
160
+ - app/helpers/pgbus/button_helper.rb
161
+ - app/helpers/pgbus/dashboard_helper.rb
162
+ - app/helpers/pgbus/events_helper.rb
163
+ - app/helpers/pgbus/insights_helper.rb
164
+ - app/helpers/pgbus/processes_helper.rb
150
165
  - app/helpers/pgbus/streams_helper.rb
151
166
  - app/models/pgbus/application_record.rb
152
167
  - app/models/pgbus/batch_entry.rb
@@ -174,15 +189,26 @@ files:
174
189
  - app/views/pgbus/dashboard/_recent_failures.html.erb
175
190
  - app/views/pgbus/dashboard/_stats_cards.html.erb
176
191
  - app/views/pgbus/dashboard/show.html.erb
192
+ - app/views/pgbus/dead_letter/_error.html.erb
177
193
  - app/views/pgbus/dead_letter/_messages_table.html.erb
194
+ - app/views/pgbus/dead_letter/_row.html.erb
178
195
  - app/views/pgbus/dead_letter/index.html.erb
179
196
  - app/views/pgbus/dead_letter/show.html.erb
180
- - app/views/pgbus/events/_pending_table.html.erb
197
+ - app/views/pgbus/events/_list.html.erb
198
+ - app/views/pgbus/events/_processed_table.html.erb
199
+ - app/views/pgbus/events/_replay.html.erb
200
+ - app/views/pgbus/events/_row.html.erb
201
+ - app/views/pgbus/events/_tabs.html.erb
181
202
  - app/views/pgbus/events/index.html.erb
182
203
  - app/views/pgbus/events/show.html.erb
204
+ - app/views/pgbus/insights/_chart_card.html.erb
205
+ - app/views/pgbus/insights/_latency_table.html.erb
206
+ - app/views/pgbus/insights/_status_table.html.erb
207
+ - app/views/pgbus/insights/_throughput_table.html.erb
183
208
  - app/views/pgbus/insights/show.html.erb
184
- - app/views/pgbus/jobs/_enqueued_table.html.erb
185
- - app/views/pgbus/jobs/_failed_table.html.erb
209
+ - app/views/pgbus/jobs/_list.html.erb
210
+ - app/views/pgbus/jobs/_row.html.erb
211
+ - app/views/pgbus/jobs/_tabs.html.erb
186
212
  - app/views/pgbus/jobs/index.html.erb
187
213
  - app/views/pgbus/jobs/show.html.erb
188
214
  - app/views/pgbus/locks/_concurrency.html.erb
@@ -190,6 +216,8 @@ files:
190
216
  - app/views/pgbus/locks/index.html.erb
191
217
  - app/views/pgbus/outbox/index.html.erb
192
218
  - app/views/pgbus/processes/_processes_table.html.erb
219
+ - app/views/pgbus/processes/_row.html.erb
220
+ - app/views/pgbus/processes/_status.html.erb
193
221
  - app/views/pgbus/processes/index.html.erb
194
222
  - app/views/pgbus/queues/_queues_list.html.erb
195
223
  - app/views/pgbus/queues/index.html.erb
@@ -197,6 +225,8 @@ files:
197
225
  - app/views/pgbus/recurring_tasks/_tasks_table.html.erb
198
226
  - app/views/pgbus/recurring_tasks/index.html.erb
199
227
  - app/views/pgbus/recurring_tasks/show.html.erb
228
+ - app/views/pgbus/shared/_empty_row.html.erb
229
+ - app/views/pgbus/shared/_pager.html.erb
200
230
  - config/i18n-tasks.yml
201
231
  - config/locales/da.yml
202
232
  - config/locales/de.yml
@@ -222,6 +252,7 @@ files:
222
252
  - lib/generators/pgbus/add_job_stats_queue_index_generator.rb
223
253
  - lib/generators/pgbus/add_outbox_generator.rb
224
254
  - lib/generators/pgbus/add_presence_generator.rb
255
+ - lib/generators/pgbus/add_presence_seats_generator.rb
225
256
  - lib/generators/pgbus/add_processed_event_completion_generator.rb
226
257
  - lib/generators/pgbus/add_queue_states_generator.rb
227
258
  - lib/generators/pgbus/add_recurring_generator.rb
@@ -239,6 +270,7 @@ files:
239
270
  - lib/generators/pgbus/templates/add_job_stats_queue_index.rb.erb
240
271
  - lib/generators/pgbus/templates/add_outbox.rb.erb
241
272
  - lib/generators/pgbus/templates/add_presence.rb.erb
273
+ - lib/generators/pgbus/templates/add_presence_seats.rb.erb
242
274
  - lib/generators/pgbus/templates/add_processed_event_completion.rb.erb
243
275
  - lib/generators/pgbus/templates/add_queue_states.rb.erb
244
276
  - lib/generators/pgbus/templates/add_recurring_tables.rb.erb
@@ -283,8 +315,10 @@ files:
283
315
  - lib/pgbus/concurrency/semaphore.rb
284
316
  - lib/pgbus/configuration.rb
285
317
  - lib/pgbus/configuration/capsule_dsl.rb
318
+ - lib/pgbus/configuration/presence_settings.rb
286
319
  - lib/pgbus/current_attributes.rb
287
320
  - lib/pgbus/database_tasks_guard.rb
321
+ - lib/pgbus/dead_letter_header.rb
288
322
  - lib/pgbus/dedicated_connection.rb
289
323
  - lib/pgbus/dedup_cache.rb
290
324
  - lib/pgbus/doctor.rb
@@ -409,11 +443,26 @@ files:
409
443
  - lib/pgbus/visibility_heartbeat.rb
410
444
  - lib/pgbus/web/authentication.rb
411
445
  - lib/pgbus/web/data_source.rb
446
+ - lib/pgbus/web/data_source/dashboard_signals.rb
447
+ - lib/pgbus/web/data_source/dead_letter.rb
448
+ - lib/pgbus/web/data_source/event_list.rb
449
+ - lib/pgbus/web/data_source/job_list.rb
450
+ - lib/pgbus/web/data_source/list_counts.rb
451
+ - lib/pgbus/web/data_source/process_list.rb
452
+ - lib/pgbus/web/data_source/queue_summary.rb
453
+ - lib/pgbus/web/dead_letter_reason.rb
454
+ - lib/pgbus/web/event_state.rb
412
455
  - lib/pgbus/web/health_app.rb
413
456
  - lib/pgbus/web/health_server.rb
457
+ - lib/pgbus/web/health_verdict.rb
458
+ - lib/pgbus/web/insights_charts.rb
414
459
  - lib/pgbus/web/job_context.rb
460
+ - lib/pgbus/web/job_state.rb
415
461
  - lib/pgbus/web/metrics_serializer.rb
416
462
  - lib/pgbus/web/payload_filter.rb
463
+ - lib/pgbus/web/process_activity.rb
464
+ - lib/pgbus/web/process_state.rb
465
+ - lib/pgbus/web/queue_summary.rb
417
466
  - lib/pgbus/web/stream_app.rb
418
467
  - lib/pgbus/web/streamer.rb
419
468
  - lib/pgbus/web/streamer/connection.rb
@@ -428,9 +477,11 @@ files:
428
477
  - lib/pgbus/web/streamer/master_hub.rb
429
478
  - lib/pgbus/web/streamer/master_hub_boot.rb
430
479
  - lib/pgbus/web/streamer/outbound_pump.rb
480
+ - lib/pgbus/web/streamer/presence_tracker.rb
431
481
  - lib/pgbus/web/streamer/registry.rb
432
482
  - lib/pgbus/web/streamer/stream_counter.rb
433
483
  - lib/pgbus/web/streamer/stream_event_dispatcher.rb
484
+ - lib/pgbus/web/time_format.rb
434
485
  - lib/puma/plugin/pgbus_streams.rb
435
486
  - lib/tasks/pgbus_autovacuum.rake
436
487
  - lib/tasks/pgbus_doctor.rake
@@ -1,169 +0,0 @@
1
- <turbo-frame id="pending-events">
2
- <%# Bulk form outside table to avoid nested form issues %>
3
- <form id="bulk-discard-events-form" action="<%= pgbus.discard_selected_events_path %>" method="post" data-turbo-frame="_top" class="hidden">
4
- <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
5
- </form>
6
- <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-bulk-scope="events">
7
- <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
8
- <thead class="bg-gray-50 dark:bg-gray-900">
9
- <tr>
10
- <% if @pending.any? %>
11
- <th class="w-10 px-4 py-3">
12
- <input type="checkbox" data-bulk-select-all
13
- aria-label="<%= t("pgbus.helpers.bulk_select_all") %>"
14
- class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
15
- </th>
16
- <% end %>
17
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.pending_headers.id") %></th>
18
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.pending_headers.routing_key") %></th>
19
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.pending_headers.handler_queue") %></th>
20
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.pending_headers.enqueued") %></th>
21
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.pending_headers.reads") %></th>
22
- </tr>
23
- </thead>
24
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
25
- <% @pending.each do |m| %>
26
- <%# Preserve the raw message so corrupt payloads stay visible in the
27
- edit textarea and full-JSON panel. pgbus_parse_message swallows
28
- JSON::ParserError and returns {}, which would otherwise hide
29
- exactly the data the operator is trying to inspect/repair.
30
- Also coerce scalar JSON values (numbers, booleans, nil, arrays)
31
- to {} for hash-style access below — otherwise payload["foo"]
32
- raises NoMethodError. %>
33
- <% payload_raw = pgbus_parse_message(m[:message]) %>
34
- <% payload = payload_raw.is_a?(Hash) ? payload_raw : {} %>
35
- <% parse_ok = m[:message].blank? || payload_raw != {} || m[:message].to_s.strip == "{}" %>
36
- <tr>
37
- <td class="w-10 px-4 py-3 align-top">
38
- <input type="checkbox" data-bulk-item data-pgbus-action="bulk-row-toggle"
39
- aria-label="<%= t("pgbus.helpers.bulk_select_row", id: m[:msg_id]) %>"
40
- data-queue-name="<%= m[:queue_name] %>" data-msg-id="<%= m[:msg_id] %>"
41
- class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 mt-1">
42
- <input type="hidden" name="messages[][queue_name]" value="<%= m[:queue_name] %>" form="bulk-discard-events-form" disabled>
43
- <input type="hidden" name="messages[][msg_id]" value="<%= m[:msg_id] %>" form="bulk-discard-events-form" disabled>
44
- </td>
45
- <td colspan="5" class="p-0">
46
- <details class="group">
47
- <summary class="flex cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900 list-none">
48
- <span class="w-16 shrink-0 px-4 py-3 text-sm font-mono text-gray-900 dark:text-white"><%= m[:msg_id] %></span>
49
- <span class="flex-1 px-4 py-3 text-sm font-mono text-indigo-600 truncate"><%= payload["routing_key"] || payload["event_id"] || "—" %></span>
50
- <span class="w-40 shrink-0 px-4 py-3 text-sm text-gray-700"><%= m[:queue_name] %></span>
51
- <span class="w-28 shrink-0 px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(m[:enqueued_at]) %></span>
52
- <span class="w-16 shrink-0 px-4 py-3 text-sm text-gray-500"><%= m[:read_ct] %></span>
53
- </summary>
54
- <div class="px-4 pb-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-100">
55
- <div class="flex items-center justify-between mt-3 mb-3">
56
- <span class="text-xs font-mono text-gray-400"><%= t("pgbus.events.pending_table.event_id") %> <%= payload["event_id"] %></span>
57
- <div class="flex space-x-2">
58
- <%# Mark Handled — handler_class is resolved server-side
59
- from the subscriber registry, so we only send queue_name. %>
60
- <%= button_to t("pgbus.events.pending_table.mark_handled"),
61
- pgbus.mark_handled_event_path(m[:msg_id], queue_name: m[:queue_name]),
62
- method: :post,
63
- class: "rounded-md bg-green-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-green-500",
64
- data: { turbo_confirm: t("pgbus.events.pending_table.mark_handled_confirm"), turbo_frame: "_top" } %>
65
- <%# Reroute — dropdown with available handlers %>
66
- <details class="relative inline-block">
67
- <summary class="rounded-md bg-yellow-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-yellow-500 cursor-pointer list-none">
68
- <%= t("pgbus.events.pending_table.reroute") %>
69
- </summary>
70
- <div class="absolute right-0 z-10 mt-1 w-64 rounded-md bg-white dark:bg-gray-800 shadow-lg ring-1 ring-gray-200 dark:ring-gray-700 p-2">
71
- <p class="text-xs text-gray-500 mb-2"><%= t("pgbus.events.pending_table.reroute_label") %></p>
72
- <% @subscribers.each do |s| %>
73
- <% next if s[:physical_queue_name] == m[:queue_name] %>
74
- <%= button_to s[:handler_class],
75
- pgbus.reroute_event_path(m[:msg_id], queue_name: m[:queue_name], target_queue: s[:physical_queue_name]),
76
- method: :post,
77
- class: "block w-full text-left rounded px-2 py-1 text-xs text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700",
78
- data: { turbo_confirm: t("pgbus.events.pending_table.reroute_confirm"), turbo_frame: "_top" } %>
79
- <% end %>
80
- </div>
81
- </details>
82
- <%# Discard %>
83
- <%= button_to t("pgbus.events.pending_table.discard"),
84
- pgbus.discard_event_path(m[:msg_id], queue_name: m[:queue_name]),
85
- method: :post,
86
- class: "rounded-md bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-500",
87
- data: { turbo_confirm: t("pgbus.events.pending_table.discard_confirm"), turbo_frame: "_top" } %>
88
- </div>
89
- </div>
90
- <%# Payload display %>
91
- <div class="grid grid-cols-2 gap-4 mb-3">
92
- <div>
93
- <span class="text-xs font-medium text-gray-500"><%= t("pgbus.events.pending_table.arguments") %></span>
94
- <pre class="text-xs text-gray-700 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto max-h-40"><%= parse_ok ? (JSON.pretty_generate(payload["payload"] || payload_raw) rescue "—") : m[:message] %></pre>
95
- </div>
96
- <div>
97
- <span class="text-xs font-medium text-gray-500"><%= t("pgbus.events.pending_table.metadata") %></span>
98
- <div class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 space-y-1">
99
- <% if m[:read_ct] %><p><strong><%= t("pgbus.events.pending_table.metadata_labels.read_count") %></strong> <%= m[:read_ct] %></p><% end %>
100
- <% if m[:vt] %><p><strong><%= t("pgbus.events.pending_table.metadata_labels.visible_at") %></strong> <%= m[:vt] %></p><% end %>
101
- <% if m[:last_read_at] %><p><strong><%= t("pgbus.events.pending_table.metadata_labels.last_read") %></strong> <%= m[:last_read_at] %></p><% end %>
102
- </div>
103
- </div>
104
- </div>
105
- <%# Persisted Current attributes (issue #431) %>
106
- <% if (event_context = pgbus_job_context(m[:message])) %>
107
- <div class="mb-3" data-testid="job-context">
108
- <span class="text-xs font-medium text-gray-500"><%= t("pgbus.events.pending_table.context") %></span>
109
- <div class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 space-y-2">
110
- <% event_context.each do |klass_name, attrs| %>
111
- <div>
112
- <p class="font-semibold text-gray-700 dark:text-gray-300 font-mono"><%= klass_name %></p>
113
- <dl class="grid grid-cols-1 gap-x-4 gap-y-0.5 sm:grid-cols-2">
114
- <% attrs.each do |name, value| %>
115
- <div class="flex gap-2">
116
- <dt class="text-gray-500 font-mono"><%= name %></dt>
117
- <dd class="text-gray-900 dark:text-white font-mono break-all"><%= value %></dd>
118
- </div>
119
- <% end %>
120
- </dl>
121
- </div>
122
- <% end %>
123
- </div>
124
- </div>
125
- <% end %>
126
- <%# Edit Payload form %>
127
- <details class="mt-2">
128
- <summary class="text-xs font-medium text-indigo-600 cursor-pointer hover:text-indigo-500"><%= t("pgbus.events.pending_table.edit_payload") %></summary>
129
- <div class="mt-2 p-3 bg-white dark:bg-gray-800 rounded ring-1 ring-gray-200 dark:ring-gray-700">
130
- <%= form_with url: pgbus.edit_payload_event_path(m[:msg_id]), method: :post, local: true, data: { turbo_frame: "_top" } do |f| %>
131
- <%= hidden_field_tag :queue_name, m[:queue_name] %>
132
- <label class="text-xs font-medium text-gray-500"><%= t("pgbus.events.pending_table.edit_payload_label") %></label>
133
- <textarea name="payload" rows="6"
134
- class="mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-900 dark:text-white text-xs font-mono shadow-sm focus:border-indigo-500 focus:ring-indigo-500"><%= parse_ok ? (JSON.pretty_generate(payload_raw) rescue m[:message]) : m[:message] %></textarea>
135
- <div class="mt-2 flex justify-end">
136
- <button type="submit"
137
- class="rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500"
138
- data-turbo-confirm="<%= t("pgbus.events.pending_table.edit_payload_confirm") %>">
139
- <%= t("pgbus.events.pending_table.edit_payload") %>
140
- </button>
141
- </div>
142
- <% end %>
143
- </div>
144
- </details>
145
- <%# Full JSON %>
146
- <details class="mt-2">
147
- <summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700"><%= t("pgbus.events.pending_table.full_json_payload") %></summary>
148
- <pre class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto max-h-96"><%= parse_ok ? (JSON.pretty_generate(payload_raw) rescue m[:message]) : m[:message] %></pre>
149
- </details>
150
- <% if m[:headers] %>
151
- <details class="mt-2">
152
- <summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700"><%= t("pgbus.events.pending_table.headers_section") %></summary>
153
- <pre class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto"><%= JSON.pretty_generate(JSON.parse(m[:headers])) rescue m[:headers] %></pre>
154
- </details>
155
- <% end %>
156
- </div>
157
- </details>
158
- </td>
159
- </tr>
160
- <% end %>
161
- <% if @pending.empty? %>
162
- <%# Header only renders the bulk-select column when rows are present,
163
- so the empty-state colspan must match (5 without, 6 with). %>
164
- <tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.events.index.pending_empty") %></td></tr>
165
- <% end %>
166
- </tbody>
167
- </table>
168
- </div>
169
- </turbo-frame>
@@ -1,118 +0,0 @@
1
- <turbo-frame id="jobs-enqueued" data-auto-refresh data-src="<%= pgbus.jobs_path(request.query_parameters.merge(frame: 'enqueued')) %>">
2
- <div data-bulk-page>
3
- <div class="flex items-center justify-between mb-3">
4
- <h2 class="text-lg font-semibold text-gray-900 dark:text-white"><%= t("pgbus.jobs.enqueued_table.title") %></h2>
5
- <% if @jobs.any? %>
6
- <div class="flex items-center space-x-2">
7
- <div data-bulk-actions class="hidden flex items-center space-x-2">
8
- <span class="text-sm text-gray-500 dark:text-gray-400"><span data-bulk-count>0</span> <%= t("pgbus.helpers.bulk_selected") %></span>
9
- <button type="submit" form="bulk-discard-enqueued-form"
10
- class="rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500"
11
- data-turbo-confirm="<%= t("pgbus.jobs.index.discard_selected_confirm") %>">
12
- <%= t("pgbus.jobs.index.discard_selected") %>
13
- </button>
14
- </div>
15
- <%= button_to t("pgbus.jobs.enqueued_table.discard_all"), pgbus.discard_all_enqueued_jobs_path, method: :post,
16
- class: "rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500",
17
- data: { turbo_confirm: t("pgbus.jobs.enqueued_table.discard_all_confirm"), turbo_frame: "_top" } %>
18
- </div>
19
- <% end %>
20
- </div>
21
- <%# Bulk form outside table to avoid nested form issues %>
22
- <form id="bulk-discard-enqueued-form" action="<%= pgbus.discard_selected_enqueued_jobs_path %>" method="post" data-turbo-frame="_top" class="hidden">
23
- <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
24
- </form>
25
- <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-bulk-scope="enqueued">
26
- <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
27
- <thead class="bg-gray-50 dark:bg-gray-900">
28
- <tr>
29
- <% if @jobs.any? %>
30
- <th class="w-10 px-4 py-3">
31
- <input type="checkbox" data-bulk-select-all
32
- aria-label="<%= t("pgbus.helpers.bulk_select_all") %>"
33
- class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
34
- </th>
35
- <% end %>
36
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.enqueued_table.headers.id") %></th>
37
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.enqueued_table.headers.job_class") %></th>
38
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.enqueued_table.headers.queue") %></th>
39
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.enqueued_table.headers.enqueued") %></th>
40
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.enqueued_table.headers.reads") %></th>
41
- </tr>
42
- </thead>
43
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
44
- <% @jobs.each do |j| %>
45
- <% payload = pgbus_parse_message(j[:message]) %>
46
- <tr>
47
- <td class="w-10 px-4 py-3 align-top">
48
- <input type="checkbox" data-bulk-item data-pgbus-action="bulk-row-toggle"
49
- aria-label="<%= t("pgbus.helpers.bulk_select_row", id: j[:msg_id]) %>"
50
- data-queue-name="<%= j[:queue_name] %>" data-msg-id="<%= j[:msg_id] %>"
51
- class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 mt-1">
52
- <input type="hidden" name="messages[][queue_name]" value="<%= j[:queue_name] %>" form="bulk-discard-enqueued-form" disabled>
53
- <input type="hidden" name="messages[][msg_id]" value="<%= j[:msg_id] %>" form="bulk-discard-enqueued-form" disabled>
54
- </td>
55
- <td colspan="5" class="p-0">
56
- <details class="group">
57
- <summary class="flex cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900 list-none">
58
- <span class="w-16 shrink-0 px-4 py-3 text-sm font-mono text-gray-900 dark:text-white"><%= j[:msg_id] %></span>
59
- <span class="flex-1 px-4 py-3 text-sm font-medium text-gray-900 dark:text-white"><%= payload["job_class"] || "—" %></span>
60
- <span class="w-40 shrink-0 px-4 py-3 text-sm text-gray-700"><%= j[:queue_name] %></span>
61
- <span class="w-28 shrink-0 px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(j[:enqueued_at]) %></span>
62
- <span class="w-16 shrink-0 px-4 py-3 text-sm text-gray-500"><%= j[:read_ct] %></span>
63
- </summary>
64
- <div class="px-4 pb-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-100">
65
- <div class="flex items-center mt-3 mb-3">
66
- <span class="text-xs font-mono text-gray-400"><%= t("pgbus.jobs.enqueued_table.job_id") %> <%= payload["job_id"] %></span>
67
- </div>
68
- <div class="grid grid-cols-2 gap-4 mb-3">
69
- <div>
70
- <span class="text-xs font-medium text-gray-500"><%= t("pgbus.jobs.enqueued_table.arguments") %></span>
71
- <pre class="text-xs text-gray-700 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto max-h-40"><%= JSON.pretty_generate(payload["arguments"] || []) rescue "—" %></pre>
72
- </div>
73
- <div>
74
- <span class="text-xs font-medium text-gray-500"><%= t("pgbus.jobs.enqueued_table.metadata") %></span>
75
- <div class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 space-y-1">
76
- <% if payload["queue_name"] %><p><strong><%= t("pgbus.jobs.enqueued_table.metadata_labels.queue") %></strong> <%= payload["queue_name"] %></p><% end %>
77
- <% if payload["priority"] %><p><strong><%= t("pgbus.jobs.enqueued_table.metadata_labels.priority") %></strong> <%= payload["priority"] %></p><% end %>
78
- <% if payload["locale"] %><p><strong><%= t("pgbus.jobs.enqueued_table.metadata_labels.locale") %></strong> <%= payload["locale"] %></p><% end %>
79
- <% if payload["timezone"] %><p><strong><%= t("pgbus.jobs.enqueued_table.metadata_labels.timezone") %></strong> <%= payload["timezone"] %></p><% end %>
80
- <% if payload["scheduled_at"] %><p><strong><%= t("pgbus.jobs.enqueued_table.metadata_labels.scheduled") %></strong> <%= payload["scheduled_at"] %></p><% end %>
81
- <% if j[:vt] %><p><strong><%= t("pgbus.jobs.enqueued_table.metadata_labels.visible_at") %></strong> <%= j[:vt] %></p><% end %>
82
- <% if j[:last_read_at] %><p><strong><%= t("pgbus.jobs.enqueued_table.metadata_labels.last_read") %></strong> <%= j[:last_read_at] %></p><% end %>
83
- </div>
84
- </div>
85
- </div>
86
- <details class="mt-2">
87
- <summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700"><%= t("pgbus.jobs.enqueued_table.full_json_payload") %></summary>
88
- <pre class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto max-h-96"><%= JSON.pretty_generate(payload) rescue j[:message] %></pre>
89
- </details>
90
- <% if j[:headers] %>
91
- <details class="mt-2">
92
- <summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700"><%= t("pgbus.jobs.enqueued_table.headers_section") %></summary>
93
- <pre class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto"><%= JSON.pretty_generate(JSON.parse(j[:headers])) rescue j[:headers] %></pre>
94
- </details>
95
- <% end %>
96
- <div class="flex space-x-2 mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
97
- <%= button_to t("pgbus.jobs.enqueued_table.retry"), pgbus.retry_message_queue_path(name: j[:queue_name], msg_id: j[:msg_id]),
98
- method: :post,
99
- class: "rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500",
100
- data: { turbo_confirm: t("pgbus.jobs.enqueued_table.retry_confirm"), turbo_frame: "_top" } %>
101
- <%= button_to t("pgbus.jobs.enqueued_table.discard"), pgbus.discard_message_queue_path(name: j[:queue_name], msg_id: j[:msg_id]),
102
- method: :post,
103
- class: "rounded-md bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-500",
104
- data: { turbo_confirm: t("pgbus.jobs.enqueued_table.discard_confirm"), turbo_frame: "_top" } %>
105
- </div>
106
- </div>
107
- </details>
108
- </td>
109
- </tr>
110
- <% end %>
111
- <% if @jobs.empty? %>
112
- <tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.jobs.enqueued_table.empty") %></td></tr>
113
- <% end %>
114
- </tbody>
115
- </table>
116
- </div>
117
- </div>
118
- </turbo-frame>
@@ -1,76 +0,0 @@
1
- <turbo-frame id="jobs-failed" data-auto-refresh data-src="<%= pgbus.jobs_path(frame: 'failed') %>">
2
- <div class="mb-8" data-bulk-page>
3
- <div class="flex items-center justify-between mb-3">
4
- <h2 class="text-lg font-semibold text-gray-900 dark:text-white"><%= t("pgbus.jobs.failed_table.title") %></h2>
5
- <% if @failed.any? %>
6
- <div class="flex items-center space-x-2">
7
- <div data-bulk-actions class="hidden flex items-center space-x-2">
8
- <span class="text-sm text-gray-500 dark:text-gray-400"><span data-bulk-count>0</span> <%= t("pgbus.helpers.bulk_selected") %></span>
9
- <button type="submit" form="bulk-discard-failed-form"
10
- class="rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500"
11
- data-turbo-confirm="<%= t("pgbus.jobs.index.discard_selected_confirm") %>">
12
- <%= t("pgbus.jobs.index.discard_selected") %>
13
- </button>
14
- </div>
15
- </div>
16
- <% end %>
17
- </div>
18
- <%# Bulk form outside table to avoid nested form issues %>
19
- <form id="bulk-discard-failed-form" action="<%= pgbus.discard_selected_failed_jobs_path %>" method="post" data-turbo-frame="_top" class="hidden">
20
- <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
21
- </form>
22
- <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-bulk-scope="failed">
23
- <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
24
- <thead class="bg-gray-50 dark:bg-gray-900">
25
- <tr>
26
- <% if @failed.any? %>
27
- <th class="w-10 px-4 py-3">
28
- <input type="checkbox" data-bulk-select-all
29
- aria-label="<%= t("pgbus.helpers.bulk_select_all") %>"
30
- class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
31
- </th>
32
- <% end %>
33
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.failed_table.headers.id") %></th>
34
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.failed_table.headers.queue") %></th>
35
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.failed_table.headers.error") %></th>
36
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.failed_table.headers.retries") %></th>
37
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.failed_table.headers.failed") %></th>
38
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.jobs.failed_table.headers.actions") %></th>
39
- </tr>
40
- </thead>
41
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
42
- <% @failed.each do |f| %>
43
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900">
44
- <td class="w-10 px-4 py-3">
45
- <input type="checkbox" name="ids[]" value="<%= f["id"] %>" form="bulk-discard-failed-form"
46
- aria-label="<%= t("pgbus.helpers.bulk_select_row", id: f["id"]) %>"
47
- data-bulk-item
48
- class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
49
- </td>
50
- <td data-label="ID" class="px-4 py-3 text-sm font-mono text-gray-900 dark:text-white">
51
- <%= link_to f["id"], pgbus.job_path(f["id"]), class: "text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
52
- </td>
53
- <td data-label="Queue" class="px-4 py-3 text-sm text-gray-700"><%= f["queue_name"] %></td>
54
- <td data-label="Error" class="px-4 py-3 text-sm text-red-600 max-w-sm truncate" title="<%= f["error_class"] %>: <%= f["error_message"] %>">
55
- <span class="font-medium"><%= f["error_class"] %></span>: <%= truncate(f["error_message"].to_s, length: 80) %>
56
- </td>
57
- <td data-label="Retries" class="px-4 py-3 text-sm text-gray-500"><%= f["retry_count"] %></td>
58
- <td data-label="Failed" class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(f["failed_at"]) %></td>
59
- <td data-label="Actions" class="px-4 py-3 text-sm text-right space-x-2">
60
- <%= button_to t("pgbus.jobs.failed_table.retry"), pgbus.retry_job_path(f["id"]), method: :post,
61
- class: "text-xs text-indigo-600 hover:text-indigo-800 font-medium",
62
- data: { turbo_frame: "_top" } %>
63
- <%= button_to t("pgbus.jobs.failed_table.discard"), pgbus.discard_job_path(f["id"]), method: :post,
64
- class: "text-xs text-red-600 hover:text-red-800 font-medium",
65
- data: { turbo_confirm: t("pgbus.jobs.failed_table.discard_confirm"), turbo_frame: "_top" } %>
66
- </td>
67
- </tr>
68
- <% end %>
69
- <% if @failed.empty? %>
70
- <tr><td colspan="7" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.jobs.failed_table.empty") %></td></tr>
71
- <% end %>
72
- </tbody>
73
- </table>
74
- </div>
75
- </div>
76
- </turbo-frame>