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,162 @@
1
+ <%# One pending event plus its expansion row (issue #494). Locals: row (a
2
+ DataSource#event_rows hash), event (its EventState::Result), selectable,
3
+ columns.
4
+ The raw message is kept so a corrupt payload stays visible in the edit
5
+ textarea and the full-JSON panel: pgbus_parse_message returns {} for
6
+ unparseable JSON, which would hide exactly what the operator is repairing.
7
+ Scalar JSON (numbers, arrays) is coerced to {} for hash access. %>
8
+ <% payload_raw = pgbus_parse_message(row[:payload]) %>
9
+ <% payload = payload_raw.is_a?(Hash) ? payload_raw : {} %>
10
+ <% parse_ok = row[:payload].blank? || payload_raw != {} || row[:payload].to_s.strip == "{}" %>
11
+ <% queue_row = row[:source] == "queue" %>
12
+ <% failed_id = row[:failed_event_id] %>
13
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50" data-event-row data-state="<%= event.state %>" data-msg-id="<%= row[:msg_id] %>">
14
+ <% if selectable %>
15
+ <td class="w-10 px-4 py-3 align-top">
16
+ <% if queue_row %>
17
+ <input type="checkbox" data-bulk-item data-pgbus-action="bulk-row-toggle"
18
+ aria-label="<%= t("pgbus.helpers.bulk_select_row", id: row[:msg_id]) %>"
19
+ class="h-4 w-4 rounded border-gray-300 text-indigo-600 dark:text-indigo-400 focus:ring-indigo-600 mt-1">
20
+ <input type="hidden" name="messages[][queue_name]" value="<%= row[:queue_name] %>" form="bulk-discard-events-form" disabled>
21
+ <input type="hidden" name="messages[][msg_id]" value="<%= row[:msg_id] %>" form="bulk-discard-events-form" disabled>
22
+ <% end %>
23
+ </td>
24
+ <% end %>
25
+ <td data-label="<%= t("pgbus.events.list.headers.id") %>" class="px-4 py-3 text-sm font-mono text-gray-900 dark:text-white align-top">
26
+ <details data-job-toggle class="group">
27
+ <summary class="cursor-pointer list-none text-indigo-600 hover:text-indigo-800 dark:hover:text-indigo-300 dark:text-indigo-400"><%= row[:msg_id] || row[:id] %></summary>
28
+ </details>
29
+ </td>
30
+ <td data-label="<%= t("pgbus.events.list.headers.event") %>" class="px-4 py-3 text-sm align-top">
31
+ <span class="block font-mono text-gray-900 dark:text-white"><%= pgbus_event_routing_key(row[:payload]) %></span>
32
+ <span class="block text-xs font-mono text-gray-500 dark:text-gray-400"><%= payload["event_id"] || "—" %></span>
33
+ </td>
34
+ <td data-label="<%= t("pgbus.events.list.headers.handler") %>" class="px-4 py-3 text-sm align-top">
35
+ <span class="block font-medium text-gray-900 dark:text-white"><%= row[:handler_class] || "—" %></span>
36
+ <span class="block text-xs text-gray-500 dark:text-gray-400"><%= row[:queue_name] %></span>
37
+ </td>
38
+ <td data-label="<%= t("pgbus.events.list.headers.state") %>" class="px-4 py-3 text-sm align-top"><%= pgbus_event_state_badge(event.state) %></td>
39
+ <td data-label="<%= t("pgbus.events.list.headers.attempts") %>" class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 align-top"><%= pgbus_job_attempts(row, @state_context.jobs.max_retries) %></td>
40
+ <td data-label="<%= t("pgbus.events.list.headers.reason") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-200 align-top" data-testid="event-reason">
41
+ <%= pgbus_event_reason(event) %>
42
+ </td>
43
+ <td data-label="<%= t("pgbus.events.list.headers.actions") %>" class="px-4 py-3 text-sm text-right align-top">
44
+ <div class="flex justify-end space-x-2">
45
+ <% if queue_row %>
46
+ <%# Marking a running event handled would race the handler still running it. %>
47
+ <% unless event.state == "running" %>
48
+ <%# handler_class is resolved server-side from the subscriber registry. %>
49
+ <%= pgbus_button_to t("pgbus.events.list.mark_handled"),
50
+ pgbus.mark_handled_event_path(row[:msg_id], queue_name: row[:queue_name]),
51
+ method: :post, variant: :link, size: :sm, tone: :green,
52
+ data: { turbo_confirm: t("pgbus.events.list.mark_handled_confirm"), turbo_frame: "_top" } %>
53
+ <% end %>
54
+ <%= pgbus_button_to t("pgbus.events.list.discard"),
55
+ pgbus.discard_event_path(row[:msg_id], queue_name: row[:queue_name]),
56
+ method: :post, variant: :link, size: :sm, tone: :red,
57
+ data: { turbo_confirm: t("pgbus.events.list.discard_confirm"), turbo_frame: "_top" } %>
58
+ <% elsif failed_id %>
59
+ <%= pgbus_button_to t("pgbus.events.list.discard"), pgbus.discard_job_path(failed_id),
60
+ method: :post, variant: :link, size: :sm, tone: :red,
61
+ data: { turbo_confirm: t("pgbus.events.list.discard_failure_confirm"), turbo_frame: "_top" } %>
62
+ <% end %>
63
+ </div>
64
+ </td>
65
+ </tr>
66
+ <tr class="pgbus-job-detail" data-event-detail>
67
+ <td colspan="<%= columns %>" class="px-4 pb-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-100 dark:border-gray-700">
68
+ <div class="flex items-center justify-between mt-3 mb-3">
69
+ <span class="text-xs font-mono text-gray-500 dark:text-gray-400"><%= t("pgbus.events.list.event_id") %> <%= payload["event_id"] || "—" %></span>
70
+ <div class="flex items-center space-x-2">
71
+ <% if failed_id %>
72
+ <%= pgbus_link_to t("pgbus.events.list.view_failure"), pgbus.job_path(failed_id),
73
+ variant: :link, size: :sm, data: { turbo_frame: "_top" } %>
74
+ <% end %>
75
+ <% if queue_row %>
76
+ <details class="relative inline-block">
77
+ <summary class="<%= pgbus_button_classes(:warning, :sm, extra: "cursor-pointer list-none") %>">
78
+ <%= t("pgbus.events.list.reroute") %>
79
+ </summary>
80
+ <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">
81
+ <p class="text-xs text-gray-500 dark:text-gray-400 mb-2"><%= t("pgbus.events.list.reroute_label") %></p>
82
+ <% @subscribers.each do |s| %>
83
+ <% next if s[:physical_queue_name] == row[:queue_name] %>
84
+ <%= pgbus_button_to s[:handler_class],
85
+ pgbus.reroute_event_path(row[:msg_id], queue_name: row[:queue_name], target_queue: s[:physical_queue_name]),
86
+ method: :post, variant: :menu, size: :sm,
87
+ data: { turbo_confirm: t("pgbus.events.list.reroute_confirm"), turbo_frame: "_top" } %>
88
+ <% end %>
89
+ </div>
90
+ </details>
91
+ <% end %>
92
+ </div>
93
+ </div>
94
+ <% if row[:error_class] %>
95
+ <p class="mb-3 text-xs text-red-700 dark:text-red-400"><span class="font-medium"><%= row[:error_class] %></span>: <%= truncate(row[:error_message].to_s, length: 300) %></p>
96
+ <% end %>
97
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-3">
98
+ <div>
99
+ <span class="text-xs font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.events.list.payload") %></span>
100
+ <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"><%= parse_ok ? (JSON.pretty_generate(payload["payload"] || payload_raw) rescue "—") : row[:payload] %></pre>
101
+ </div>
102
+ <div>
103
+ <span class="text-xs font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.events.list.metadata") %></span>
104
+ <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">
105
+ <% unless row[:read_ct].nil? %><p><strong><%= t("pgbus.events.list.metadata_labels.read_count") %></strong> <%= row[:read_ct] %></p><% end %>
106
+ <% if payload["published_at"] %><p><strong><%= t("pgbus.events.list.metadata_labels.published") %></strong> <%= pgbus_timestamp(payload["published_at"]) %></p><% end %>
107
+ <% if row[:enqueued_at] %><p><strong><%= t("pgbus.events.list.metadata_labels.enqueued") %></strong> <%= pgbus_timestamp(row[:enqueued_at]) %></p><% end %>
108
+ <% if row[:vt] %><p><strong><%= t("pgbus.events.list.metadata_labels.visible_at") %></strong> <%= pgbus_timestamp(row[:vt]) %></p><% end %>
109
+ <% if row[:last_read_at] %><p><strong><%= t("pgbus.events.list.metadata_labels.last_read") %></strong> <%= pgbus_timestamp(row[:last_read_at]) %></p><% end %>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ <%# Persisted Current attributes (issue #431) %>
114
+ <% if (event_context = pgbus_job_context(row[:payload])) %>
115
+ <div class="mb-3" data-testid="job-context">
116
+ <span class="text-xs font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.events.list.context") %></span>
117
+ <div class="text-xs text-gray-600 dark:text-gray-300 bg-white dark:bg-gray-800 rounded p-2 mt-1 space-y-2">
118
+ <% event_context.each do |klass_name, attrs| %>
119
+ <div>
120
+ <p class="font-semibold text-gray-700 dark:text-gray-300 font-mono"><%= klass_name %></p>
121
+ <dl class="grid grid-cols-1 gap-x-4 gap-y-0.5 sm:grid-cols-2">
122
+ <% attrs.each do |name, value| %>
123
+ <div class="flex gap-2">
124
+ <dt class="text-gray-500 dark:text-gray-400 font-mono"><%= name %></dt>
125
+ <dd class="text-gray-900 dark:text-white font-mono break-all"><%= value %></dd>
126
+ </div>
127
+ <% end %>
128
+ </dl>
129
+ </div>
130
+ <% end %>
131
+ </div>
132
+ </div>
133
+ <% end %>
134
+ <% if queue_row %>
135
+ <details class="mt-2">
136
+ <summary class="text-xs font-medium text-indigo-600 dark:text-indigo-400 cursor-pointer hover:text-indigo-800 dark:hover:text-indigo-300"><%= t("pgbus.events.list.edit_payload") %></summary>
137
+ <div class="mt-2 p-3 bg-white dark:bg-gray-800 rounded ring-1 ring-gray-200 dark:ring-gray-700">
138
+ <%= form_with url: pgbus.edit_payload_event_path(row[:msg_id]), method: :post, local: true, data: { turbo_frame: "_top" } do |f| %>
139
+ <%= hidden_field_tag :queue_name, row[:queue_name], id: nil %>
140
+ <label for="event-payload-<%= row[:msg_id] %>" class="text-xs font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.events.list.edit_payload_label") %></label>
141
+ <textarea id="event-payload-<%= row[:msg_id] %>" name="payload" rows="6"
142
+ 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 row[:payload]) : row[:payload] %></textarea>
143
+ <div class="mt-2 flex justify-end">
144
+ <%= pgbus_button_tag t("pgbus.events.list.edit_payload"), variant: :primary, size: :sm,
145
+ confirm: t("pgbus.events.list.edit_payload_confirm") %>
146
+ </div>
147
+ <% end %>
148
+ </div>
149
+ </details>
150
+ <% end %>
151
+ <details class="mt-2">
152
+ <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.events.list.full_json_payload") %></summary>
153
+ <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"><%= parse_ok ? (JSON.pretty_generate(payload_raw) rescue row[:payload]) : row[:payload] %></pre>
154
+ </details>
155
+ <% if row[:headers] %>
156
+ <details class="mt-2">
157
+ <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.events.list.headers_section") %></summary>
158
+ <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(row[:headers])) rescue row[:headers] %></pre>
159
+ </details>
160
+ <% end %>
161
+ </td>
162
+ </tr>
@@ -0,0 +1,13 @@
1
+ <%# State tabs of the pending-events list (issue #494), the jobs list's tabs
2
+ in event words. A tab keeps the queue filter and the processed page. %>
3
+ <nav class="mb-4 flex flex-wrap gap-2" aria-label="<%= t("pgbus.events.list.tabs_label") %>" data-testid="event-state-tabs">
4
+ <% Pgbus::EventsController::STATES.each do |tab| %>
5
+ <% active = tab == @state %>
6
+ <a href="<%= events_list_path(state: (tab unless tab == "all"), page: nil) %>"
7
+ data-state="<%= tab %>" aria-current="<%= active ? "page" : "false" %>"
8
+ 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' %>">
9
+ <span><%= t("pgbus.events.list.tabs.#{tab}") %></span>
10
+ <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' %>"><%= pgbus_job_count(@counts, tab) %></span>
11
+ </a>
12
+ <% end %>
13
+ </nav>
@@ -1,77 +1,56 @@
1
1
  <div class="flex items-center justify-between mb-6">
