pgbus 0.17.1 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +48 -13
  4. data/app/controllers/concerns/pgbus/job_listing.rb +37 -0
  5. data/app/controllers/concerns/pgbus/process_listing.rb +17 -0
  6. data/app/controllers/pgbus/application_controller.rb +5 -2
  7. data/app/controllers/pgbus/batches_controller.rb +4 -1
  8. data/app/controllers/pgbus/dashboard_controller.rb +20 -6
  9. data/app/controllers/pgbus/dead_letter_controller.rb +27 -3
  10. data/app/controllers/pgbus/events_controller.rb +52 -3
  11. data/app/controllers/pgbus/insights_controller.rb +19 -1
  12. data/app/controllers/pgbus/jobs_controller.rb +45 -26
  13. data/app/controllers/pgbus/locks_controller.rb +7 -2
  14. data/app/controllers/pgbus/outbox_controller.rb +4 -1
  15. data/app/controllers/pgbus/processes_controller.rb +3 -1
  16. data/app/controllers/pgbus/queues_controller.rb +16 -4
  17. data/app/controllers/pgbus/recurring_tasks_controller.rb +5 -7
  18. data/app/frontend/pgbus/modules/charts.js +119 -77
  19. data/app/frontend/pgbus/style.css +1 -1
  20. data/app/frontend/pgbus/tailwind.css +18 -0
  21. data/app/helpers/pgbus/application_helper.rb +155 -113
  22. data/app/helpers/pgbus/button_helper.rb +88 -0
  23. data/app/helpers/pgbus/dashboard_helper.rb +157 -0
  24. data/app/helpers/pgbus/events_helper.rb +40 -0
  25. data/app/helpers/pgbus/insights_helper.rb +39 -0
  26. data/app/helpers/pgbus/processes_helper.rb +79 -0
  27. data/app/views/pgbus/batches/_batches_table.html.erb +16 -13
  28. data/app/views/pgbus/batches/show.html.erb +3 -4
  29. data/app/views/pgbus/dashboard/_processes_table.html.erb +11 -11
  30. data/app/views/pgbus/dashboard/_queue_health.html.erb +42 -51
  31. data/app/views/pgbus/dashboard/_queues_table.html.erb +18 -16
  32. data/app/views/pgbus/dashboard/_recent_failures.html.erb +16 -12
  33. data/app/views/pgbus/dashboard/_stats_cards.html.erb +70 -36
  34. data/app/views/pgbus/dashboard/show.html.erb +1 -1
  35. data/app/views/pgbus/dead_letter/_error.html.erb +13 -0
  36. data/app/views/pgbus/dead_letter/_messages_table.html.erb +37 -96
  37. data/app/views/pgbus/dead_letter/_row.html.erb +75 -0
  38. data/app/views/pgbus/dead_letter/index.html.erb +6 -9
  39. data/app/views/pgbus/dead_letter/show.html.erb +37 -21
  40. data/app/views/pgbus/events/_list.html.erb +55 -0
  41. data/app/views/pgbus/events/_processed_table.html.erb +45 -0
  42. data/app/views/pgbus/events/_replay.html.erb +10 -0
  43. data/app/views/pgbus/events/_row.html.erb +162 -0
  44. data/app/views/pgbus/events/_tabs.html.erb +13 -0
  45. data/app/views/pgbus/events/index.html.erb +27 -48
  46. data/app/views/pgbus/events/show.html.erb +25 -7
  47. data/app/views/pgbus/insights/_chart_card.html.erb +21 -0
  48. data/app/views/pgbus/insights/_latency_table.html.erb +20 -0
  49. data/app/views/pgbus/insights/_status_table.html.erb +19 -0
  50. data/app/views/pgbus/insights/_throughput_table.html.erb +18 -0
  51. data/app/views/pgbus/insights/show.html.erb +83 -100
  52. data/app/views/pgbus/jobs/_list.html.erb +64 -0
  53. data/app/views/pgbus/jobs/_row.html.erb +107 -0
  54. data/app/views/pgbus/jobs/_tabs.html.erb +14 -0
  55. data/app/views/pgbus/jobs/index.html.erb +10 -10
  56. data/app/views/pgbus/jobs/show.html.erb +18 -20
  57. data/app/views/pgbus/locks/_concurrency.html.erb +25 -23
  58. data/app/views/pgbus/locks/_uniqueness.html.erb +20 -22
  59. data/app/views/pgbus/outbox/index.html.erb +30 -27
  60. data/app/views/pgbus/processes/_processes_table.html.erb +6 -25
  61. data/app/views/pgbus/processes/_row.html.erb +48 -0
  62. data/app/views/pgbus/processes/_status.html.erb +4 -0
  63. data/app/views/pgbus/queues/_queues_list.html.erb +28 -32
  64. data/app/views/pgbus/queues/show.html.erb +69 -118
  65. data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +75 -76
  66. data/app/views/pgbus/recurring_tasks/index.html.erb +1 -1
  67. data/app/views/pgbus/recurring_tasks/show.html.erb +45 -51
  68. data/app/views/pgbus/shared/_empty_row.html.erb +2 -0
  69. data/app/views/pgbus/shared/_pager.html.erb +34 -0
  70. data/config/i18n-tasks.yml +11 -2
  71. data/config/locales/da.yml +405 -113
  72. data/config/locales/de.yml +405 -113
  73. data/config/locales/en.yml +405 -113
  74. data/config/locales/es.yml +404 -112
  75. data/config/locales/fi.yml +404 -112
  76. data/config/locales/fr.yml +406 -114
  77. data/config/locales/it.yml +405 -113
  78. data/config/locales/ja.yml +405 -113
  79. data/config/locales/nb.yml +405 -113
  80. data/config/locales/nl.yml +405 -113
  81. data/config/locales/pt.yml +406 -114
  82. data/config/locales/sv.yml +404 -112
  83. data/lib/generators/pgbus/templates/initializer.rb.erb +4 -0
  84. data/lib/pgbus/active_job/executor.rb +16 -6
  85. data/lib/pgbus/cli/dlq.rb +32 -5
  86. data/lib/pgbus/client.rb +4 -2
  87. data/lib/pgbus/configuration.rb +47 -2
  88. data/lib/pgbus/dead_letter_header.rb +129 -0
  89. data/lib/pgbus/doctor.rb +8 -4
  90. data/lib/pgbus/event_bus/handler.rb +18 -11
  91. data/lib/pgbus/event_bus/registry.rb +6 -0
  92. data/lib/pgbus/failed_event_recorder.rb +34 -0
  93. data/lib/pgbus/instrumentation.rb +2 -1
  94. data/lib/pgbus/mcp/base_tool.rb +25 -0
  95. data/lib/pgbus/mcp/tools/concurrency_tool.rb +23 -4
  96. data/lib/pgbus/mcp/tools/dlq_detail_tool.rb +4 -2
  97. data/lib/pgbus/mcp/tools/dlq_tool.rb +10 -4
  98. data/lib/pgbus/mcp/tools/locks_tool.rb +23 -5
  99. data/lib/pgbus/mcp/tools/recurring_tool.rb +33 -4
  100. data/lib/pgbus/outbox/poller.rb +1 -1
  101. data/lib/pgbus/process/consumer.rb +53 -3
  102. data/lib/pgbus/process/dispatcher.rb +1 -1
  103. data/lib/pgbus/process/supervisor.rb +32 -11
  104. data/lib/pgbus/process/worker.rb +45 -8
  105. data/lib/pgbus/recurring/scheduler.rb +2 -1
  106. data/lib/pgbus/version.rb +1 -1
  107. data/lib/pgbus/web/data_source/dashboard_signals.rb +170 -0
  108. data/lib/pgbus/web/data_source/dead_letter.rb +142 -0
  109. data/lib/pgbus/web/data_source/event_list.rb +161 -0
  110. data/lib/pgbus/web/data_source/job_list.rb +323 -0
  111. data/lib/pgbus/web/data_source/list_counts.rb +80 -0
  112. data/lib/pgbus/web/data_source/process_list.rb +69 -0
  113. data/lib/pgbus/web/data_source/queue_summary.rb +122 -0
  114. data/lib/pgbus/web/data_source.rb +132 -254
  115. data/lib/pgbus/web/dead_letter_reason.rb +81 -0
  116. data/lib/pgbus/web/event_state.rb +71 -0
  117. data/lib/pgbus/web/health_app.rb +7 -3
  118. data/lib/pgbus/web/health_verdict.rb +78 -0
  119. data/lib/pgbus/web/insights_charts.rb +84 -0
  120. data/lib/pgbus/web/job_state.rb +111 -0
  121. data/lib/pgbus/web/process_activity.rb +97 -0
  122. data/lib/pgbus/web/process_state.rb +187 -0
  123. data/lib/pgbus/web/queue_summary.rb +104 -0
  124. data/lib/pgbus/web/time_format.rb +128 -0
  125. metadata +43 -4
  126. data/app/views/pgbus/events/_pending_table.html.erb +0 -169
  127. data/app/views/pgbus/jobs/_enqueued_table.html.erb +0 -118
  128. data/app/views/pgbus/jobs/_failed_table.html.erb +0 -76
