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
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgbus
4
+ # The Processes page's words (issue #493): ProcessState results as
5
+ # sentences. Moments go through pgbus_time, seconds through pgbus_duration,
6
+ # counts through number_with_delimiter.
7
+ module ProcessesHelper
8
+ PART_SEPARATOR = " · "
9
+
10
+ # Metadata keys the row always renders (status, activity, recycle limits,
11
+ # Process cell, uptime), left out of the expansion's raw chips. Counters
12
+ # and RSS stay: Recycle shows them only when a limit is configured.
13
+ RENDERED_METADATA_KEYS = %w[
14
+ rates in_flight loop_tick_at buffered capacity busy started_at supervisor_pid max_jobs max_memory_mb
15
+ max_lifetime queues topics tasks threads capsule process pid hostname
16
+ ].freeze
17
+
18
+ SECONDS_ARGS = %i[limit within interval age].freeze
19
+ NUMBER_ARGS = %i[number busy capacity].freeze
20
+
21
+ # "Loop last ticked 2m ago (limit 1m 30s) — the supervisor's watchdog
22
+ # restarts it". The _html key lets the <time> through.
23
+ def pgbus_process_status(result)
24
+ reason = translate("pgbus.processes.state.reasons.#{result.reason_key}_html", **pgbus_process_args(result.reason_args))
25
+ return reason unless result.next_key
26
+
27
+ safe_join([reason, " — ", t("pgbus.processes.state.next.#{result.next_key}", **pgbus_process_args(result.next_args))])
28
+ end
29
+
30
+ def pgbus_process_activity(result)
31
+ parts = result.activity.map do |key, args|
32
+ next t("pgbus.processes.state.activity.supervises", children: pgbus_process_children(args[:children])) if key == "supervises"
33
+
34
+ t("pgbus.processes.state.activity.#{key}", **pgbus_process_args(args))
35
+ end
36
+ pgbus_process_join(parts)
37
+ end
38
+
39
+ def pgbus_process_recycle(result)
40
+ parts = Array(result.recycle).map do |key, args|
41
+ values = if key == "lifetime"
42
+ { uptime: pgbus_duration(args[:uptime]), limit: pgbus_duration(args[:limit]) }
43
+ else
44
+ args.transform_values { |v| v.nil? ? Web::TimeFormat::NONE : number_with_delimiter(v) }
45
+ end
46
+ t("pgbus.processes.state.recycle.#{key}", **values)
47
+ end
48
+ pgbus_process_join(parts)
49
+ end
50
+
51
+ def pgbus_process_raw_metadata(metadata)
52
+ return {} unless metadata.is_a?(Hash)
53
+
54
+ metadata.reject { |k, _| RENDERED_METADATA_KEYS.include?(k.to_s) }
55
+ end
56
+
57
+ private
58
+
59
+ def pgbus_process_args(args)
60
+ args.to_h do |key, value|
61
+ case key
62
+ when :ago then [key, pgbus_time(value)]
63
+ when *SECONDS_ARGS then [key, pgbus_duration(value)]
64
+ when *NUMBER_ARGS then [key, number_with_delimiter(value)]
65
+ else [key, value]
66
+ end
67
+ end
68
+ end
69
+
70
+ def pgbus_process_children(children)
71
+ children.map { |kind, count| t("pgbus.processes.state.kinds.#{kind}", count: count, default: "#{count} #{kind}") }
72
+ .join(", ")
73
+ end
74
+
75
+ def pgbus_process_join(parts)
76
+ parts.empty? ? Web::TimeFormat::NONE : safe_join(parts, PART_SEPARATOR)
77
+ end
78
+ end
79
+ end
@@ -86,7 +86,6 @@
86
86
  <%# Importmap for ES modules — keys are internal symbols, values are app-controlled paths, no user input %>
87
87
  <% importmaps = Pgbus::FrontendsController.js_modules.keys.index_with { |mod| frontend_module_path(mod, format: :js, locale: nil) } %>
88
88
  <%= tag.script({ imports: importmaps }.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce) %> <%# herb:disable erb-no-unsafe-raw %>
89
- <%= tag.script("", src: frontend_static_path(:apexcharts, format: :js, locale: nil), nonce: content_security_policy_nonce) %>
90
89
 
91
90
  <script type="module" nonce="<%= content_security_policy_nonce %>">
92
91
  import "application";
@@ -1,4 +1,5 @@
1
- <turbo-frame id="batches-list" data-auto-refresh data-src="<%= pgbus.batches_path(frame: 'list') %>">
1
+ <turbo-frame id="batches-list" data-auto-refresh data-turbo-action="advance"
2
+ data-src="<%= pgbus.batches_path(request.query_parameters.merge(frame: "list")) %>">
2
3
  <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
3
4
  <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
4
5
  <thead class="bg-gray-50 dark:bg-gray-900">
@@ -14,18 +15,17 @@
14
15
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
15
16
  <% @batches.each do |batch| %>
16
17
  <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
