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
@@ -3,7 +3,7 @@
3
3
  <h1 class="text-2xl font-bold text-gray-900 dark:text-white"><%= t("pgbus.insights.show.title") %></h1>
4
4
  <p class="mt-1 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.description_html", range: pgbus_time_range_label(@minutes)) %></p>
5
5
  </div>
6
- <nav class="inline-flex rounded-lg shadow-sm" role="group" aria-label="Time range">
6
+ <div class="inline-flex rounded-lg shadow-sm" role="group" aria-label="Time range">
7
7
  <% ranges = [["1h", 60], ["4h", 240], ["24h", 1440], ["7d", 10_080], ["30d", 43_200]] %>
8
8
  <% ranges.each_with_index do |(label, mins), i| %>
9
9
  <%
@@ -20,88 +20,85 @@
20
20
  %>
21
21
  <%= link_to label, pgbus.insights_path(minutes: mins), class: "#{base} #{colors} #{rounded}" %>
22
22
  <% end %>
23
- </nav>
23
+ </div>
24
24
  </div>
25
25
 
26
26
  <!-- Summary cards -->
27
27
  <div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-6 mb-8">
28
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
28
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
29
29
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.summary.total_jobs") %></dt>
30
30
  <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@summary[:total]) %></dd>
31
- </div>
32
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
31
+ </dl>
32
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
33
33
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.summary.succeeded") %></dt>
34
34
  <dd class="mt-1 text-2xl font-semibold text-green-600 dark:text-green-400"><%= pgbus_number(@summary[:success]) %></dd>
35
- </div>
36
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
35
+ </dl>
36
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
37
37
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.summary.failed") %></dt>
38
38
  <dd class="mt-1 text-2xl font-semibold text-red-600 dark:text-red-400"><%= pgbus_number(@summary[:failed]) %></dd>
39
- </div>
40
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
39
+ </dl>
40
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
41
41
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.summary.dead_lettered") %></dt>
42
42
  <dd class="mt-1 text-2xl font-semibold text-orange-600 dark:text-orange-400"><%= pgbus_number(@summary[:dead_lettered]) %></dd>
43
- </div>
44
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
43
+ </dl>
44
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
45
45
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.summary.avg_duration") %></dt>
46
46
  <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_ms_duration(@summary[:avg_duration_ms]) %></dd>
47
- </div>
48
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
47
+ </dl>
48
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
49
49
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.summary.max_duration") %></dt>
50
50
  <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_ms_duration(@summary[:max_duration_ms]) %></dd>
51
- </div>
51
+ </dl>
52
52
  </div>
53
53
 
54
54
  <% if @latency_available %>
55
55
  <!-- Latency summary cards -->
56
56
  <div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-5 mb-8">
57
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
57
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
58
58
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.summary.avg_latency") %></dt>
59
59
  <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_ms_duration(@summary[:avg_latency_ms]) %></dd>
60
- </div>
61
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
60
+ </dl>
61
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
62
62
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.summary.p50_latency") %></dt>
63
63
  <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_ms_duration(@summary[:p50_latency_ms]) %></dd>
64
- </div>
65
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
64
+ </dl>
65
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
66
66
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.summary.p95_latency") %></dt>
67
- <dd class="mt-1 text-2xl font-semibold text-yellow-600 dark:text-yellow-400"><%= pgbus_ms_duration(@summary[:p95_latency_ms]) %></dd>
68
- </div>
69
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
67
+ <dd class="mt-1 text-2xl font-semibold text-yellow-700 dark:text-yellow-400"><%= pgbus_ms_duration(@summary[:p95_latency_ms]) %></dd>
68
+ </dl>
69
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
70
70
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.summary.p99_latency") %></dt>
71
71
  <dd class="mt-1 text-2xl font-semibold text-orange-600 dark:text-orange-400"><%= pgbus_ms_duration(@summary[:p99_latency_ms]) %></dd>
72
- </div>
73
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
72
+ </dl>
73
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
74
74
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.summary.avg_retries") %></dt>
75
75
  <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= @summary[:avg_retries]&.round(2) || 0 %></dd>
76
- </div>
76
+ </dl>
77
77
  </div>
78
78
  <% end %>
79
79
 
80
80
  <!-- Charts -->