2
2
  <h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.events.index.title") %></h1>
3
- <% if @pending.any? %>
4
- <div class="flex items-center space-x-2">
5
- <div data-bulk-actions class="hidden flex items-center space-x-2">
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-events-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.events.index.discard_selected_confirm") %>">
10
- <%= t("pgbus.events.index.discard_selected") %>
11
- </button>
12
- </div>
13
- </div>
14
- <% end %>
15
3
  </div>
16
4
 
17
- <!-- Registered Subscribers -->
5
+ <%# Registered subscribers, each with whether a running consumer drains it. %>
18
6
  <div class="mb-8">
19
7
  <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3"><%= t("pgbus.events.index.subscribers_title") %></h2>
20
- <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
8
+ <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700" data-testid="subscribers">
21
9
  <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
22
10
  <thead class="bg-gray-50 dark:bg-gray-900">
23
11
  <tr>
24
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.subscribers_headers.pattern") %></th>
25
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.subscribers_headers.handler") %></th>
26
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.subscribers_headers.queue") %></th>
12
+ <% %w[pattern handler queue consumer pending].each do |header| %>
13
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.events.index.subscribers_headers.#{header}") %></th>
14
+ <% end %>
27
15
  </tr>
28
16
  </thead>
29
17
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
18
+ <% covered = @state_context.covered_queues %>
30
19
  <% @subscribers.each do |s| %>