@@ -0,0 +1,13 @@
1
+ <%# The recorded error of a dead message: the full error line, which attempt
2
+ it came from when later ones recorded nothing, and the backtrace head.
3
+ Shared by the list expansion and the show page. Local: reason. %>
4
+ <% if reason.error_class %>
5
+ <p class="mb-2 text-sm text-red-700 dark:text-red-300 break-words"><span class="font-medium"><%= reason.error_class %></span>: <%= reason.error_message %></p>
6
+ <% if (later = reason.later_attempts) %>
7
+ <p class="mb-2 text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.dead_letter.detail.error_from_attempt", attempt: reason.error_attempt, from: later.first, to: later.last) %></p>
8
+ <% end %>
9
+ <% if reason.backtrace.present? %>
10
+ <span class="text-xs font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dead_letter.detail.backtrace") %></span>
11
+ <pre tabindex="0" class="mb-3 mt-1 max-h-40 overflow-auto rounded bg-white dark:bg-gray-800 p-2 text-xs text-gray-700 dark:text-gray-200"><%= reason.backtrace %></pre>
12
+ <% end %>
13
+ <% end %>
@@ -1,4 +1,29 @@
1
- <turbo-frame id="dlq-messages" data-auto-refresh data-src="<%= pgbus.dead_letter_index_path(frame: 'list', page: @page) %>">
1
+ <%# Dead-letter list (issue #495). Every row says why the message died, read
2
+ from the pgbus_dead_letter header block by DeadLetterReason; a row's
3
+ expansion is the <tr> after it, shown by CSS (.pgbus-job-detail in
4
+ tailwind.css) while the ID cell's <details> is open, like the Jobs list.
5
+ @list_path (->(extra) { url }) carries the dlq/error_class filters into
6
+ the frame source, the chips and the pager. %>
7
+ <% columns = @messages.any? ? 8 : 7 %>
8
+ <turbo-frame id="dlq-messages" data-auto-refresh data-src="<%= @list_path.call(frame: "list", page: (@page if @page > 1)) %>">
9
+ <nav class="mb-4 flex flex-wrap items-center gap-2" aria-label="<%= t("pgbus.dead_letter.filters.label") %>" data-testid="dlq-filters">
10
+ <% [nil, *@dlq_counts.keys].each do |name| %>
11
+ <% active = name == @dlq %>
12
+ <a href="<%= @list_path.call(dlq: name, page: nil) %>" data-turbo-frame="_top"
13
+ aria-current="<%= active ? "page" : "false" %>"
14
+ class="inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium <%= active ? 'bg-gray-800 text-white dark:bg-gray-700' : 'text-gray-600 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-white' %>">
15
+ <span><%= name || t("pgbus.dead_letter.filters.all") %></span>
16
+ <span class="rounded-full px-2 py-0.5 text-xs <%= active ? 'bg-gray-600 text-white' : 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200' %>"><%= number_with_delimiter(name ? @dlq_counts[name] : @dlq_counts.values.sum) %></span>
17
+ </a>
18
+ <% end %>
19
+ <% if @error_class %>
20
+ <span class="inline-flex items-center gap-2 rounded-md bg-red-50 dark:bg-red-900/30 px-3 py-1 text-sm text-red-800 dark:text-red-200" data-testid="dlq-error-filter">
21
+ <%= t("pgbus.dead_letter.filters.error_class", error_class: @error_class) %>
22
+ <%= pgbus_link_to t("pgbus.dead_letter.filters.clear_error_class"), @list_path.call(error_class: nil, page: nil),
23
+ size: :sm, tone: :red, data: { turbo_frame: "_top" } %>
24
+ </span>
25
+ <% end %>
26
+ </nav>
2
27
  <%# Count badge lives inside the frame so it refreshes in lockstep with the table. %>