81
81
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
82
- <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
83
- <h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4"><%= t("pgbus.insights.show.charts.throughput") %></h3>
84
- <div id="throughput-chart" style="height: 280px;"></div>
85
- </div>
86
- <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
87
- <h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4"><%= t("pgbus.insights.show.charts.status_distribution") %></h3>
88
- <div id="status-chart" style="height: 280px;"></div>
89
- </div>
82
+ <%= render layout: "pgbus/insights/chart_card", locals: { id: "throughput-chart", title: t("pgbus.insights.show.charts.throughput"), empty: @charts[:throughput].empty?, caption: (pgbus_throughput_caption(@charts[:throughput], @minutes) unless @charts[:throughput].empty?) } do %>
83
+ <%= render "pgbus/insights/throughput_table" %>
84
+ <% end %>
85
+ <%= render layout: "pgbus/insights/chart_card", locals: { id: "status-chart", title: t("pgbus.insights.show.charts.status_distribution"), empty: @charts[:status].empty?, caption: (pgbus_status_caption(@charts[:status]) unless @charts[:status].empty?) } do %>
86
+ <%= render "pgbus/insights/status_table" %>
87
+ <% end %>
90
88
  </div>
91
89
 
92
90
  <% if @latency_available %>
93
91
  <!-- Latency chart -->
94
92
  <div class="grid grid-cols-1 gap-6 mb-8">
95
- <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
96
- <h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-4"><%= t("pgbus.insights.show.charts.latency") %></h3>
97
- <div id="latency-chart" style="height: 280px;"></div>
98
- </div>
93
+ <%= render layout: "pgbus/insights/chart_card", locals: { id: "latency-chart", title: t("pgbus.insights.show.charts.latency"), empty: @charts[:latency].empty?, caption: (pgbus_latency_caption(@summary) unless @charts[:latency].empty?) } do %>
94
+ <%= render "pgbus/insights/latency_table" %>
95
+ <% end %>
99
96
  </div>
100
97
 
101
98
  <!-- Latency by queue -->
102
99
  <div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700 mb-8">
103
100
  <div class="px-5 py-4 border-b border-gray-200 dark:border-gray-700">
104
- <h3 class="text-sm font-medium text-gray-700 dark:text-gray-300"><%= t("pgbus.insights.show.latency_by_queue.title") %></h3>
101
+ <h2 class="text-sm font-medium text-gray-700 dark:text-gray-300"><%= t("pgbus.insights.show.latency_by_queue.title") %></h2>
105
102
  </div>
106
103
  <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
107
104
  <thead class="bg-gray-50 dark:bg-gray-900">
@@ -115,14 +112,14 @@
115
112
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
116
113
  <% @latency_by_queue.each do |row| %>
117
114
  <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
118
- <td data-label="Queue" class="px-4 py-3 text-sm font-medium text-gray-700 dark:text-gray-300"><%= row[:queue_name] %></td>
119
- <td data-label="Count" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(row[:count]) %></td>
120
- <td data-label="Avg" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:avg_ms]) %></td>
121
- <td data-label="P95" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:p95_ms]) %></td>
115
+ <td data-label="<%= t("pgbus.insights.show.latency_by_queue.headers.queue") %>" class="px-4 py-3 text-sm font-medium text-gray-700 dark:text-gray-300"><%= row[:queue_name] %></td>
116
+ <td data-label="<%= t("pgbus.insights.show.latency_by_queue.headers.count") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(row[:count]) %></td>
117
+ <td data-label="<%= t("pgbus.insights.show.latency_by_queue.headers.avg") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:avg_ms]) %></td>
118
+ <td data-label="<%= t("pgbus.insights.show.latency_by_queue.headers.p95") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:p95_ms]) %></td>
122
119
  </tr>
123
120
  <% end %>
124
121
  <% if @latency_by_queue.empty? %>
125
- <tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500"><%= t("pgbus.insights.show.latency_by_queue.empty") %></td></tr>
122
+ <%= render "pgbus/shared/empty_row", colspan: 4, message: t("pgbus.insights.show.latency_by_queue.empty") %>
126
123
  <% end %>
127
124
  </tbody>
128
125
  </table>
@@ -132,7 +129,7 @@
132
129
  <!-- Slowest job classes -->
133
130
  <div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
134
131
  <div class="px-5 py-4 border-b border-gray-200 dark:border-gray-700">