31
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900">
32
- <td data-label="Pattern" class="px-4 py-3 text-sm font-mono text-indigo-600"><%= s[:pattern] %></td>
33
- <td data-label="Handler" class="px-4 py-3 text-sm text-gray-900 dark:text-white"><%= s[:handler_class] %></td>
34
- <td data-label="Queue" class="px-4 py-3 text-sm text-gray-500"><%= s[:queue_name] %></td>
20
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
21
+ <td data-label="<%= t("pgbus.events.index.subscribers_headers.pattern") %>" class="px-4 py-3 text-sm font-mono text-indigo-700 dark:text-indigo-300"><%= s[:pattern] %></td>
22
+ <td data-label="<%= t("pgbus.events.index.subscribers_headers.handler") %>" class="px-4 py-3 text-sm text-gray-900 dark:text-white"><%= s[:handler_class] %></td>
23
+ <td data-label="<%= t("pgbus.events.index.subscribers_headers.queue") %>" class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400"><%= s[:queue_name] %></td>
24
+ <td data-label="<%= t("pgbus.events.index.subscribers_headers.consumer") %>" class="px-4 py-3 text-sm">
25
+ <% if covered.nil? %>
26
+ <span class="text-gray-600 dark:text-gray-300"><%= t("pgbus.events.index.subscribers.unknown") %></span>
27
+ <% elsif covered.include?(s[:physical_queue_name]) %>
28
+ <span class="text-green-700 dark:text-green-400"><%= t("pgbus.events.index.subscribers.covered") %></span>
29
+ <% else %>
30
+ <span class="text-yellow-800 dark:text-yellow-300"><%= t("pgbus.events.index.subscribers.uncovered") %></span>
31
+ <% end %>
32
+ </td>
33
+ <td data-label="<%= t("pgbus.events.index.subscribers_headers.pending") %>" class="px-4 py-3 text-sm">
34
+ <%= pgbus_link_to t("pgbus.events.index.subscribers.view_pending"), pgbus.events_path(queue: s[:physical_queue_name]),
35
+ variant: :link, size: :sm %>
36
+ </td>
35
37
  </tr>