3
28
  <% if @total_count && @total_count > 0 %>
4
29
  <p class="mb-3 text-sm text-gray-500 dark:text-gray-400">
@@ -18,110 +43,26 @@
18
43
  <th class="w-10 px-4 py-3">
19
44
  <input type="checkbox" data-bulk-select-all
20
45
  aria-label="<%= t("pgbus.helpers.bulk_select_all") %>"
21
- class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600">
46
+ class="h-4 w-4 rounded border-gray-300 text-indigo-600 dark:text-indigo-400 focus:ring-indigo-600">
22
47
  </th>
23
48
  <% end %>
24
- <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>
25
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dead_letter.messages_table.headers.job_class") %></th>
26
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dead_letter.messages_table.headers.source_queue") %></th>
27
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dead_letter.messages_table.headers.enqueued") %></th>
28
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dead_letter.messages_table.headers.reads") %></th>
49
+ <% %w[id job source_queue died attempts reason].each do |header| %>
50
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dead_letter.messages_table.headers.#{header}") %></th>
51
+ <% end %>
52
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dead_letter.messages_table.headers.actions") %></th>
29
53
  </tr>
30
54
  </thead>
31
55
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
32
- <% @messages.each do |m| %>
33
- <% payload = pgbus_parse_message(m[:message]) %>
34
- <% source_queue = m[:queue_name].to_s.delete_suffix(Pgbus::DEAD_LETTER_SUFFIX) %>
35
- <tr>
36
- <td class="w-10 px-4 py-3 align-top">
37
- <input type="checkbox" data-bulk-item data-pgbus-action="bulk-row-toggle"
38
- aria-label="<%= t("pgbus.helpers.bulk_select_row", id: m[:msg_id]) %>"
39
- data-queue-name="<%= m[:queue_name] %>" data-msg-id="<%= m[:msg_id] %>"
40
- class="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 mt-1">
41
- <input type="hidden" name="messages[][queue_name]" value="<%= m[:queue_name] %>" form="bulk-discard-dlq-form" disabled>
42
- <input type="hidden" name="messages[][msg_id]" value="<%= m[:msg_id] %>" form="bulk-discard-dlq-form" disabled>
43
- </td>
44
- <td colspan="5" class="p-0">
45
- <details class="group">
46
- <summary class="flex cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900 list-none">
47
- <span class="w-16 shrink-0 px-4 py-3 text-sm font-mono text-gray-900 dark:text-white"><%= m[:msg_id] %></span>
48
- <span class="flex-1 px-4 py-3 text-sm font-medium text-gray-900 dark:text-white"><%= payload["job_class"] || "—" %></span>
49
- <span class="w-40 shrink-0 px-4 py-3 text-sm text-gray-700"><%= source_queue %></span>
50
- <span class="w-28 shrink-0 px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(m[:enqueued_at]) %></span>
51
- <span class="w-16 shrink-0 px-4 py-3 text-sm text-gray-500"><%= m[:read_ct] %></span>
52
- </summary>
53
- <div class="px-4 pb-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-100">
54
- <div class="flex items-center justify-between mt-3 mb-3">
55
- <span class="text-xs font-mono text-gray-400"><%= t("pgbus.dead_letter.messages_table.job_id") %> <%= payload["job_id"] %></span>
56
- <div class="flex space-x-2">
57
- <%= button_to t("pgbus.dead_letter.messages_table.retry"), pgbus.retry_dead_letter_path(m[:msg_id], queue_name: m[:queue_name]), method: :post,
58
- class: "rounded-md bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500",
59
- data: { turbo_frame: "_top" } %>
60
- <%= button_to t("pgbus.dead_letter.messages_table.discard"), pgbus.discard_dead_letter_path(m[:msg_id], queue_name: m[:queue_name]), method: :post,
61
- class: "rounded-md bg-red-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-red-500",
62
- data: { turbo_confirm: t("pgbus.dead_letter.messages_table.discard_confirm"), turbo_frame: "_top" } %>
63
- </div>
64
- </div>
65
- <div class="grid grid-cols-2 gap-4 mb-3">
66
- <div>
67
- <span class="text-xs font-medium text-gray-500"><%= t("pgbus.dead_letter.messages_table.arguments") %></span>
68
- <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>
69
- </div>
70
- <div>
71
- <span class="text-xs font-medium text-gray-500"><%= t("pgbus.dead_letter.messages_table.metadata") %></span>
72
- <div class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 space-y-1">
73
- <% if payload["queue_name"] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.queue") %></strong> <%= payload["queue_name"] %></p><% end %>
74
- <% if payload["priority"] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.priority") %></strong> <%= payload["priority"] %></p><% end %>
75
- <% if payload["executions"] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.executions") %></strong> <%= payload["executions"] %></p><% end %>
76
- <% if m[:vt] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.visible_at") %></strong> <%= m[:vt] %></p><% end %>
77
- <% if m[:last_read_at] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.last_read") %></strong> <%= m[:last_read_at] %></p><% end %>
78
- </div>
79
- </div>
80
- </div>
81
- <details class="mt-2">
82
- <summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700"><%= t("pgbus.dead_letter.messages_table.full_json_payload") %></summary>
83
- <pre class="text-xs text-gray-600 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto max-h-96"><%= JSON.pretty_generate(payload) rescue m[:message] %></pre>
84
- </details>
85
- <% if m[:headers] %>
86
- <details class="mt-2">
87
- <summary class="text-xs font-medium text-gray-500 cursor-pointer hover:text-gray-700"><%= t("pgbus.dead_letter.messages_table.headers_section") %></summary>
88
- <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>
89
- </details>
90
- <% end %>
91
- </div>
92
- </details>
93
- </td>
94
- </tr>
56
+ <% @dlq_rows.each do |m, reason| %>
57
+ <%= render "pgbus/dead_letter/row", m: m, reason: reason, columns: columns %>
95
58
  <% end %>