135
- <h3 class="text-sm font-medium text-gray-700 dark:text-gray-300"><%= t("pgbus.insights.show.slowest.title") %></h3>
132
+ <h2 class="text-sm font-medium text-gray-700 dark:text-gray-300"><%= t("pgbus.insights.show.slowest.title") %></h2>
136
133
  </div>
137
134
  <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
138
135
  <thead class="bg-gray-50 dark:bg-gray-900">
@@ -146,14 +143,14 @@
146
143
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
147
144
  <% @slowest.each do |row| %>
148
145
  <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
149
- <td data-label="Job Class" class="px-4 py-3 text-sm font-medium text-gray-700 dark:text-gray-300"><%= row[:job_class] %></td>
150
- <td data-label="Count" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(row[:count]) %></td>
151
- <td data-label="Avg" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:avg_ms]) %></td>
152
- <td data-label="Max" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:max_ms]) %></td>
146
+ <td data-label="<%= t("pgbus.insights.show.slowest.headers.job_class") %>" class="px-4 py-3 text-sm font-medium text-gray-700 dark:text-gray-300"><%= row[:job_class] %></td>
147
+ <td data-label="<%= t("pgbus.insights.show.slowest.headers.count") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(row[:count]) %></td>
148
+ <td data-label="<%= t("pgbus.insights.show.slowest.headers.avg") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:avg_ms]) %></td>
149
+ <td data-label="<%= t("pgbus.insights.show.slowest.headers.max") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:max_ms]) %></td>
153
150
  </tr>
154
151
  <% end %>
155
152
  <% if @slowest.empty? %>
156
- <tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500"><%= t("pgbus.insights.show.slowest.empty") %></td></tr>
153
+ <%= render "pgbus/shared/empty_row", colspan: 4, message: t("pgbus.insights.show.slowest.empty") %>
157
154
  <% end %>
158
155
  </tbody>
159
156
  </table>
@@ -165,22 +162,22 @@
165
162
  <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4"><%= t("pgbus.insights.show.live_streams.title") %></h2>
166
163
 
167
164
  <div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 mb-6">
168
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
165
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
169
166
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.live_streams.broadcasts") %></dt>
170
167
  <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@live_stream_metrics[:totals][:broadcasts]) %></dd>
171
- </div>
172
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
168
+ </dl>
169
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
173
170
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.live_streams.active_connections") %></dt>
174
171
  <dd class="mt-1 text-2xl font-semibold text-green-600 dark:text-green-400"><%= pgbus_number(@live_stream_metrics[:totals][:active_connections]) %></dd>
175
- </div>
176
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
172
+ </dl>
173
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
177
174
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.live_streams.total_connections") %></dt>
178
175
  <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@live_stream_metrics[:totals][:total_connections]) %></dd>
179
- </div>
180
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
176
+ </dl>
177
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
181
178
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.live_streams.streams") %></dt>
182
179
  <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@live_stream_metrics[:totals][:streams]) %></dd>
183
- </div>
180
+ </dl>
184
181
  </div>
185
182
 
186
183
  <div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
@@ -199,14 +196,14 @@
199
196
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
200
197
  <% @live_stream_metrics[:streams].sort_by { |_name, data| -data[:broadcasts] }.each do |name, data| %>
201
198
  <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
202
- <td data-label="Stream" class="px-4 py-3 text-sm font-medium text-gray-700 dark:text-gray-300"><%= name %></td>
203
- <td data-label="Broadcasts" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(data[:broadcasts]) %></td>
204
- <td data-label="Active" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(data[:active_connections]) %></td>
205
- <td data-label="Total" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(data[:total_connections]) %></td>
199
+ <td data-label="<%= t("pgbus.insights.show.live_streams.stream") %>" class="px-4 py-3 text-sm font-medium text-gray-700 dark:text-gray-300"><%= name %></td>
200
+ <td data-label="<%= t("pgbus.insights.show.live_streams.broadcasts") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(data[:broadcasts]) %></td>
201
+ <td data-label="<%= t("pgbus.insights.show.live_streams.active_connections") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(data[:active_connections]) %></td>
202
+ <td data-label="<%= t("pgbus.insights.show.live_streams.total_connections") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(data[:total_connections]) %></td>
206
203
  </tr>
207
204
  <% end %>