17
- <td data-label="Batch ID" class="px-4 py-3 text-sm">
18
- <%= link_to batch[:batch_id][0..7], pgbus.batch_path(batch[:batch_id]),
19
- class: "font-mono text-indigo-600 dark:text-indigo-400 hover:underline",
20
- data: { turbo_frame: "_top" } %>
18
+ <td data-label="<%= t("pgbus.batches.index.headers.batch_id") %>" class="px-4 py-3 text-sm">
19
+ <%= pgbus_link_to batch[:batch_id][0..7], pgbus.batch_path(batch[:batch_id]),
20
+ class: "font-mono", data: { turbo_frame: "_top" } %>
21
21
  </td>
22
- <td data-label="Description" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300 max-w-xs truncate">
22
+ <td data-label="<%= t("pgbus.batches.index.headers.description") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-300 max-w-xs truncate">
23
23
  <%= batch[:description] || "—" %>
24
24
  </td>
25
- <td data-label="Status" class="px-4 py-3 text-sm">
25
+ <td data-label="<%= t("pgbus.batches.index.headers.status") %>" class="px-4 py-3 text-sm">
26
26
  <%= pgbus_batch_status_badge(batch[:status]) %>
27
27
  </td>
28
- <td data-label="Progress" class="px-4 py-3 text-sm">
28
+ <td data-label="<%= t("pgbus.batches.index.headers.progress") %>" class="px-4 py-3 text-sm">
29
29
  <div class="flex items-center space-x-2">
30
30
  <div class="w-24 bg-gray-200 dark:bg-gray-700 rounded-full h-2">
31
31
  <div class="h-2 rounded-full <%= batch[:failed_jobs].to_i > 0 ? 'bg-amber-500' : 'bg-green-500' %>"
@@ -34,21 +34,24 @@
34
34
  <span class="text-xs text-gray-500 dark:text-gray-400 font-mono"><%= batch[:progress_pct] %>%</span>
35
35
  </div>
36
36
  </td>
37
- <td data-label="Jobs" class="px-4 py-3 text-sm text-right font-mono text-gray-500 dark:text-gray-400">
37
+ <td data-label="<%= t("pgbus.batches.index.headers.jobs") %>" class="px-4 py-3 text-sm text-right font-mono text-gray-500 dark:text-gray-400">
38
38
  <%= batch[:completed_jobs] %>/<%= batch[:total_jobs] %>
39
39
  <% if batch[:failed_jobs].to_i > 0 %>
40
- <span class="text-red-500">(<%= batch[:failed_jobs] %> failed)</span>
40
+ <span class="text-red-700 dark:text-red-400">(<%= batch[:failed_jobs] %> failed)</span>
41
41
  <% end %>
42
42
  </td>
43
- <td data-label="Created" class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400">
44
- <%= pgbus_time_ago(batch[:created_at]) %>
43
+ <td data-label="<%= t("pgbus.batches.index.headers.created") %>" class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400">
44
+ <%= pgbus_time(batch[:created_at]) %>
45
45
  </td>
46
46
  </tr>
47
47
  <% end %>
48
48
  <% if @batches.empty? %>
49
- <tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400 dark:text-gray-500"><%= t("pgbus.batches.index.empty") %></td></tr>
49
+ <%= render "pgbus/shared/empty_row", colspan: 6, message: t("pgbus.batches.index.empty") %>
50
50
  <% end %>
51
51
  </tbody>
52
52
  </table>
53
+ <%= render "pgbus/shared/pager", page: @page, per_page: @per_page, shown: @batches.size,
54
+ total_count: @batches_count.total, has_more: @batches_count.capped? && @batches.size == @per_page,
55
+ page_path: ->(n) { pgbus.batches_path(page: n) } %>
53
56
  </div>
54
57
  </turbo-frame>
@@ -1,7 +1,6 @@
1
1
  <div class="mb-6">
2
2
  <div class="flex items-center space-x-2 mb-2">
3
- <%= link_to t("pgbus.batches.show.back"), pgbus.batches_path,
4
- class: "text-sm text-indigo-600 dark:text-indigo-400 hover:underline" %>
3
+ <%= pgbus_link_to t("pgbus.batches.show.back"), pgbus.batches_path %>
5
4
  </div>
6
5
  <h1 class="text-2xl font-bold text-gray-900 dark:text-white">
7
6
  <%= t("pgbus.batches.show.title") %>
@@ -27,12 +26,12 @@
27
26
  </div>
28
27
  <div class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
29
28
  <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.show.created_at") %></dt>
30
- <dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0"><%= @batch[:created_at] %></dd>
29
+ <dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0"><%= pgbus_timestamp(@batch[:created_at]) %></dd>
31
30
  </div>
32
31
  <% if @batch[:finished_at] %>
33
32
  <div class="px-4 py-3 sm:grid sm:grid-cols-3 sm:gap-4">
34
33
  <dt class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.batches.show.finished_at") %></dt>
35
- <dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0"><%= @batch[:finished_at] %></dd>
34
+ <dd class="mt-1 text-sm text-gray-900 dark:text-white sm:col-span-2 sm:mt-0"><%= pgbus_timestamp(@batch[:finished_at]) %></dd>
36
35
  </div>