96
59
  <% if @messages.empty? %>
97
- <tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.dead_letter.messages_table.empty") %></td></tr>
60
+ <%# No rows means no checkbox column. %>
61
+ <%= render "pgbus/shared/empty_row", colspan: columns, message: t("pgbus.dead_letter.messages_table.empty") %>
98
62
  <% end %>
99
63
  </tbody>
100
64
  </table>
101
- <% if @total_count && @total_count > 0 %>
102
- <div class="flex items-center justify-between border-t border-gray-200 dark:border-gray-700 px-4 py-3">
103
- <p class="text-sm text-gray-500 dark:text-gray-400">
104
- <% from = (@page - 1) * @per_page + 1 %>
105
- <% to = [from + @messages.size - 1, @total_count].min %>
106
- <%= t("pgbus.helpers.pagination.showing", from: from, to: to, total: @total_count) %>
107
- </p>
108
- <% if @total_pages > 1 %>
109
- <nav class="inline-flex rounded-lg shadow-sm" role="navigation" aria-label="Pagination">
110
- <% if @page > 1 %>
111
- <%= link_to t("pgbus.helpers.pagination.previous"), pgbus.dead_letter_index_path(page: @page - 1),
112
- class: "px-3 py-1.5 text-xs font-medium border border-gray-300 dark:border-gray-600 rounded-l-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700" %>
113
- <% else %>
114
- <span class="px-3 py-1.5 text-xs font-medium border border-gray-300 dark:border-gray-600 rounded-l-lg bg-gray-100 dark:bg-gray-900 text-gray-400 dark:text-gray-600 cursor-not-allowed"><%= t("pgbus.helpers.pagination.previous") %></span>
115
- <% end %>
116
- <% if @page < @total_pages %>
117
- <%= link_to t("pgbus.helpers.pagination.next"), pgbus.dead_letter_index_path(page: @page + 1),
118
- class: "-ml-px px-3 py-1.5 text-xs font-medium border border-gray-300 dark:border-gray-600 rounded-r-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700" %>
119
- <% else %>
120
- <span class="-ml-px px-3 py-1.5 text-xs font-medium border border-gray-300 dark:border-gray-600 rounded-r-lg bg-gray-100 dark:bg-gray-900 text-gray-400 dark:text-gray-600 cursor-not-allowed"><%= t("pgbus.helpers.pagination.next") %></span>
121
- <% end %>
122
- </nav>
123
- <% end %>
124
- </div>
125
- <% end %>
65
+ <%= render "pgbus/shared/pager", page: @page, per_page: @per_page, shown: @messages.size, total_count: @total_count.to_i,
66
+ page_path: ->(n) { @list_path.call(page: n) } %>
126
67
  </div>
127
68
  </turbo-frame>