36
38
  <% end %>
37
39
  <% if @subscribers.empty? %>
38
- <tr><td colspan="3" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.events.index.subscribers_empty") %></td></tr>
40
+ <%= render "pgbus/shared/empty_row", colspan: 5, message: t("pgbus.events.index.subscribers_empty") %>
39
41
  <% end %>
40
42
  </tbody>
41
43
  </table>
42
44
  </div>
43
45
  </div>
44
46
 
45
- <!-- Pending Events (Unprocessed messages in handler queues) -->
46
47
  <div class="mb-8">
47
48
  <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3"><%= t("pgbus.events.index.pending_title") %></h2>
48
- <%= render "pgbus/events/pending_table" %>
49
+ <%= render "pgbus/events/tabs" %>
50
+ <%= render "pgbus/events/list" %>
49
51
  </div>
50
52
 
51
- <!-- Processed Events (Audit Trail) -->
52
53
  <div>
53
54
  <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3"><%= t("pgbus.events.index.processed_title") %></h2>
54
- <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
55
- <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
56
- <thead class="bg-gray-50 dark:bg-gray-900">
57
- <tr>
58
- <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>
59
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.processed_headers.handler") %></th>
60
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.events.index.processed_headers.processed_at") %></th>
61
- </tr>
62
- </thead>
63
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
64
- <% @events.each do |e| %>
65
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900">
66
- <td data-label="Event ID" class="px-4 py-3 text-sm font-mono text-gray-900 dark:text-white"><%= e["event_id"] %></td>
67
- <td data-label="Handler" class="px-4 py-3 text-sm text-gray-700"><%= e["handler_class"] %></td>
68
- <td data-label="Processed At" class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(e["processed_at"]) %></td>
69
- </tr>
70
- <% end %>
71
- <% if @events.empty? %>
72
- <tr><td colspan="3" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.events.index.processed_empty") %></td></tr>
73
- <% end %>
74
- </tbody>
75
- </table>
76
- </div>
55
+ <%= render "pgbus/events/processed_table" %>
77
56
  </div>