37
36
  <% end %>
38
37
  <% if @batch[:on_finish_class] %>
@@ -5,23 +5,23 @@
5
5
  <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
6
6
  <thead class="bg-gray-50 dark:bg-gray-900">
7
7
  <tr>
8
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.processes_table.headers.kind") %></th>
9
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.processes_table.headers.host") %></th>
10
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.processes_table.headers.pid") %></th>
11
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.processes_table.headers.status") %></th>
8
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.processes_table.headers.kind") %></th>
9
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.processes_table.headers.host") %></th>
10
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.processes_table.headers.pid") %></th>
11
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.processes_table.headers.status") %></th>
12
12
  </tr>
13
13
  </thead>
14
14
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
15
- <% @processes.each do |p| %>
16
- <tr>
17
- <td data-label="Kind" class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white"><%= p[:kind] %></td>
18
- <td data-label="Host" class="px-4 py-3 text-sm text-gray-500"><%= p[:hostname] %></td>
19
- <td data-label="PID" class="px-4 py-3 text-sm text-gray-500"><%= p[:pid] %></td>
20
- <td data-label="Status" class="px-4 py-3 text-sm"><%= pgbus_status_badge(p[:status] || p[:healthy]) %></td>
15
+ <% @processes.each do |process| %>
16
+ <tr id="dashboard-process-<%= process[:id] %>">
17
+ <td data-label="<%= t("pgbus.dashboard.processes_table.headers.kind") %>" class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white"><%= process[:kind] %></td>
18
+ <td data-label="<%= t("pgbus.dashboard.processes_table.headers.host") %>" class="px-4 py-3 text-sm lg:whitespace-nowrap text-gray-500 dark:text-gray-400"><%= process[:hostname] %></td>
19
+ <td data-label="<%= t("pgbus.dashboard.processes_table.headers.pid") %>" class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400"><%= process[:pid] %></td>
20
+ <td data-label="<%= t("pgbus.dashboard.processes_table.headers.status") %>" class="px-4 py-3 text-sm"><%= render "pgbus/processes/status", state: @process_states.fetch(process[:id]) %></td>
21
21
  </tr>
22
22
  <% end %>
23
23
  <% if @processes.empty? %>
24
- <tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.dashboard.processes_table.empty") %></td></tr>
24
+ <%= render "pgbus/shared/empty_row", colspan: 4, message: t("pgbus.dashboard.processes_table.empty") %>
25
25
  <% end %>
26
26
  </tbody>
27
27
  </table>
@@ -1,78 +1,69 @@
1
1
  <turbo-frame id="dashboard-health" data-auto-refresh data-src="<%= pgbus.root_path(frame: 'health') %>">
2
+ <% verdict = Pgbus::Web::HealthVerdict.present(@health) %>
2
3
  <div class="mb-8">
3
4
  <h2 class="text-lg font-semibold text-gray-900 dark:text-white mb-3"><%= t("pgbus.dashboard.queue_health.title") %></h2>
4
5
 
5
6
  <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-4">
6
7
  <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
7
8
  <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.dead_tuples") %></p>
8
- <p class="mt-1 text-3xl font-semibold <%= @health[:total_dead_tuples] > 10_000 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-900 dark:text-white' %>">
9
- <%= pgbus_number(@health[:total_dead_tuples]) %>
10
- </p>
11
- <p class="text-xs text-gray-400 dark:text-gray-500"><%= pgbus_number(@health[:total_live_tuples]) %> <%= t("pgbus.dashboard.queue_health.live") %></p>
9
+ <p class="<%= pgbus_health_number_css(verdict.dead_tuples) %>"><%= pgbus_number(@health[:total_dead_tuples]) %></p>
10
+ <p class="text-xs text-gray-500 dark:text-gray-400"><%= pgbus_number(@health[:total_live_tuples]) %> <%= t("pgbus.dashboard.queue_health.live") %></p>
11
+ <%= pgbus_health_words(verdict.dead_tuples) %>
12
12
  </div>
13
13
 
14
14
  <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
15
15
  <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.worst_bloat") %></p>
16
- <p class="mt-1 text-3xl font-semibold <%= @health[:worst_bloat_ratio] > 0.2 ? 'text-red-600 dark:text-red-400' : @health[:worst_bloat_ratio] > 0.1 ? 'text-amber-600 dark:text-amber-400' : 'text-green-600 dark:text-green-400' %>">
17
- <%= (@health[:worst_bloat_ratio] * 100).round(1) %>%
18
- </p>
16
+ <p class="<%= pgbus_health_number_css(verdict.bloat) %>"><%= pgbus_percent(@health[:worst_bloat_ratio]) %></p>
19
17
  <% if @health[:tables_needing_vacuum] > 0 %>
20
- <p class="text-xs text-amber-500"><%= t("pgbus.dashboard.queue_health.tables_need_vacuum", count: @health[:tables_needing_vacuum]) %></p>
18
+ <p class="text-xs text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.tables_need_vacuum", count: @health[:tables_needing_vacuum]) %></p>
21
19
  <% end %>