@@ -0,0 +1,75 @@
1
+ <%# One dead-letter message plus its expansion row. Locals: m (a
2
+ DataSource#dlq_messages hash), reason (its DeadLetterReason::Result),
3
+ columns. %>
4
+ <% payload = pgbus_parse_message(m[:message]) %>
5
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50" data-dlq-row="<%= m[:msg_id] %>">
6
+ <td class="w-10 px-4 py-3 align-top">
7
+ <input type="checkbox" data-bulk-item data-pgbus-action="bulk-row-toggle"
8
+ aria-label="<%= t("pgbus.helpers.bulk_select_row", id: m[:msg_id]) %>"
9
+ data-queue-name="<%= m[:queue_name] %>" data-msg-id="<%= m[:msg_id] %>"
10
+ class="h-4 w-4 rounded border-gray-300 text-indigo-600 dark:text-indigo-400 focus:ring-indigo-600 mt-1">
11
+ <input type="hidden" name="messages[][queue_name]" value="<%= m[:queue_name] %>" form="bulk-discard-dlq-form" disabled>
12
+ <input type="hidden" name="messages[][msg_id]" value="<%= m[:msg_id] %>" form="bulk-discard-dlq-form" disabled>
13
+ </td>
14
+ <td data-label="<%= t("pgbus.dead_letter.messages_table.headers.id") %>" class="px-4 py-3 text-sm font-mono text-gray-900 dark:text-white align-top">
15
+ <details data-job-toggle class="group">
16
+ <summary class="cursor-pointer list-none text-indigo-600 hover:text-indigo-800 dark:hover:text-indigo-300 dark:text-indigo-400"><%= m[:msg_id] %></summary>
17
+ </details>
18
+ </td>
19
+ <td data-label="<%= t("pgbus.dead_letter.messages_table.headers.job") %>" class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white align-top"><%= pgbus_dead_letter_job(m[:message]) %></td>
20
+ <td data-label="<%= t("pgbus.dead_letter.messages_table.headers.source_queue") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-200 align-top"><%= reason.source_queue %></td>
21
+ <td data-label="<%= t("pgbus.dead_letter.messages_table.headers.died") %>" class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 align-top"><%= pgbus_time(reason.died_at) %></td>
22
+ <td data-label="<%= t("pgbus.dead_letter.messages_table.headers.attempts") %>" class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 align-top"><%= pgbus_dead_letter_attempts(reason) %></td>
23
+ <td data-label="<%= t("pgbus.dead_letter.messages_table.headers.reason") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-200 align-top" data-testid="dlq-reason">
24
+ <%= pgbus_dead_letter_reason(reason, filter_path: @list_path) %>
25
+ </td>
26
+ <td data-label="<%= t("pgbus.dead_letter.messages_table.headers.actions") %>" class="px-4 py-3 text-sm text-right align-top">
27
+ <div class="flex justify-end space-x-2">
28
+ <%= pgbus_button_to t("pgbus.dead_letter.messages_table.retry"), pgbus.retry_dead_letter_path(m[:msg_id], queue_name: m[:queue_name]), method: :post,
29
+ variant: :link, size: :sm,
30
+ data: { turbo_frame: "_top" } %>
31
+ <%= pgbus_button_to t("pgbus.dead_letter.messages_table.discard"), pgbus.discard_dead_letter_path(m[:msg_id], queue_name: m[:queue_name]), method: :post,
32
+ variant: :link, size: :sm, tone: :red,
33
+ data: { turbo_confirm: t("pgbus.dead_letter.messages_table.discard_confirm"), turbo_frame: "_top" } %>
34
+ </div>
35
+ </td>
36
+ </tr>
37
+ <tr class="pgbus-job-detail" data-job-detail data-dlq-detail="<%= m[:msg_id] %>">
38
+ <td colspan="<%= columns %>" class="px-4 pb-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-100 dark:border-gray-700">
39
+ <div class="flex items-center justify-between mt-3 mb-3">
40
+ <span class="text-xs font-mono text-gray-500 dark:text-gray-400"><%= t("pgbus.dead_letter.messages_table.job_id") %> <%= payload["job_id"] || payload["event_id"] || "—" %></span>
41
+ <%= pgbus_link_to t("pgbus.dead_letter.messages_table.view"), pgbus.dead_letter_path(m[:msg_id], queue_name: m[:queue_name]),
42
+ size: :sm, data: { turbo_frame: "_top" } %>
43
+ </div>
44
+ <%= render "pgbus/dead_letter/error", reason: reason %>
45
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-3">
46
+ <div>
47
+ <span class="text-xs font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dead_letter.messages_table.arguments") %></span>
48
+ <pre class="text-xs text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto max-h-40"><%= JSON.pretty_generate(payload["arguments"] || payload["payload"] || []) rescue "—" %></pre>
49
+ </div>
50
+ <div>
51
+ <span class="text-xs font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dead_letter.messages_table.metadata") %></span>
52
+ <div class="text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800 rounded p-2 mt-1 space-y-1">
53
+ <p><strong><%= t("pgbus.dead_letter.detail.dlq") %></strong> <%= m[:queue_name] %></p>
54
+ <% if reason.source %><p><strong><%= t("pgbus.dead_letter.detail.source") %></strong> <%= t("pgbus.dead_letter.detail.sources.#{reason.source}", default: reason.source) %></p><% end %>
55
+ <p><strong><%= t("pgbus.dead_letter.detail.died") %></strong> <%= pgbus_timestamp(reason.died_at) %></p>
56
+ <% if payload["queue_name"] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.queue") %></strong> <%= payload["queue_name"] %></p><% end %>
57
+ <% if payload["priority"] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.priority") %></strong> <%= payload["priority"] %></p><% end %>
58
+ <% if payload["executions"] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.executions") %></strong> <%= payload["executions"] %></p><% end %>
59
+ <% if m[:vt] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.visible_at") %></strong> <%= pgbus_timestamp(m[:vt]) %></p><% end %>
60
+ <% if m[:last_read_at] %><p><strong><%= t("pgbus.dead_letter.messages_table.metadata_labels.last_read") %></strong> <%= pgbus_timestamp(m[:last_read_at]) %></p><% end %>
61
+ </div>
62
+ </div>
63
+ </div>
64
+ <details class="mt-2">
65
+ <summary class="text-xs font-medium text-gray-500 dark:text-gray-400 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200"><%= t("pgbus.dead_letter.messages_table.full_json_payload") %></summary>
66
+ <pre class="text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800 rounded p-2 mt-1 overflow-x-auto max-h-96"><%= JSON.pretty_generate(payload) rescue m[:message] %></pre>
67
+ </details>
68
+ <% if m[:headers] %>
69
+ <details class="mt-2">
70
+ <summary class="text-xs font-medium text-gray-500 dark:text-gray-400 cursor-pointer hover:text-gray-700 dark:hover:text-gray-200"><%= t("pgbus.dead_letter.messages_table.headers_section") %></summary>
71
+ <pre class="text-xs text-gray-600 dark:text-gray-300 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>
72
+ </details>
73
+ <% end %>
74
+ </td>
75
+ </tr>
@@ -4,17 +4,14 @@
4
4
  <div class="flex items-center space-x-2">