208
205
  <% if @live_stream_metrics[:streams].empty? %>
209
- <tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500"><%= t("pgbus.insights.show.live_streams.empty") %></td></tr>
206
+ <%= render "pgbus/shared/empty_row", colspan: 4, message: t("pgbus.insights.show.live_streams.empty") %>
210
207
  <% end %>
211
208
  </tbody>
212
209
  </table>
@@ -220,26 +217,26 @@
220
217
  <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-4"><%= t("pgbus.insights.show.streams.title") %></h2>
221
218
 
222
219
  <div class="grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-5 mb-6">
223
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
220
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
224
221
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.streams.summary.broadcasts") %></dt>
225
222
  <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@stream_summary[:broadcasts]) %></dd>
226
- </div>
227
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
223
+ </dl>
224
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
228
225
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.streams.summary.connects") %></dt>
229
226
  <dd class="mt-1 text-2xl font-semibold text-green-600 dark:text-green-400"><%= pgbus_number(@stream_summary[:connects]) %></dd>
230
- </div>
231
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
227
+ </dl>
228
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
232
229
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.streams.summary.disconnects") %></dt>
233
230
  <dd class="mt-1 text-2xl font-semibold text-orange-600 dark:text-orange-400"><%= pgbus_number(@stream_summary[:disconnects]) %></dd>
234
- </div>
235
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
231
+ </dl>
232
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
236
233
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.streams.summary.active") %></dt>
237
234
  <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@stream_summary[:active_estimate]) %></dd>
238
- </div>
239
- <div class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
235
+ </dl>
236
+ <dl class="rounded-lg bg-white dark:bg-gray-800 p-4 shadow ring-1 ring-gray-200 dark:ring-gray-700">
240
237
  <dt class="text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.insights.show.streams.summary.avg_fanout") %></dt>
241
238
  <dd class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white"><%= @stream_summary[:avg_fanout]&.round(1) || 0 %></dd>
242
- </div>
239
+ </dl>
243
240
  </div>
244
241
 
245
242
  <div class="rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
@@ -258,14 +255,14 @@
258
255
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
259
256
  <% @top_streams.each do |row| %>
260
257
  <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
261
- <td data-label="Stream" class="px-4 py-3 text-sm font-medium text-gray-700 dark:text-gray-300"><%= row[:stream_name] %></td>
262
- <td data-label="Broadcasts" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(row[:count]) %></td>
263
- <td data-label="Avg Fanout" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= row[:avg_fanout]&.round(1) || 0 %></td>
264
- <td data-label="Avg Duration" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:avg_ms]) %></td>
258
+ <td data-label="<%= t("pgbus.insights.show.streams.top.headers.stream") %>" class="px-4 py-3 text-sm font-medium text-gray-700 dark:text-gray-300"><%= row[:stream_name] %></td>
259
+ <td data-label="<%= t("pgbus.insights.show.streams.top.headers.broadcasts") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_number(row[:count]) %></td>
260
+ <td data-label="<%= t("pgbus.insights.show.streams.top.headers.avg_fanout") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= row[:avg_fanout]&.round(1) || 0 %></td>
261
+ <td data-label="<%= t("pgbus.insights.show.streams.top.headers.avg_ms") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-700 dark:text-gray-300"><%= pgbus_ms_duration(row[:avg_ms]) %></td>
265
262
  </tr>
266
263
  <% end %>
267
264
  <% if @top_streams.empty? %>
268
- <tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500"><%= t("pgbus.insights.show.streams.top.empty") %></td></tr>
265
+ <%= render "pgbus/shared/empty_row", colspan: 4, message: t("pgbus.insights.show.streams.top.empty") %>
269
266
  <% end %>
270
267
  </tbody>
271
268
  </table>
@@ -273,31 +270,17 @@
273
270
  </div>
274
271
  <% end %>
275
272
 
273
+ <%# The chart series, embedded once (issue #492): charts.js reads them here
274
+ instead of fetching /pgbus/api/insights. json_escape keeps a value from
275
+ closing the tag; a data block needs no nonce. %>
276
+ <%= tag.script(json_escape(@chart_payload.to_json).html_safe, type: "application/json", id: "pgbus-insights-data") %> <%# herb:disable erb-no-unsafe-raw %>
277
+
276
278
  <script type="module" nonce="<%= content_security_policy_nonce %>">