20
+ <%= pgbus_health_words(verdict.bloat) %>
22
21
  </div>
23
22
 
24
23
  <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
25
24
  <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.last_vacuum") %></p>
26
- <p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white">
27
- <% if @health[:oldest_vacuum_ago_sec] %>
28
- <%= pgbus_duration(@health[:oldest_vacuum_ago_sec]) %>
29
- <% else %>
30
- —
31
- <% end %>
32
- </p>
33
- <p class="text-xs text-gray-400 dark:text-gray-500"><%= t("pgbus.dashboard.queue_health.oldest_vacuum_ago") %></p>
25
+ <p class="<%= pgbus_health_number_css(verdict.vacuum) %>"><%= pgbus_duration(@health[:oldest_vacuum_ago_sec]) %></p>
26
+ <p class="text-xs text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.oldest_vacuum_ago") %></p>
27
+ <%= pgbus_health_words(verdict.vacuum) %>
34
28
  </div>
35
29
 
36
30
  <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
37
31
  <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.mvcc_horizon") %></p>
38
- <p class="mt-1 text-3xl font-semibold <%= (@health[:oldest_transaction_age_sec] || 0) > 60 ? 'text-red-600 dark:text-red-400' : (@health[:oldest_transaction_age_sec] || 0) > 30 ? 'text-amber-600 dark:text-amber-400' : 'text-green-600 dark:text-green-400' %>">
39
- <% if @health[:oldest_transaction_age_sec] %>
40
- <%= pgbus_duration(@health[:oldest_transaction_age_sec]) %>
41
- <% else %>
42
- —
43
- <% end %>
44
- </p>
45
- <p class="text-xs text-gray-400 dark:text-gray-500"><%= t("pgbus.dashboard.queue_health.oldest_open_txn") %></p>
32
+ <p class="<%= pgbus_health_number_css(verdict.transaction) %>"><%= pgbus_duration(@health[:oldest_transaction_age_sec]) %></p>
33
+ <p class="text-xs text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.oldest_open_txn") %></p>
34
+ <%= pgbus_health_words(verdict.transaction) %>
46
35
  </div>
47
36
  </div>
48
37
 
49
- <% if @health[:tables].any? %>
50
- <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
51
- <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
52
- <thead class="bg-gray-50 dark:bg-gray-900">
53
- <tr>
54
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queue_health.headers.table") %></th>
55
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queue_health.headers.kind") %></th>
56
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queue_health.headers.live") %></th>
57
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queue_health.headers.dead") %></th>
58
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queue_health.headers.bloat") %></th>
59
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queue_health.headers.last_vacuum") %></th>
38
+ <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
39
+ <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
40
+ <thead class="bg-gray-50 dark:bg-gray-900">
41
+ <tr>
42
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.headers.table") %></th>
43
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.headers.kind") %></th>
44
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.headers.live") %></th>
45
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.headers.dead") %></th>
46
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.headers.bloat") %></th>
47
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.headers.last_vacuum") %></th>
48
+ </tr>
49
+ </thead>
50
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
51
+ <% @health[:tables].each do |table| %>
52
+ <% bloat = Pgbus::Web::HealthVerdict.level(table[:bloat_ratio].to_f, Pgbus::Web::HealthVerdict::BLOAT_WARN, Pgbus::Web::HealthVerdict::BLOAT_CRITICAL) %>
53
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
54
+ <td data-label="<%= t("pgbus.dashboard.queue_health.headers.table") %>" class="px-4 py-3 text-sm font-mono text-gray-900 dark:text-white"><%= table[:table] %></td>
55
+ <td data-label="<%= t("pgbus.dashboard.queue_health.headers.kind") %>" class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.kinds.#{table[:kind]}") %></td>
56
+ <td data-label="<%= t("pgbus.dashboard.queue_health.headers.live") %>" class="px-4 py-3 text-sm text-right text-gray-700 dark:text-gray-300"><%= pgbus_number(table[:live_tuples]) %></td>
57
+ <td data-label="<%= t("pgbus.dashboard.queue_health.headers.dead") %>" class="px-4 py-3 text-sm text-right text-gray-700 dark:text-gray-300"><%= pgbus_number(table[:dead_tuples]) %></td>
58
+ <td data-label="<%= t("pgbus.dashboard.queue_health.headers.bloat") %>" class="px-4 py-3 text-sm text-right <%= bloat == :ok ? 'text-gray-700 dark:text-gray-300' : bloat == :warn ? 'text-amber-700 dark:text-amber-300 font-semibold' : 'text-red-700 dark:text-red-300 font-semibold' %>"><%= pgbus_percent(table[:bloat_ratio]) %></td>
59
+ <td data-label="<%= t("pgbus.dashboard.queue_health.headers.last_vacuum") %>" class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400"><%= pgbus_duration(table[:last_vacuum_ago_sec]) %></td>
60
60
  </tr>