5
5
  <div data-bulk-actions class="hidden flex items-center space-x-2">
6
6
  <span class="text-sm text-gray-500 dark:text-gray-400"><span data-bulk-count>0</span> <%= t("pgbus.helpers.bulk_selected") %></span>
7
- <button type="submit" form="bulk-discard-dlq-form"
8
- class="rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500"
9
- data-turbo-confirm="<%= t("pgbus.dead_letter.index.discard_selected_confirm") %>">
10
- <%= t("pgbus.dead_letter.index.discard_selected") %>
11
- </button>
7
+ <%= pgbus_button_tag t("pgbus.dead_letter.index.discard_selected"), variant: :danger, form: "bulk-discard-dlq-form",
8
+ confirm: t("pgbus.dead_letter.index.discard_selected_confirm") %>
12
9
  </div>
13
- <%= button_to t("pgbus.dead_letter.index.retry_all"), pgbus.retry_all_dead_letter_index_path, method: :post,
14
- class: "rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-500",
10
+ <%= pgbus_button_to t("pgbus.dead_letter.index.retry_all"), pgbus.retry_all_dead_letter_index_path, method: :post,
11
+ variant: :primary,
15
12
  data: { turbo_confirm: t("pgbus.dead_letter.index.retry_all_confirm") } %>
16
- <%= button_to t("pgbus.dead_letter.index.discard_all"), pgbus.discard_all_dead_letter_index_path, method: :post,
17
- class: "rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500",
13
+ <%= pgbus_button_to t("pgbus.dead_letter.index.discard_all"), pgbus.discard_all_dead_letter_index_path, method: :post,
14
+ variant: :danger,
18
15
  data: { turbo_confirm: t("pgbus.dead_letter.index.discard_all_confirm") } %>
19
16
  </div>
20
17
  <% end %>
@@ -1,40 +1,56 @@
1
1
  <div class="mb-6">
2
- <%= link_to "\u2190 #{t("pgbus.dead_letter.show.back")}", pgbus.dead_letter_index_path, class: "text-sm text-indigo-600 hover:text-indigo-500" %>
2
+ <%= pgbus_link_to "\u2190 #{t("pgbus.dead_letter.show.back")}", pgbus.dead_letter_index_path %>
3
3
  </div>
4
4
 
5
5
  <% if @message %>
6
6
  <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-6"><%= t("pgbus.dead_letter.show.title", id: @message[:msg_id]) %></h1>
7
7
 
8
+ <div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700 p-6 mb-6" data-testid="dead-letter-reason">
9
+ <h2 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2"><%= t("pgbus.dead_letter.detail.why") %></h2>
10
+ <p class="text-sm text-gray-900 dark:text-white mb-3"><%= pgbus_dead_letter_reason(@reason) %></p>
11
+ <%= render "pgbus/dead_letter/error", reason: @reason %>
12
+ <dl class="grid grid-cols-1 gap-4 sm:grid-cols-3">
13
+ <div>
14
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dead_letter.show.labels.source_queue") %></dt>
15
+ <dd class="text-sm text-gray-900 dark:text-white"><%= @reason.source_queue %></dd>
16
+ </div>
17
+ <div>
18
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dead_letter.show.labels.attempts") %></dt>
19
+ <dd class="text-sm text-gray-900 dark:text-white"><%= pgbus_dead_letter_attempts(@reason) %></dd>
20
+ </div>
21
+ <div>
22
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dead_letter.show.labels.died") %></dt>
23
+ <dd class="text-sm text-gray-900 dark:text-white"><%= pgbus_timestamp(@reason.died_at) %></dd>
24
+ </div>
25
+ </dl>
26
+ </div>
27
+
8
28
  <div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700 p-6 mb-6">
9
29
  <dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
10
30
  <div>
11
- <dt class="text-sm font-medium text-gray-500"><%= t("pgbus.dead_letter.show.labels.source_queue") %></dt>
31
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dead_letter.show.labels.dlq") %></dt>
12
32
  <dd class="text-sm text-gray-900 dark:text-white"><%= @message[:queue_name] %></dd>
13
33
  </div>
14
34
  <div>
15
- <dt class="text-sm font-medium text-gray-500"><%= t("pgbus.dead_letter.show.labels.enqueued_at") %></dt>
16
- <dd class="text-sm text-gray-900 dark:text-white"><%= @message[:enqueued_at] %></dd>
17
- </div>
18
- <div>
19
- <dt class="text-sm font-medium text-gray-500"><%= t("pgbus.dead_letter.show.labels.read_count") %></dt>
20
- <dd class="text-sm text-gray-900 dark:text-white"><%= @message[:read_ct] %></dd>
35
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dead_letter.show.labels.enqueued_at") %></dt>
36
+ <dd class="text-sm text-gray-900 dark:text-white"><%= pgbus_timestamp(@message[:enqueued_at]) %></dd>
21
37
  </div>
22
38
  <div>
23
- <dt class="text-sm font-medium text-gray-500"><%= t("pgbus.dead_letter.show.labels.visibility_timeout") %></dt>
24
- <dd class="text-sm text-gray-900 dark:text-white"><%= @message[:vt] %></dd>
39
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dead_letter.show.labels.visibility_timeout") %></dt>
40
+ <dd class="text-sm text-gray-900 dark:text-white"><%= pgbus_timestamp(@message[:vt]) %></dd>
25
41
  </div>
26
42
  </dl>
27
43
  </div>
28
44
 
29
45
  <% if (context = pgbus_job_context(@message[:message])) %>