277
279
  import { renderCharts, observeThemeChanges } from "charts";
278
280
 
279
- const i18n = {
280
- seriesName: <%= t("pgbus.insights.show.charts.series_name").to_json.html_safe %>,
281
- noData: <%= t("pgbus.insights.show.charts.no_data").to_json.html_safe %>,
282
- failedToLoad: <%= t("pgbus.insights.show.charts.failed_to_load").to_json.html_safe %>,
283
- latencyAvg: <%= t("pgbus.insights.show.charts.latency_avg").to_json.html_safe %>,
284
- latencyP95: <%= t("pgbus.insights.show.charts.latency_p95").to_json.html_safe %>,
285
- };
286
-
287
- let chartData = null;
288
-
289
- observeThemeChanges(() => chartData, i18n);
281
+ const i18n = <%= json_escape(pgbus_chart_i18n.to_json).html_safe %>;
282
+ const data = JSON.parse(document.getElementById("pgbus-insights-data").textContent);
290
283
 
291
- fetch(<%= pgbus.api_insights_path(minutes: @minutes).to_json.html_safe %>)
292
- .then(r => {
293
- if (!r.ok) throw new Error("HTTP " + r.status);
294
- return r.json();
295
- })
296
- .then(data => { chartData = data; renderCharts(data, i18n); })
297
- .catch(err => {
298
- const msg = '<p class="text-center text-sm text-gray-400 dark:text-gray-500 pt-24">' + i18n.failedToLoad + "</p>";
299
- document.querySelectorAll("#throughput-chart, #status-chart, #latency-chart").forEach(el => {
300
- el.innerHTML = msg;
301
- });
302
- });
284
+ renderCharts(data, i18n);
285
+ observeThemeChanges(() => data, i18n);
303
286
  </script>