61
- </thead>
62
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
63
- <% @health[:tables].each do |t| %>
64
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900">
65
- <td class="px-4 py-3 text-sm font-mono text-gray-900 dark:text-white"><%= t[:table] %></td>
66
- <td class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queue_health.kinds.#{t[:kind]}") %></td>
67
- <td class="px-4 py-3 text-sm text-right text-gray-700 dark:text-gray-300"><%= pgbus_number(t[:live_tuples]) %></td>
68
- <td class="px-4 py-3 text-sm text-right <%= t[:dead_tuples] > 1000 ? 'text-amber-600 dark:text-amber-400 font-semibold' : 'text-gray-700 dark:text-gray-300' %>"><%= pgbus_number(t[:dead_tuples]) %></td>
69
- <td class="px-4 py-3 text-sm text-right <%= t[:bloat_ratio] > 0.2 ? 'text-red-600 dark:text-red-400 font-semibold' : t[:bloat_ratio] > 0.1 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-500 dark:text-gray-400' %>"><%= (t[:bloat_ratio] * 100).round(1) %>%</td>
70
- <td class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400"><%= t[:last_vacuum_ago_sec] ? pgbus_duration(t[:last_vacuum_ago_sec]) : "—" %></td>
71
- </tr>
72
- <% end %>
73
- </tbody>
74
- </table>
75
- </div>
76
- <% end %>
61
+ <% end %>
62
+ <% if @health[:tables].empty? %>
63
+ <%= render "pgbus/shared/empty_row", colspan: 6, message: t("pgbus.dashboard.queue_health.empty") %>
64
+ <% end %>
65
+ </tbody>
66
+ </table>
67
+ </div>
77
68
  </div>
78
69
  </turbo-frame>
@@ -2,37 +2,39 @@
2
2
  <div class="mb-8">
3
3
  <div class="flex items-center justify-between mb-3">
4
4
  <h2 class="text-lg font-semibold text-gray-900 dark:text-white"><%= t("pgbus.dashboard.queues_table.title") %></h2>
5
- <%= link_to t("pgbus.dashboard.queues_table.view_all"), pgbus.queues_path, class: "text-sm text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
5
+ <%= pgbus_link_to t("pgbus.dashboard.queues_table.view_all"), pgbus.queues_path, data: { turbo_frame: "_top" } %>
6
6
  </div>
7
7
 
8
8
  <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
9
9
  <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
10
10
  <thead class="bg-gray-50 dark:bg-gray-900">
11
11
  <tr>
12
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.queue") %></th>
13
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.depth") %></th>
14
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.visible") %></th>
15
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.parked") %></th>
16
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.oldest_claimable") %></th>
17
- <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.queues_table.headers.total") %></th>
12
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queues_table.headers.queue") %></th>
13
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queues_table.headers.status") %></th>
14
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queues_table.headers.depth") %></th>
15
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queues_table.headers.visible") %></th>
16
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queues_table.headers.parked") %></th>
17
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queues_table.headers.oldest_claimable") %></th>
18
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.queues_table.headers.total") %></th>
18
19
  </tr>
19
20
  </thead>
20
21
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
21
22
  <% @queues.each do |q| %>
22
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 dark:bg-gray-900">
23
- <td data-label="Queue" class="px-4 py-3 text-sm">
24
- <%= link_to q[:name], pgbus.queue_path(name: q[:name]), class: "font-medium text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
23
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
24
+ <td data-label="<%= t("pgbus.dashboard.queues_table.headers.queue") %>" class="px-4 py-3 text-sm">
25
+ <%= pgbus_link_to q[:name], pgbus.queue_path(name: q[:name]), data: { turbo_frame: "_top" } %>
25
26
  <%= pgbus_queue_badge(q[:name]) %>
26
27
  </td>
27
- <td data-label="Depth" class="px-4 py-3 text-sm text-right text-gray-700"><%= pgbus_number(q[:queue_length]) %></td>
28
- <td data-label="Visible" class="px-4 py-3 text-sm text-right text-gray-700"><%= pgbus_number(q[:queue_visible_length]) %></td>
29
- <td data-label="Parked" class="px-4 py-3 text-sm text-right text-gray-500"><%= pgbus_number(q[:parked_length]) %></td>
30
- <td data-label="Oldest claimable" class="px-4 py-3 text-sm text-right text-gray-500"><%= q[:oldest_claimable_age_sec] || "—" %></td>
31
- <td data-label="Total" class="px-4 py-3 text-sm text-right text-gray-500"><%= pgbus_number(q[:total_messages]) %></td>
28
+ <td data-label="<%= t("pgbus.dashboard.queues_table.headers.status") %>" class="px-4 py-3 text-sm"><%= pgbus_queue_status(@queue_lines[q[:name]]) %></td>
29
+ <td data-label="<%= t("pgbus.dashboard.queues_table.headers.depth") %>" class="px-4 py-3 text-sm text-right text-gray-700 dark:text-gray-200"><%= pgbus_number(q[:queue_length]) %></td>
30
+ <td data-label="<%= t("pgbus.dashboard.queues_table.headers.visible") %>" class="px-4 py-3 text-sm text-right text-gray-700 dark:text-gray-200"><%= pgbus_number(q[:queue_visible_length]) %></td>
31
+ <td data-label="<%= t("pgbus.dashboard.queues_table.headers.parked") %>" class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400"><%= pgbus_number(q[:parked_length]) %></td>
32
+ <td data-label="<%= t("pgbus.dashboard.queues_table.headers.oldest_claimable") %>" class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400"><%= pgbus_duration(q[:oldest_claimable_age_sec]) %></td>
33
+ <td data-label="<%= t("pgbus.dashboard.queues_table.headers.total") %>" class="px-4 py-3 text-sm text-right text-gray-500 dark:text-gray-400"><%= pgbus_number(q[:total_messages]) %></td>
32
34
  </tr>