@@ -1,28 +1,46 @@
1
1
  <div class="mb-6">
2
- <%= link_to "\u2190 #{t("pgbus.events.show.back")}", pgbus.events_path, class: "text-sm text-indigo-600 hover:text-indigo-500" %>
2
+ <%= pgbus_link_to "← #{t("pgbus.events.show.back")}", pgbus.events_path %>
3
3
  </div>
4
4
 
5
5
  <% if @event %>
6
- <h1 class="text-2xl font-bold text-gray-900 dark:text-white mb-6"><%= t("pgbus.events.show.title", event_id: @event["event_id"]) %></h1>
6
+ <div class="flex items-center justify-between mb-6">
7
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.events.show.title", event_id: @event["event_id"]) %></h1>
8
+ <% if @claim.state == "completed" %>
9
+ <%= render "pgbus/events/replay", event: @event, replay_state: @replay_state %>
10
+ <% end %>
11
+ </div>
7
12
 
8
13
  <div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700 p-6">
9
14
  <dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
10
15
  <div>
11
- <dt class="text-sm font-medium text-gray-500"><%= t("pgbus.events.show.labels.event_id") %></dt>
16
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.events.show.labels.event_id") %></dt>
12
17
  <dd class="text-sm font-mono text-gray-900 dark:text-white"><%= @event["event_id"] %></dd>
13
18
  </div>
14
19
  <div>
15
- <dt class="text-sm font-medium text-gray-500"><%= t("pgbus.events.show.labels.handler") %></dt>
20
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.events.show.labels.handler") %></dt>
16
21
  <dd class="text-sm text-gray-900 dark:text-white"><%= @event["handler_class"] %></dd>
17
22
  </div>
18
23
  <div>
19
- <dt class="text-sm font-medium text-gray-500"><%= t("pgbus.events.show.labels.processed_at") %></dt>
20
- <dd class="text-sm text-gray-900 dark:text-white"><%= @event["processed_at"] %></dd>
24
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.events.show.labels.state") %></dt>
25
+ <dd class="text-sm text-gray-900 dark:text-white">
26
+ <%= pgbus_processed_event_badge(@claim) %>
27
+ <span class="ml-2"><%= pgbus_processed_event_reason(@claim) %></span>
28
+ </dd>
29
+ </div>
30
+ <div>
31
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.events.show.labels.processed_at") %></dt>
32
+ <dd class="text-sm text-gray-900 dark:text-white"><%= pgbus_timestamp(@event["processed_at"]) %></dd>
21
33
  </div>
34
+ <% if @event["completed_at"] %>
35
+ <div>
36
+ <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.events.show.labels.completed_at") %></dt>
37
+ <dd class="text-sm text-gray-900 dark:text-white"><%= pgbus_timestamp(@event["completed_at"]) %></dd>
38
+ </div>
39
+ <% end %>
22
40
  </dl>
23
41
  </div>
24
42
  <% else %>
25
43
  <div class="text-center py-12">
26
- <p class="text-gray-400"><%= t("pgbus.events.show.not_found") %></p>
44
+ <p class="text-gray-500 dark:text-gray-400"><%= t("pgbus.events.show.not_found") %></p>
27
45
  </div>
28
46
  <% end %>