@@ -0,0 +1,64 @@
1
+ <%# Unified job list (issue #489). Each row's state comes from SQL; JobState
2
+ adds the reason. A row's expansion is the <tr> after it, shown by CSS
3
+ (.pgbus-job-detail in tailwind.css) while the ID cell's <details> is open,
4
+ so the auto-refresh pause on `details[open]` keeps working.
5
+ Reads @list_path (->(extra) { url }) for the frame source and the pager,
6
+ so the queue page refreshes and pages on its own URL, and @list_scoped:
7
+ a list cut to one queue offers no "Discard all enqueued", which acts on
8
+ every queue. %>
9
+ <% selectable = @rows.any? { |r| r[:source] != "blocked" } %>
10
+ <% columns = selectable ? 8 : 7 %>
11
+ <turbo-frame id="jobs-list" data-auto-refresh data-turbo-action="advance"
12
+ data-src="<%= @list_path.call(page: (@page if @page > 1), frame: "list") %>">
13
+ <div data-bulk-page>
14
+ <div class="flex items-center justify-end mb-3 space-x-2">
15
+ <div data-bulk-actions class="hidden flex items-center space-x-2">
16
+ <span class="text-sm text-gray-500 dark:text-gray-400"><span data-bulk-count>0</span> <%= t("pgbus.helpers.bulk_selected") %></span>
17
+ <%= pgbus_button_tag t("pgbus.jobs.index.discard_selected"), variant: :danger, form: "bulk-discard-jobs-form",
18
+ confirm: t("pgbus.jobs.index.discard_selected_confirm") %>
19
+ </div>
20
+ <%# Blocked and orphaned rows are not queue messages: the action archives none of them. %>
21
+ <% if !@list_scoped && %w[ready scheduled running retrying].sum { |s| @counts[s] }.positive? %>
22
+ <%= pgbus_button_to t("pgbus.jobs.list.discard_all_enqueued"), pgbus.discard_all_enqueued_jobs_path, method: :post,
23
+ variant: :danger,
24
+ data: { turbo_confirm: t("pgbus.jobs.list.discard_all_enqueued_confirm"), turbo_frame: "_top" } %>
25
+ <% end %>
26
+ </div>
27
+ <%# Bulk form outside the table to avoid nested forms. Queue rows post
28
+ messages[]; rows backed by a failed event post ids[]. %>
29
+ <form id="bulk-discard-jobs-form" action="<%= pgbus.discard_selected_enqueued_jobs_path %>" method="post" data-turbo-frame="_top" class="hidden">
30
+ <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
31
+ </form>
32
+ <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="jobs">
33
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
34
+ <thead class="bg-gray-50 dark:bg-gray-900">
35
+ <tr>
36
+ <% if selectable %>
37
+ <th class="w-10 px-4 py-3">
38
+ <input type="checkbox" data-bulk-select-all
39
+ aria-label="<%= t("pgbus.helpers.bulk_select_all") %>"
40
+ class="h-4 w-4 rounded border-gray-300 text-indigo-600 dark:text-indigo-400 focus:ring-indigo-600">
41
+ </th>
42
+ <% end %>
43
+ <% %w[id job queue state attempts reason].each do |header| %>
44
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.jobs.list.headers.#{header}") %></th>
45
+ <% end %>
46
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.jobs.list.headers.actions") %></th>
47
+ </tr>
48
+ </thead>
49
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
50
+ <% @rows.each do |row| %>
51
+ <% job = Pgbus::Web::JobState.present(row, @state_context, ahead: @ahead[[row[:queue_name], row[:msg_id]]]) %>
52
+ <%= render "pgbus/jobs/row", row: row, job: job, selectable: selectable, columns: columns %>
53
+ <% end %>
54
+ <% if @rows.empty? %>
55
+ <%= render "pgbus/shared/empty_row", colspan: columns, message: t("pgbus.jobs.list.empty.#{@state}") %>
56
+ <% end %>
57
+ </tbody>
58
+ </table>
59
+ <%= render "pgbus/shared/pager", page: @page, per_page: @per_page, shown: @rows.size,
60
+ total_count: @counts[@state], has_more: @counts.capped?(@state) && @rows.size == @per_page,
61
+ page_path: ->(n) { @list_path.call(page: n) } %>
62
+ </div>
63
+ </div>
64
+ </turbo-frame>
@@ -0,0 +1,107 @@
1
+ <%# One job of the unified list plus its expansion row. Locals: row (a
2
+ DataSource#job_rows hash), job (its JobState::Result), selectable, columns. %>
3
+ <% payload = pgbus_parse_message(row[:payload]) %>
4
+ <% queue_row = row[:source] == "queue" %>
5
+ <% failed_id = row[:failed_event_id] %>
6
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50" data-job-row data-state="<%= job.state %>">
7
+ <% if selectable %>
8
+ <td class="w-10 px-4 py-3 align-top">
9
+ <% if failed_id %>
10
+ <input type="checkbox" name="ids[]" value="<%= failed_id %>" form="bulk-discard-jobs-form" data-bulk-item
11
+ aria-label="<%= t("pgbus.helpers.bulk_select_row", id: row[:msg_id] || failed_id) %>"
12
+ class="h-4 w-4 rounded border-gray-300 text-indigo-600 dark:text-indigo-400 focus:ring-indigo-600 mt-1">
13
+ <% elsif queue_row %>
14
+ <input type="checkbox" data-bulk-item data-pgbus-action="bulk-row-toggle"
15
+ aria-label="<%= t("pgbus.helpers.bulk_select_row", id: row[:msg_id]) %>"
16
+ class="h-4 w-4 rounded border-gray-300 text-indigo-600 dark:text-indigo-400 focus:ring-indigo-600 mt-1">
17
+ <input type="hidden" name="messages[][queue_name]" value="<%= row[:queue_name] %>" form="bulk-discard-jobs-form" disabled>
18
+ <input type="hidden" name="messages[][msg_id]" value="<%= row[:msg_id] %>" form="bulk-discard-jobs-form" disabled>
19
+ <% end %>
20
+ </td>
21
+ <% end %>
22
+ <td data-label="<%= t("pgbus.jobs.list.headers.id") %>" class="px-4 py-3 text-sm font-mono text-gray-900 dark:text-white align-top">
23
+ <details data-job-toggle class="group">
24
+ <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>
25
+ </details>
26
+ </td>
27
+ <td data-label="<%= t("pgbus.jobs.list.headers.job") %>" class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white align-top"><%= row[:job_class].presence || "—" %></td>
28
+ <td data-label="<%= t("pgbus.jobs.list.headers.queue") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-200 align-top"><%= row[:queue_name] %></td>
29
+ <td data-label="<%= t("pgbus.jobs.list.headers.state") %>" class="px-4 py-3 text-sm align-top"><%= pgbus_job_state_badge(job.state) %></td>
30
+ <td data-label="<%= t("pgbus.jobs.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.max_retries) %></td>
31
+ <td data-label="<%= t("pgbus.jobs.list.headers.reason") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-200 align-top" data-testid="job-reason">
32
+ <%= pgbus_job_reason(job) %>
33
+ </td>
34
+ <td data-label="<%= t("pgbus.jobs.list.headers.actions") %>" class="px-4 py-3 text-sm text-right align-top">
35
+ <div class="flex justify-end space-x-2">
36
+ <% if row[:source] == "blocked" %>
37
+ <%= pgbus_link_to t("pgbus.jobs.list.view_locks"), pgbus.locks_path,
38
+ variant: :link, size: :sm, data: { turbo_frame: "_top" } %>
39
+ <% elsif failed_id %>
40
+ <%# A retry attempt in progress (running) must not have its lease reset. %>
41
+ <% unless job.state == "running" %>
42
+ <%= pgbus_button_to t("pgbus.jobs.list.retry"), pgbus.retry_job_path(failed_id), method: :post,
43
+ variant: :link, size: :sm,
44
+ data: { turbo_frame: "_top" } %>
45
+ <% end %>
46
+ <%= pgbus_button_to t("pgbus.jobs.list.discard"), pgbus.discard_job_path(failed_id), method: :post,
47
+ variant: :link, size: :sm, tone: :red,
48
+ data: { turbo_confirm: t("pgbus.jobs.list.discard_confirm"), turbo_frame: "_top" } %>
49
+ <% else %>
50
+ <%# Resetting a running job's lease would run it twice: no Retry for it. %>
51
+ <% unless job.state == "running" %>
52
+ <%= pgbus_button_to t("pgbus.jobs.list.retry"), pgbus.retry_message_queue_path(name: row[:queue_name], msg_id: row[:msg_id]),
53
+ method: :post,
54
+ variant: :link, size: :sm,
55
+ data: { turbo_confirm: t("pgbus.jobs.list.retry_confirm"), turbo_frame: "_top" } %>
56
+ <% end %>
57
+ <%= pgbus_button_to t("pgbus.jobs.list.discard"), pgbus.discard_message_queue_path(name: row[:queue_name], msg_id: row[:msg_id]),
58
+ method: :post,
59
+ variant: :link, size: :sm, tone: :red,
60
+ data: { turbo_confirm: t("pgbus.jobs.list.discard_confirm"), turbo_frame: "_top" } %>
61
+ <% end %>
62
+ </div>
63
+ </td>
64
+ </tr>
65
+ <tr class="pgbus-job-detail" data-job-detail>
66
+ <td colspan="<%= columns %>" class="px-4 pb-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-100 dark:border-gray-700">
67
+ <div class="flex items-center justify-between mt-3 mb-3">
68
+ <span class="text-xs font-mono text-gray-500 dark:text-gray-400"><%= t("pgbus.jobs.list.job_id") %> <%= payload["job_id"] || "—" %></span>
69
+ <% if failed_id %>
70
+ <%= pgbus_link_to t("pgbus.jobs.list.view_failure"), pgbus.job_path(failed_id),
71
+ variant: :link, size: :sm, data: { turbo_frame: "_top" } %>
72
+ <% end %>
73
+ </div>
74
+ <% if row[:error_class] %>
75
+ <p class="mb-3 text-xs text-red-600 dark:text-red-400"><span class="font-medium"><%= row[:error_class] %></span>: <%= truncate(row[:error_message].to_s, length: 300) %></p>
76
+ <% end %>
77
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-3">
78
+ <div>
79
+ <span class="text-xs font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.jobs.list.arguments") %></span>
80
+ <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"] || []) rescue "—" %></pre>
81
+ </div>
82
+ <div>
83
+ <span class="text-xs font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.jobs.list.metadata") %></span>
84
+ <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">
85
+ <% if payload["priority"] %><p><strong><%= t("pgbus.jobs.list.metadata_labels.priority") %></strong> <%= payload["priority"] %></p><% end %>
86
+ <% if payload["locale"] %><p><strong><%= t("pgbus.jobs.list.metadata_labels.locale") %></strong> <%= payload["locale"] %></p><% end %>
87
+ <% if payload["timezone"] %><p><strong><%= t("pgbus.jobs.list.metadata_labels.timezone") %></strong> <%= payload["timezone"] %></p><% end %>
88
+ <% if payload["scheduled_at"] %><p><strong><%= t("pgbus.jobs.list.metadata_labels.scheduled") %></strong> <%= pgbus_timestamp(payload["scheduled_at"]) %></p><% end %>
89
+ <% if row[:enqueued_at] %><p><strong><%= t("pgbus.jobs.list.metadata_labels.enqueued") %></strong> <%= pgbus_timestamp(row[:enqueued_at]) %></p><% end %>
90
+ <% if row[:vt] %><p><strong><%= t("pgbus.jobs.list.metadata_labels.visible_at") %></strong> <%= pgbus_timestamp(row[:vt]) %></p><% end %>
91
+ <% if row[:last_read_at] %><p><strong><%= t("pgbus.jobs.list.metadata_labels.last_read") %></strong> <%= pgbus_timestamp(row[:last_read_at]) %></p><% end %>
92
+ <% if row[:concurrency_key] %><p><strong><%= t("pgbus.jobs.list.metadata_labels.concurrency_key") %></strong> <%= row[:concurrency_key] %></p><% end %>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ <details class="mt-2">
97
+ <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.jobs.list.full_json_payload") %></summary>
98
+ <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 row[:payload] %></pre>
99
+ </details>
100
+ <% if row[:headers] %>
101
+ <details class="mt-2">
102
+ <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.jobs.list.headers_section") %></summary>
103
+ <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>
104
+ </details>
105
+ <% end %>
106
+ </td>
107
+ </tr>
@@ -0,0 +1,14 @@
1
+ <%# State tabs of the unified job list. Locals: state (the active tab),
2
+ counts (DataSource#job_state_counts), list_path (->(extra) { url }, so
3
+ the queue page keeps its tabs on its own URL). %>
4
+ <nav class="mb-4 flex flex-wrap gap-2" aria-label="<%= t("pgbus.jobs.list.tabs_label") %>" data-testid="job-state-tabs">
5
+ <% Pgbus::JobListing::STATES.each do |tab| %>
6
+ <% active = tab == state %>
7
+ <a href="<%= list_path.call(state: (tab unless tab == "all"), page: nil) %>"
8
+ data-state="<%= tab %>" aria-current="<%= active ? "page" : "false" %>"
9
+ 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' %>">
10
+ <span><%= t("pgbus.jobs.list.tabs.#{tab}") %></span>
11
+ <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>
12
+ </a>
13
+ <% end %>
14
+ </nav>
@@ -1,16 +1,16 @@
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.jobs.index.title") %></h1>
3
- <% if @failed.any? %>
4
- <div class="flex space-x-2">
5
- <%= button_to t("pgbus.jobs.index.retry_all"), pgbus.retry_all_jobs_path, method: :post,
6
- class: "rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-500",
3
+ <div class="flex space-x-2">
4
+ <%# Retry All / Discard All act on failed events in every queue, not just a filtered one. %>
5
+ <% if @failed_total.positive? %>
6
+ <%= pgbus_button_to t("pgbus.jobs.index.retry_all"), pgbus.retry_all_jobs_path, method: :post, variant: :primary,
7
7
  data: { turbo_confirm: t("pgbus.jobs.index.retry_all_confirm") } %>
8
- <%= button_to t("pgbus.jobs.index.discard_all"), pgbus.discard_all_jobs_path, method: :post,
9
- class: "rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500",
8
+ <%= pgbus_button_to t("pgbus.jobs.index.discard_all"), pgbus.discard_all_jobs_path, method: :post, variant: :danger,
10
9
  data: { turbo_confirm: t("pgbus.jobs.index.discard_all_confirm") } %>
11
- </div>
12
- <% end %>
10
+ <% end %>
11
+ </div>
13
12
  </div>
14
13
 
15
- <%= render "pgbus/jobs/failed_table" %>
16
- <%= render "pgbus/jobs/enqueued_table" %>
14
+ <%= render "pgbus/jobs/tabs", state: @state, counts: @counts, list_path: @list_path %>
15
+
16
+ <%= render "pgbus/jobs/list" %>