33
35
  <% end %>
34
36
  <% if @queues.empty? %>
35
- <tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.dashboard.queues_table.empty") %></td></tr>
37
+ <%= render "pgbus/shared/empty_row", colspan: 7, message: t("pgbus.dashboard.queues_table.empty") %>
36
38
  <% end %>
37
39
  </tbody>
38
40
  </table>
@@ -2,29 +2,33 @@
2
2
  <div>
3
3
  <div class="flex items-center justify-between mb-3">
4
4
  <h2 class="text-lg font-semibold text-gray-900 dark:text-white"><%= t("pgbus.dashboard.recent_failures.title") %></h2>
5
- <% if @recent_failures.any? %>
6
- <%= link_to t("pgbus.dashboard.recent_failures.view_all"), pgbus.jobs_path(status: "failed"), class: "text-sm text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
7
- <% end %>
5
+ <%= pgbus_link_to t("pgbus.dashboard.recent_failures.view_all"), pgbus.jobs_path(state: "retrying"), data: { turbo_frame: "_top" } %>
8
6
  </div>
9
- <div class="overflow-hidden rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
7
+ <div class="overflow-x-auto rounded-lg bg-white dark:bg-gray-800 shadow ring-1 ring-gray-200 dark:ring-gray-700">
10
8
  <table class="pgbus-table min-w-full divide-y divide-gray-200 dark:divide-gray-700">
11
9
  <thead class="bg-gray-50 dark:bg-gray-900">
12
10
  <tr>
13
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.recent_failures.headers.queue") %></th>
14
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.recent_failures.headers.error") %></th>
15
- <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500"><%= t("pgbus.dashboard.recent_failures.headers.when") %></th>
11
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.recent_failures.headers.job") %></th>
12
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.recent_failures.headers.queue") %></th>
13
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.recent_failures.headers.error") %></th>
14
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.recent_failures.headers.attempt") %></th>
15
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.recent_failures.headers.when") %></th>
16
16
  </tr>
17
17
  </thead>
18
18
  <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
19
19
  <% @recent_failures.each do |f| %>
20
- <tr>
21
- <td data-label="Queue" class="px-4 py-3 text-sm text-gray-700"><%= f["queue_name"] %></td>
22
- <td data-label="Error" class="px-4 py-3 text-sm text-red-600 truncate max-w-xs" title="<%= f["error_class"] %>: <%= f["error_message"] %>"><%= f["error_class"] %>: <%= truncate(f["error_message"].to_s, length: 60) %></td>
23
- <td data-label="Time" class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(f["failed_at"]) %></td>
20
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
21
+ <td data-label="<%= t("pgbus.dashboard.recent_failures.headers.job") %>" class="px-4 py-3 text-sm">
22
+ <%= pgbus_link_to pgbus_failure_job(f), pgbus.job_path(f["id"]), data: { turbo_frame: "_top" } %>
23
+ </td>
24
+ <td data-label="<%= t("pgbus.dashboard.recent_failures.headers.queue") %>" class="px-4 py-3 text-sm text-gray-700 dark:text-gray-200"><%= f["queue_name"] %></td>
25
+ <td data-label="<%= t("pgbus.dashboard.recent_failures.headers.error") %>" class="px-4 py-3 text-sm text-red-700 dark:text-red-300" title="<%= f["error_class"] %>: <%= f["error_message"] %>"><span class="block max-w-md truncate"><%= f["error_class"] %>: <%= truncate(f["error_message"].to_s, length: 60) %></span></td>
26
+ <td data-label="<%= t("pgbus.dashboard.recent_failures.headers.attempt") %>" class="px-4 py-3 text-sm text-right text-gray-700 dark:text-gray-200"><%= pgbus_failure_attempt(f) %></td>
27
+ <td data-label="<%= t("pgbus.dashboard.recent_failures.headers.when") %>" class="px-4 py-3 text-sm text-gray-500 dark:text-gray-400"><%= pgbus_time(f["failed_at"]) %></td>
24
28
  </tr>
25
29
  <% end %>
26
30
  <% if @recent_failures.empty? %>
27
- <tr><td colspan="3" class="px-4 py-8 text-center text-sm text-gray-400"><%= t("pgbus.dashboard.recent_failures.empty") %></td></tr>
31
+ <%= render "pgbus/shared/empty_row", colspan: 5, message: t("pgbus.dashboard.recent_failures.empty") %>
28
32
  <% end %>