@@ -0,0 +1,21 @@
1
+ <%# One Insights chart (issue #492): the caption says what it shows, the
2
+ chart div is one img for assistive tech (role + the caption as its name)
3
+ and the yielded data table is the full alternative. Locals: id, title,
4
+ caption, empty. Without data the card says so instead of drawing. %>
5
+ <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
6
+ <h2 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4"><%= title %></h2>
7
+ <% if empty %>
8
+ <p data-testid="chart-empty" class="py-24 text-center text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.charts.no_data") %></p>
9
+ <% else %>
10
+ <figure>
11
+ <div id="<%= id %>" role="img" aria-label="<%= strip_tags(caption).to_str %>" style="height: 280px;"></div>
12
+ <figcaption class="mt-3 text-sm text-gray-700 dark:text-gray-300"><%= caption %></figcaption>
13
+ <details class="mt-3">
14
+ <summary class="inline-flex min-h-6 cursor-pointer items-center text-sm font-medium text-indigo-700 hover:text-indigo-900 dark:text-indigo-300 dark:hover:text-indigo-200"><%= t("pgbus.insights.show.charts.data_table") %></summary>
15
+ <div class="mt-2 overflow-hidden rounded-lg ring-1 ring-gray-200 dark:ring-gray-700">
16
+ <%= yield %>
17
+ </div>
18
+ </details>
19
+ </figure>
20
+ <% end %>
21
+ </div>
@@ -0,0 +1,20 @@
1
+ <% chart = @charts[:latency] %>
2
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
3
+ <caption class="sr-only"><%= chart.step_minutes == 1 ? t("pgbus.insights.show.tables.captions.latency_minute") : t("pgbus.insights.show.tables.captions.latency_step", step: pgbus_duration(chart.step_minutes * 60)) %></caption>
4
+ <thead class="bg-gray-50 dark:bg-gray-900">
5
+ <tr>
6
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.tables.headers.from") %></th>
7
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.tables.headers.avg") %></th>
8
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.tables.headers.highest_p95") %></th>
9
+ </tr>
10
+ </thead>
11
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
12
+ <% chart.rows.each do |row| %>
13
+ <tr>
14
+ <td data-label="<%= t("pgbus.insights.show.tables.headers.from") %>" class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300"><%= pgbus_absolute_time(row[:from]) %></td>
15
+ <td data-label="<%= t("pgbus.insights.show.tables.headers.avg") %>" class="px-4 py-2 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:avg_ms]) %></td>
16
+ <td data-label="<%= t("pgbus.insights.show.tables.headers.highest_p95") %>" class="px-4 py-2 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:p95_ms]) %></td>
17
+ </tr>
18
+ <% end %>
19
+ </tbody>
20
+ </table>
@@ -0,0 +1,19 @@
1
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
2
+ <caption class="sr-only"><%= t("pgbus.insights.show.tables.captions.status") %></caption>
3
+ <thead class="bg-gray-50 dark:bg-gray-900">
4
+ <tr>
5
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.tables.headers.status") %></th>
6
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.tables.headers.jobs") %></th>
7
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.tables.headers.share") %></th>
8
+ </tr>
9
+ </thead>
10
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
11
+ <% @charts[:status].rows.each do |row| %>
12
+ <tr>
13
+ <td data-label="<%= t("pgbus.insights.show.tables.headers.status") %>" class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300"><%= pgbus_job_stat_status(row[:status]) %></td>
14
+ <td data-label="<%= t("pgbus.insights.show.tables.headers.jobs") %>" class="px-4 py-2 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= number_with_delimiter(row[:count]) %></td>
15
+ <td data-label="<%= t("pgbus.insights.show.tables.headers.share") %>" class="px-4 py-2 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_percent(row[:share]) %></td>
16
+ </tr>
17
+ <% end %>
18
+ </tbody>
19
+ </table>
@@ -0,0 +1,18 @@
1
+ <% chart = @charts[:throughput] %>
2
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
3
+ <caption class="sr-only"><%= chart.step_minutes == 1 ? t("pgbus.insights.show.tables.captions.throughput_minute") : t("pgbus.insights.show.tables.captions.throughput_step", step: pgbus_duration(chart.step_minutes * 60)) %></caption>
4
+ <thead class="bg-gray-50 dark:bg-gray-900">
5
+ <tr>
6
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.tables.headers.from") %></th>
7
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.tables.headers.jobs") %></th>
8
+ </tr>
9
+ </thead>
10
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
11
+ <% chart.rows.each do |row| %>
12
+ <tr>
13
+ <td data-label="<%= t("pgbus.insights.show.tables.headers.from") %>" class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300"><%= pgbus_absolute_time(row[:from]) %></td>
14
+ <td data-label="<%= t("pgbus.insights.show.tables.headers.jobs") %>" class="px-4 py-2 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= number_with_delimiter(row[:count]) %></td>
15
+ </tr>
16
+ <% end %>
17
+ </tbody>
18
+ </table>