30
46
  <div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700 p-6 mb-6" data-testid="job-context">
31
- <h2 class="text-sm font-medium text-gray-500 mb-2"><%= t("pgbus.dead_letter.show.context") %></h2>
47
+ <h2 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2"><%= t("pgbus.dead_letter.show.context") %></h2>
32
48
  <% context.each do |klass_name, attrs| %>
33
49
  <h3 class="text-xs font-semibold text-gray-700 dark:text-gray-300 mt-3 mb-1 font-mono"><%= klass_name %></h3>
34
50
  <dl class="grid grid-cols-1 gap-x-4 gap-y-1 sm:grid-cols-2">
35
51
  <% attrs.each do |name, value| %>
36
52
  <div class="flex gap-2 text-xs">
37
- <dt class="text-gray-500 font-mono"><%= name %></dt>
53
+ <dt class="text-gray-500 dark:text-gray-400 font-mono"><%= name %></dt>
38
54
  <dd class="text-gray-900 dark:text-white font-mono break-all"><%= value %></dd>
39
55
  </div>
40
56
  <% end %>
@@ -44,26 +60,26 @@
44
60
  <% end %>
45
61
 
46
62
  <div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700 p-6 mb-6">
47
- <h2 class="text-sm font-medium text-gray-500 mb-2"><%= t("pgbus.dead_letter.show.payload") %></h2>
48
- <pre class="text-xs text-gray-600 bg-gray-50 dark:bg-gray-900 rounded p-4 overflow-x-auto"><%= JSON.pretty_generate(JSON.parse(@message[:message])) rescue @message[:message] %></pre>
63
+ <h2 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2"><%= t("pgbus.dead_letter.show.payload") %></h2>
64
+ <pre tabindex="0" class="text-xs text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-900 rounded p-4 overflow-x-auto"><%= JSON.pretty_generate(JSON.parse(@message[:message])) rescue @message[:message] %></pre>
49
65
  </div>
50
66
 
51
67
  <% if @message[:headers] %>
52
68
  <div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700 p-6 mb-6">
53
- <h2 class="text-sm font-medium text-gray-500 mb-2"><%= t("pgbus.dead_letter.show.headers") %></h2>
54
- <pre class="text-xs text-gray-600 bg-gray-50 dark:bg-gray-900 rounded p-4 overflow-x-auto"><%= JSON.pretty_generate(JSON.parse(@message[:headers])) rescue @message[:headers] %></pre>
69
+ <h2 class="text-sm font-medium text-gray-500 dark:text-gray-400 mb-2"><%= t("pgbus.dead_letter.show.headers") %></h2>
70
+ <pre tabindex="0" class="text-xs text-gray-600 dark:text-gray-300 bg-gray-50 dark:bg-gray-900 rounded p-4 overflow-x-auto"><%= JSON.pretty_generate(JSON.parse(@message[:headers])) rescue @message[:headers] %></pre>
55
71
  </div>
56
72
  <% end %>
57
73
 
58
74
  <div class="flex space-x-2">
59
- <%= button_to t("pgbus.dead_letter.show.retry"), pgbus.retry_dead_letter_path(@message[:msg_id], queue_name: @message[:queue_name]), method: :post,
60
- class: "rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500" %>
61
- <%= button_to t("pgbus.dead_letter.show.discard"), pgbus.discard_dead_letter_path(@message[:msg_id], queue_name: @message[:queue_name]), method: :post,
62
- class: "rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-500",
75
+ <%= pgbus_button_to t("pgbus.dead_letter.show.retry"), pgbus.retry_dead_letter_path(@message[:msg_id], queue_name: @message[:queue_name]), method: :post,
76
+ variant: :primary %>
77
+ <%= pgbus_button_to t("pgbus.dead_letter.show.discard"), pgbus.discard_dead_letter_path(@message[:msg_id], queue_name: @message[:queue_name]), method: :post,
78
+ variant: :danger,
63
79
  data: { turbo_confirm: t("pgbus.dead_letter.show.discard_confirm") } %>
64
80
  </div>
65
81
  <% else %>
66
82
  <div class="text-center py-12">
67
- <p class="text-gray-400"><%= t("pgbus.dead_letter.show.not_found") %></p>
83
+ <p class="text-gray-500 dark:text-gray-400"><%= t("pgbus.dead_letter.show.not_found") %></p>
68
84
  </div>
69
85
  <% end %>