29
33
  </tbody>
30
34
  </table>
@@ -1,54 +1,88 @@
1
1
  <turbo-frame id="dashboard-stats" data-auto-refresh data-src="<%= pgbus.root_path(frame: 'stats') %>">
2
- <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-7 mb-8">
3
- <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
2
+ <%# The verdict /readyz serves (HealthAnalyzer). Its reasons are operator
3
+ diagnostics in English and show verbatim, like an error message. %>
4
+ <% verdict = @signals[:verdict] %>
5
+ <section data-testid="health-verdict" aria-labelledby="health-verdict-title" class="mb-6 rounded-lg p-4 ring-1 <%= pgbus_verdict_css(verdict) %>">
6
+ <h2 id="health-verdict-title" class="text-sm font-semibold">
7
+ <% if verdict %>
8
+ <%= t("pgbus.dashboard.verdict.title", status: t("pgbus.dashboard.verdict.statuses.#{verdict[:status].to_s.downcase}")) %>
9
+ <% else %>
10
+ <%= t("pgbus.dashboard.verdict.unavailable") %>
11
+ <% end %>
12
+ </h2>
13
+ <% if verdict %>
14
+ <p class="mt-1 text-sm"><%= t("pgbus.dashboard.verdict.meaning.#{verdict[:status].to_s.downcase}") %></p>
15
+ <% if verdict[:reasons].any? %>
16
+ <ul class="mt-2 list-disc pl-5 text-sm space-y-1">
17
+ <% verdict[:reasons].each do |reason| %>
18
+ <li><%= reason %></li>
19
+ <% end %>
20
+ </ul>
21
+ <% end %>
22
+ <% links = pgbus_verdict_links(@signals) %>
23
+ <% if links.any? && verdict[:status] != "OK" %>
24
+ <p class="mt-2 text-sm"><%= t("pgbus.dashboard.verdict.look_at") %> <%= safe_join(links, " · ") %></p>
25
+ <% end %>
26
+ <% end %>
27
+ </section>
28
+
29
+ <%# Every card links to the page that explains its number. turbo_frame:
30
+ "_top" because the cards sit inside the dashboard-stats frame, and the
31
+ target pages have no frame of that id to render into. %>
32
+ <% card = Pgbus::DashboardHelper::CARD_CSS %>
33
+ <% number = Pgbus::DashboardHelper::CARD_NUMBER_CSS %>
34
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-8">
35
+ <%= link_to pgbus.queues_path, data: { turbo_frame: "_top" }, class: card do %>
4
36
  <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.queues") %></p>
5
- <p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:total_queues] %></p>
6
- </div>
37
+ <p class="<%= number %>"><%= number_with_delimiter(@signals[:queues][:total]) %></p>
38
+ <p class="text-xs text-gray-600 dark:text-gray-300"><%= pgbus_queue_card_words(@signals[:queues]) %></p>
39
+ <% end %>
7
40
 
8
- <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
41
+ <%= link_to pgbus.jobs_path(state: "ready"), data: { turbo_frame: "_top" }, class: card do %>
9
42
  <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.enqueued") %></p>
10
- <p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= pgbus_number(@stats[:total_depth]) %></p>
11
- <p class="text-xs text-gray-400 dark:text-gray-500"><%= pgbus_number(@stats[:total_visible]) %> <%= t("pgbus.dashboard.stats_cards.visible") %></p>
12
- </div>
43
+ <p class="<%= number %>"><%= pgbus_number(@signals[:queues][:depth]) %></p>
44
+ <p class="text-xs text-gray-600 dark:text-gray-300"><%= pgbus_enqueued_card_words(@signals[:queues]) %></p>
45
+ <% end %>
13
46
 
14
- <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
47
+ <%= link_to pgbus.processes_path, data: { turbo_frame: "_top" }, class: card do %>
48
+ <% processes = @signals[:processes] %>
15
49
  <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.processes") %></p>
16
- <p class="mt-1 text-3xl font-semibold <%= @stats[:active_processes] > 0 ? 'text-green-600 dark:text-green-400' : 'text-gray-400' %>">
17
- <%= @stats[:active_processes] %>
18
- </p>
19
- </div>
50
+ <p class="mt-1 text-3xl font-semibold <%= (processes[:stalled] + processes[:stale]).positive? ? 'text-amber-600 dark:text-amber-400' : processes[:healthy].positive? ? 'text-green-600 dark:text-green-400' : 'text-gray-900 dark:text-white' %>"><%= number_with_delimiter(processes[:total]) %></p>
51
+ <p class="text-xs text-gray-600 dark:text-gray-300"><%= pgbus_process_card_words(processes) %></p>
52
+ <% end %>
20
53
 
21
- <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
54
+ <%= link_to pgbus.recurring_tasks_path, data: { turbo_frame: "_top" }, class: card do %>
22
55
  <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.recurring") %></p>