@@ -0,0 +1,55 @@
1
+ <%# Pending events (issue #494): the unified job list scoped to the EventBus
2
+ handler queues. Each row's state comes from SQL, EventState adds the
3
+ reason; a row's expansion is the <tr> after it (.pgbus-job-detail in
4
+ tailwind.css), so the auto-refresh pause on `details[open]` keeps working.
5
+ Orphaned failed rows (message gone) are discarded through their failed
6
+ event and take no part in the bulk selection. %>
7
+ <% selectable = @rows.any? { |r| r[:source] == "queue" } %>
8
+ <% columns = selectable ? 8 : 7 %>
9
+ <turbo-frame id="events-list" data-auto-refresh data-turbo-action="advance"
10
+ data-src="<%= events_list_path(frame: "list") %>">
11
+ <div data-bulk-page>
12
+ <div class="flex items-center justify-end mb-3 space-x-2">
13
+ <div data-bulk-actions class="hidden flex items-center space-x-2">
14
+ <span class="text-sm text-gray-500 dark:text-gray-400"><span data-bulk-count>0</span> <%= t("pgbus.helpers.bulk_selected") %></span>
15
+ <%= pgbus_button_tag t("pgbus.events.list.discard_selected"), variant: :danger, form: "bulk-discard-events-form",
16
+ confirm: t("pgbus.events.list.discard_selected_confirm") %>
17
+ </div>
18
+ </div>
19
+ <%# Bulk form outside the table to avoid nested forms. %>
20
+ <form id="bulk-discard-events-form" action="<%= pgbus.discard_selected_events_path %>" method="post" data-turbo-frame="_top" class="hidden">
21
+ <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
22
+ </form>
23
+ <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">
24
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
25
+ <thead class="bg-gray-50 dark:bg-gray-900">
26
+ <tr>
27
+ <% if selectable %>
28
+ <th class="w-10 px-4 py-3">
29
+ <input type="checkbox" data-bulk-select-all
30
+ aria-label="<%= t("pgbus.helpers.bulk_select_all") %>"
31
+ class="h-4 w-4 rounded border-gray-300 text-indigo-600 dark:text-indigo-400 focus:ring-indigo-600">
32
+ </th>
33
+ <% end %>
34
+ <% %w[id event handler state attempts reason].each do |header| %>
35
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.events.list.headers.#{header}") %></th>
36
+ <% end %>
37
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.events.list.headers.actions") %></th>
38
+ </tr>
39
+ </thead>
40
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
41
+ <% @rows.each do |row| %>
42
+ <% event = Pgbus::Web::EventState.present(row, @state_context, ahead: @ahead[[row[:queue_name], row[:msg_id]]]) %>
43
+ <%= render "pgbus/events/row", row: row, event: event, selectable: selectable, columns: columns %>
44
+ <% end %>
45
+ <% if @rows.empty? %>
46
+ <%= render "pgbus/shared/empty_row", colspan: columns, message: t("pgbus.events.list.empty.#{@state}") %>
47
+ <% end %>
48
+ </tbody>
49
+ </table>
50
+ <%= render "pgbus/shared/pager", page: @page, per_page: @per_page, shown: @rows.size,
51
+ total_count: @counts[@state], has_more: @counts.capped?(@state) && @rows.size == @per_page,
52
+ page_path: ->(n) { events_list_path(page: n) } %>
53
+ </div>
54
+ </div>
55
+ </turbo-frame>
@@ -0,0 +1,45 @@
1
+ <%# Processed events (issue #494): the pgbus_processed_events audit, each
2
+ claim in words (completed, being handled, went silent) and a Replay where
3
+ the handler is still registered and the archive still holds the event.
4
+ Pages on processed_page, so the pending list keeps its own page. %>
5
+ <turbo-frame id="processed-events" data-auto-refresh data-turbo-action="advance"
6
+ data-src="<%= events_list_path(frame: "processed") %>">
7
+ <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
8
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
9
+ <thead class="bg-gray-50 dark:bg-gray-900">
10
+ <tr>
11
+ <% %w[event_id handler state reason].each do |header| %>
12
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.events.processed.headers.#{header}") %></th>
13
+ <% end %>
14
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.events.processed.headers.actions") %></th>
15
+ </tr>
16
+ </thead>
17
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
18
+ <% now = Time.now %>
19
+ <% @events.each do |e| %>
20
+ <% claim = Pgbus::Web::EventState.processed(e, now: now, claim_window: @claim_window) %>
21
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50" data-processed-event data-state="<%= claim.state %>">
22
+ <td data-label="<%= t("pgbus.events.processed.headers.event_id") %>" class="px-4 py-3 text-sm font-mono">
23
+ <%= pgbus_link_to e["event_id"], pgbus.event_path(e["id"]), variant: :link, size: :sm, data: { turbo_frame: "_top" } %>
24
+ </td>
25
+ <td data-label="<%= t("pgbus.events.processed.headers.handler") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-200"><%= e["handler_class"] %></td>
26
+ <td data-label="<%= t("pgbus.events.processed.headers.state") %>" class="px-4 py-3 text-sm"><%= pgbus_processed_event_badge(claim) %></td>
27
+ <td data-label="<%= t("pgbus.events.processed.headers.reason") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-200"><%= pgbus_processed_event_reason(claim) %></td>
28
+ <td data-label="<%= t("pgbus.events.processed.headers.actions") %>" class="px-4 py-3 text-sm text-right">
29
+ <%# An unfinished claim's message is still in its queue: nothing to replay yet. %>
30
+ <% if claim.state == "completed" %>
31
+ <%= render "pgbus/events/replay", event: e, replay_state: @replay_states[e["id"]] %>
32
+ <% end %>
33
+ </td>
34
+ </tr>
35
+ <% end %>
36
+ <% if @events.empty? %>
37
+ <%= render "pgbus/shared/empty_row", colspan: 5, message: t("pgbus.events.processed.empty") %>
38
+ <% end %>
39
+ </tbody>
40
+ </table>
41
+ <%= render "pgbus/shared/pager", page: @processed_page, per_page: @per_page, shown: @events.size,
42
+ total_count: @processed_count.total, has_more: @processed_count.capped? && @events.size == @per_page,
43
+ page_path: ->(n) { events_list_path(processed_page: n) } %>
44
+ </div>
45
+ </turbo-frame>
@@ -0,0 +1,10 @@
1
+ <%# Replay for one processed event, or in words why there is none. Locals:
2
+ event (a pgbus_processed_events row), replay_state (:replayable,
3
+ :not_archived or :no_handler, from DataSource#event_replay_states). %>
4
+ <% if replay_state == :replayable %>
5
+ <%= pgbus_button_to t("pgbus.events.processed.replay"), pgbus.replay_event_path(event["id"]),
6
+ method: :post, variant: :link, size: :sm,
7
+ data: { turbo_confirm: t("pgbus.events.processed.replay_confirm"), turbo_frame: "_top" } %>
8
+ <% else %>
9
+ <span class="text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.events.processed.replay_unavailable.#{replay_state || :not_archived}") %></span>
10
+ <% end %>