23
- <p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:recurring_count] %></p>
24
- <p class="text-xs text-gray-400">
25
- <%= link_to t("pgbus.dashboard.stats_cards.view_tasks"), pgbus.recurring_tasks_path, class: "text-blue-500 hover:text-blue-700 dark:text-blue-400" %>
26
- </p>
27
- </div>
56
+ <p class="<%= number %>"><%= number_with_delimiter(@stats[:recurring_count]) %></p>
57
+ <p class="text-xs text-gray-600 dark:text-gray-300"><%= t("pgbus.dashboard.stats_cards.recurring_detail") %></p>
58
+ <% end %>
28
59
 
29
- <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
30
- <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.failed_dlq") %></p>
31
- <p class="mt-1 text-3xl font-semibold <%= (@stats[:failed_count] + @stats[:dlq_depth]) > 0 ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-white' %>">
32
- <%= @stats[:failed_count] %> / <%= @stats[:dlq_depth] %>
33
- </p>
34
- <p class="text-xs text-gray-400 dark:text-gray-500"><%= t("pgbus.dashboard.stats_cards.failed_dlq_labels") %></p>
35
- </div>
60
+ <%= link_to pgbus.jobs_path(state: "retrying"), data: { turbo_frame: "_top" }, class: card do %>
61
+ <% retrying = @signals[:retrying] %>
62
+ <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.retrying") %></p>
63
+ <p class="mt-1 text-3xl font-semibold <%= retrying[:count].positive? ? 'text-amber-600 dark:text-amber-400' : 'text-gray-900 dark:text-white' %>"><%= pgbus_retrying_count(retrying) %></p>
64
+ <p class="text-xs text-gray-600 dark:text-gray-300"><%= pgbus_retrying_card_words(retrying) %></p>
65
+ <% end %>
36
66
 
37
- <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
67
+ <%= link_to pgbus.dead_letter_index_path, data: { turbo_frame: "_top" }, class: card do %>
68
+ <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.dead_lettered") %></p>
69
+ <p class="mt-1 text-3xl font-semibold <%= @signals[:dlq][:depth].positive? ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-white' %>"><%= number_with_delimiter(@signals[:dlq][:depth]) %></p>
70
+ <p class="text-xs text-gray-600 dark:text-gray-300"><%= pgbus_dlq_card_words(@signals[:dlq]) %></p>
71
+ <% end %>
72
+
73
+ <%= link_to pgbus.insights_path, data: { turbo_frame: "_top" }, class: card do %>
74
+ <% throughput = @signals[:throughput] %>
38
75
  <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.throughput") %></p>
39
- <p class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white"><%= @stats[:throughput_rate] %></p>
40
- <p class="text-xs text-gray-400"><%= t("pgbus.dashboard.stats_cards.throughput_unit") %></p>
41
- </div>
42
-
43
- <%# The whole card links to the Locks page: parked jobs are only actionable
44
- there. turbo_frame: "_top" because this card sits inside the
45
- dashboard-stats frame, and /locks has no frame of that id to render into. %>
46
- <%= link_to pgbus.locks_path, data: { turbo_frame: "_top" }, class: "rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700 block hover:ring-gray-300 dark:hover:ring-gray-600" do %>
76
+ <p class="<%= number %>"><%= throughput[:available] ? number_with_precision(throughput[:jobs_per_minute], precision: 1) : Pgbus::Web::TimeFormat::NONE %></p>
77
+ <p class="text-xs text-gray-600 dark:text-gray-300"><%= throughput[:available] ? t("pgbus.dashboard.stats_cards.throughput_detail", minutes: throughput[:window_minutes]) : t("pgbus.dashboard.stats_cards.throughput_unavailable") %></p>
78
+ <% end %>
79
+
80
+ <%= link_to pgbus.locks_path, data: { turbo_frame: "_top" }, class: card do %>
47
81
  <p class="text-sm font-medium text-gray-500 dark:text-gray-400"><%= t("pgbus.dashboard.stats_cards.parked_jobs") %></p>
48
82
  <p class="mt-1 text-3xl font-semibold <%= @stats[:parked_total].to_i > 0 ? 'text-amber-600 dark:text-amber-400' : 'text-gray-900 dark:text-white' %>">
49
83
  <%= pgbus_number(@stats[:parked_total]) %>
50
84
  </p>
51
- <p class="text-xs text-gray-400 dark:text-gray-500">
85
+ <p class="text-xs text-gray-600 dark:text-gray-300">
52
86
  <%= t("pgbus.dashboard.stats_cards.parked_jobs_oldest", age: pgbus_duration(@stats[:oldest_parked_age_sec])) %>
53
87
  </p>
54
88
  <% end %>
@@ -6,7 +6,7 @@
6
6
 
7
7
  <%= render "pgbus/dashboard/queue_health" %>
8
8
 
9
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
9
+ <div class="grid grid-cols-1 2xl:grid-cols-2 gap-6">
10
10
  <%= render "pgbus/dashboard/processes_table" %>
11
11
  <%= render "pgbus/dashboard/recent_failures" %>
12
12
  </div>