pgbus 0.9.4 → 0.9.6

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +115 -0
  3. data/Rakefile +56 -3
  4. data/app/controllers/pgbus/dead_letter_controller.rb +5 -1
  5. data/app/helpers/pgbus/application_helper.rb +12 -2
  6. data/app/views/pgbus/dashboard/_processes_table.html.erb +1 -1
  7. data/app/views/pgbus/dashboard/_recent_failures.html.erb +1 -1
  8. data/app/views/pgbus/dashboard/_stats_cards.html.erb +1 -0
  9. data/app/views/pgbus/dead_letter/_messages_table.html.erb +33 -1
  10. data/app/views/pgbus/insights/show.html.erb +1 -1
  11. data/app/views/pgbus/jobs/_failed_table.html.erb +1 -1
  12. data/app/views/pgbus/processes/_processes_table.html.erb +1 -1
  13. data/app/views/pgbus/queues/show.html.erb +3 -0
  14. data/config/locales/da.yml +8 -0
  15. data/config/locales/de.yml +8 -0
  16. data/config/locales/en.yml +8 -0
  17. data/config/locales/es.yml +8 -0
  18. data/config/locales/fi.yml +8 -0
  19. data/config/locales/fr.yml +8 -0
  20. data/config/locales/it.yml +8 -0
  21. data/config/locales/ja.yml +8 -0
  22. data/config/locales/nb.yml +8 -0
  23. data/config/locales/nl.yml +8 -0
  24. data/config/locales/pt.yml +8 -0
  25. data/config/locales/sv.yml +8 -0
  26. data/lib/pgbus/cli.rb +16 -0
  27. data/lib/pgbus/client.rb +34 -7
  28. data/lib/pgbus/configuration.rb +17 -1
  29. data/lib/pgbus/integrations/appsignal/dashboard.json +353 -0
  30. data/lib/pgbus/integrations/appsignal/dashboards/pgbus_health.json +224 -85
  31. data/lib/pgbus/integrations/appsignal/dashboards/pgbus_streams.json +154 -63
  32. data/lib/pgbus/integrations/appsignal/dashboards/pgbus_throughput.json +218 -79
  33. data/lib/pgbus/integrations/appsignal/probe.rb +17 -2
  34. data/lib/pgbus/integrations/appsignal.rb +17 -0
  35. data/lib/pgbus/mcp/base_tool.rb +79 -0
  36. data/lib/pgbus/mcp/health_analyzer.rb +159 -0
  37. data/lib/pgbus/mcp/rack_app.rb +97 -0
  38. data/lib/pgbus/mcp/redactor.rb +72 -0
  39. data/lib/pgbus/mcp/runner.rb +73 -0
  40. data/lib/pgbus/mcp/server.rb +62 -0
  41. data/lib/pgbus/mcp/tools/dlq_detail_tool.rb +72 -0
  42. data/lib/pgbus/mcp/tools/dlq_tool.rb +48 -0
  43. data/lib/pgbus/mcp/tools/health_tool.rb +33 -0
  44. data/lib/pgbus/mcp/tools/job_detail_tool.rb +42 -0
  45. data/lib/pgbus/mcp/tools/jobs_tool.rb +56 -0
  46. data/lib/pgbus/mcp/tools/locks_tool.rb +28 -0
  47. data/lib/pgbus/mcp/tools/processes_tool.rb +32 -0
  48. data/lib/pgbus/mcp/tools/queue_detail_tool.rb +46 -0
  49. data/lib/pgbus/mcp/tools/queues_tool.rb +28 -0
  50. data/lib/pgbus/mcp/tools/recurring_tool.rb +27 -0
  51. data/lib/pgbus/mcp/tools/stats_tool.rb +40 -0
  52. data/lib/pgbus/mcp/tools/throughput_tool.rb +35 -0
  53. data/lib/pgbus/mcp.rb +55 -0
  54. data/lib/pgbus/process/heartbeat.rb +8 -2
  55. data/lib/pgbus/process/supervisor.rb +44 -0
  56. data/lib/pgbus/process/worker.rb +12 -1
  57. data/lib/pgbus/version.rb +1 -1
  58. data/lib/pgbus/web/data_source.rb +40 -2
  59. data/lib/pgbus.rb +15 -1
  60. metadata +21 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f659edeb84f108e166ae2ea981e5eb3e4fabfde1542769a777d9d707aba349d
4
- data.tar.gz: 2abf0072cdc0f414670367e4a59881003285c43f895618d1ec63b1b7fca5e27a
3
+ metadata.gz: 71ef7f4e4fa290e422b8ce72402833f6a31fd7cb3d081aef8a4bfcc09fc467eb
4
+ data.tar.gz: 31a6522c809392316d28125b0fe8e972c6e18ca8b48510d8f891135b75c3e63c
5
5
  SHA512:
6
- metadata.gz: c527aa3c60ae917b2589cd23b99f14348fa4eef752ae004b2ffc3a90d248e5c09bc68ee06ddfde3d335c469192dd821551b56bb9b6fb259f6f1b961a61567d9e
7
- data.tar.gz: 6f66d58dfe49bca18d3b3a1379e2c458c75954cec3c7fb1ddd23fcfa05e495b00db4d3646d42e32e40b202135e5bb7e51a18d6287ab897e7605fd87250c93391
6
+ metadata.gz: 7554038df6c2a70435a615a2594425ff05ce102953b445a7ad5f47ee3d69a7e81e32445f9e790d987d71b5b17aa807250ebadae6d50f191da3eb23c0676b8f98
7
+ data.tar.gz: fadb07428ddbaead22200de2ff1031ff924d75e4fa5cc7ea48307ab7a22e8cf84b7528ea3a69f25baab402732a7804046005e6c1b1fd33fced91823de4702896
data/README.md CHANGED
@@ -838,6 +838,121 @@ When `config.metrics_enabled = true` (default), the dashboard exposes Prometheus
838
838
  | `pgbus_worker_pool_busy` | Currently busy worker threads |
839
839
  | `pgbus_worker_pool_utilization` | Busy / capacity ratio |
840
840
 
841
+ ### MCP diagnostic server (read-only)
842
+
843
+ Pgbus ships an optional, **read-only** [MCP](https://modelcontextprotocol.io) server so an AI agent (or any MCP client) can diagnose pgbus directly — "are queues backed up?", "is `read_ct` advancing?", "are workers heart-beating but not claiming?" — instead of hand-writing `pgmq` / `pg_stat_activity` SQL against production. It is a thin adapter over the same read layer the dashboard uses, so it adds no new database access path.
844
+
845
+ Add the optional `mcp` gem to your `Gemfile` first (`gem "mcp"`); both entry points below tell you if it's missing.
846
+
847
+ #### Choosing a deployment
848
+
849
+ There are two ways to run it, depending on **who connects and from where**:
850
+
851
+ | | **stdio** (`pgbus mcp`) | **HTTP** (mount in Rails) |
852
+ |---|---|---|
853
+ | Who connects | A local operator (Claude Desktop / Claude Code on your machine) | A remote agent or alerting system |
854
+ | Process model | The MCP client **spawns** a short-lived process on demand | Runs **inside your existing Rails server** — no second process |
855
+ | Reaches production? | Only if run where prod DB creds are available | Yes — co-located with the app, same credentials |
856
+ | Use it for | Hands-on, interactive debugging | Automated/remote diagnostics & alerting |
857
+
858
+ > **Don't start a second `bin/pgbus` instance for HTTP.** The HTTP transport is a Rack app — mount it in the Rails app you already deploy.
859
+
860
+ ##### stdio (local operator)
861
+
862
+ ```bash
863
+ bundle exec pgbus mcp # speaks MCP over stdio
864
+ ```
865
+
866
+ ##### HTTP (mount in Rails)
867
+
868
+ ```ruby
869
+ # config/routes.rb
870
+ Rails.application.routes.draw do
871
+ # ... your routes ...
872
+ mount Pgbus::MCP.rack_app(token: ENV["PGBUS_MCP_TOKEN"]) => "/pgbus/mcp"
873
+ end
874
+ ```
875
+
876
+ `Pgbus::MCP.rack_app` returns a gated Rack app. It runs the transport in **stateless + JSON-response mode**, so every request is a self-contained POST with no in-memory session — safe behind multiple Puma/Falcon workers (any worker can answer any request). Keep the endpoint on an internal network / behind your VPN; it is not meant to be internet-exposed.
877
+
878
+ Options:
879
+
880
+ | Option | Default | Meaning |
881
+ |--------|---------|---------|
882
+ | `token:` | `nil` | Shared secret. When set, requests must send `Authorization: Bearer <token>` (constant-time compared). |
883
+ | `auth:` | `nil` | A callable `->(rack_request) { ... }` returning truthy to allow — mirrors `config.web_auth`. Wins over `token:`. |
884
+ | `allow_payloads:` | `false` | When true, tools honor a per-call `include_payloads` flag (see Security). |
885
+
886
+ If you set neither `token:` nor `auth:`, pgbus logs a warning — an unauthenticated diagnostic endpoint exposes operational metadata to anyone who can reach it.
887
+
888
+ > Clients must send `Accept: application/json` and `Content-Type: application/json` on every POST, or the transport replies `406 Not Acceptable`. MCP clients do this automatically.
889
+
890
+ Need a **standalone HTTP pod** instead of mounting in your main app? The same Rack app works under any Rack server, e.g. a one-line `config.ru`:
891
+
892
+ ```ruby
893
+ require "pgbus"
894
+ Pgbus::MCP.load!
895
+ run Pgbus::MCP.rack_app(token: ENV["PGBUS_MCP_TOKEN"])
896
+ # rackup -p 9293 (run it in a container that shares the app's DB config)
897
+ ```
898
+
899
+ #### Tools
900
+
901
+ All tools are read-only — no tool mutates state, and there is no raw-SQL passthrough.
902
+
903
+ | Tool | Purpose |
904
+ |------|---------|
905
+ | `pgbus_health` | One-call verdict: `OK` / `DEGRADED` / `STALLED`. STALLED is the silent-worker-wedge signal (visible backlog while workers heart-beat but don't claim). Suitable for automated alerting. |
906
+ | `pgbus_queues` | All queues: depth, visible count, oldest-message age, paused state. |
907
+ | `pgbus_queue_detail` | Per-queue metrics + paused state + table health (dead tuples, bloat, vacuum age). |
908
+ | `pgbus_processes` | Every process with kind, pid, heartbeat age, and `healthy`/`stale`/`stalled` status. |
909
+ | `pgbus_jobs` / `pgbus_job_detail` | Inspect enqueued messages (`read_ct`, `vt`, `enqueued_at`). Paginated. |
910
+ | `pgbus_dlq` / `pgbus_dlq_detail` | Dead-letter inspection. Paginated. |
911
+ | `pgbus_locks` | Active uniqueness locks (the leaked-lock diagnostic). |
912
+ | `pgbus_throughput` / `pgbus_stats` | Recent throughput time series and status counts. |
913
+ | `pgbus_recurring` | Recurring task schedule + last/next run times. |
914
+
915
+ #### Security
916
+
917
+ The server is built to be safe against a production datastore:
918
+
919
+ - **Read-only by default.** No tool mutates state and no arbitrary-query tool exists.
920
+ - **Payloads redacted.** Message bodies, headers, and job arguments are replaced with `[redacted]` unless payloads are explicitly allowed **and** `include_payloads: true` is passed on the call. Both gates must be open. Allow payloads with `PGBUS_MCP_ALLOW_PAYLOADS=1` (stdio) or `Pgbus::MCP.rack_app(allow_payloads: true)` (HTTP).
921
+ - **Bounded queries.** Every list tool paginates with a row cap (`pgbus_jobs` / `pgbus_dlq` cap at 100 rows/page; time windows cap at 1440 minutes).
922
+ - **Reuses your DB credentials.** No new privileged path — it reads through the app's existing connection config.
923
+ - **Authentication.**
924
+ - *HTTP:* set `token:` (clients send `Authorization: Bearer <token>`) or a custom `auth:` callable; unauthenticated requests get `401`.
925
+ - *stdio:* the channel is local (the client spawns the process), so the gate is a boot-time precondition — set `PGBUS_MCP_TOKEN` and the server refuses to start unless `PGBUS_MCP_AUTH_TOKEN` matches (constant-time compare).
926
+
927
+ #### Client configuration
928
+
929
+ For the **stdio** transport, a minimal MCP client config (Claude Code / Claude Desktop style):
930
+
931
+ ```json
932
+ {
933
+ "mcpServers": {
934
+ "pgbus": {
935
+ "command": "bundle",
936
+ "args": ["exec", "pgbus", "mcp"],
937
+ "env": { "RAILS_ENV": "production" }
938
+ }
939
+ }
940
+ }
941
+ ```
942
+
943
+ For the **HTTP** transport, point the client at the mounted URL with a streamable-HTTP server config, sending the bearer token, e.g.:
944
+
945
+ ```json
946
+ {
947
+ "mcpServers": {
948
+ "pgbus": {
949
+ "url": "https://your-app.internal/pgbus/mcp",
950
+ "headers": { "Authorization": "Bearer ${PGBUS_MCP_TOKEN}" }
951
+ }
952
+ }
953
+ }
954
+ ```
955
+
841
956
  ## Real-time broadcasts (turbo-streams replacement)
842
957
 
843
958
  Pgbus ships a drop-in replacement for turbo-rails' `turbo_stream_from` helper that fixes several well-known ActionCable correctness bugs by using PGMQ message IDs as a replay cursor. Same API as turbo-rails. No Redis. No ActionCable. No lost messages on reconnect.
data/Rakefile CHANGED
@@ -11,6 +11,19 @@ require "rubocop/rake_task"
11
11
  RuboCop::RakeTask.new
12
12
 
13
13
  namespace :bench do
14
+ bench_dir = "benchmarks"
15
+ # Benches that need a real PostgreSQL/PGMQ (or boot Puma) — excluded from the
16
+ # no-DB unit suite that bench:all runs in CI.
17
+ db_benches = %w[connection_pool_bench integration_bench streams_bench].freeze
18
+ # The unit suite is every *_bench.rb that doesn't need a database, derived
19
+ # from the directory so a new unit bench is picked up automatically (kept in
20
+ # sync with bench:one, which globs the same files).
21
+ unit_benches = Dir["#{bench_dir}/*_bench.rb"]
22
+ .map { |f| File.basename(f, ".rb") }
23
+ .reject { |name| db_benches.include?(name) }
24
+ .sort
25
+ .freeze
26
+
14
27
  desc "Run serialization benchmarks"
15
28
  task :serialization do
16
29
  ruby "benchmarks/serialization_bench.rb"
@@ -41,11 +54,51 @@ namespace :bench do
41
54
  ruby "benchmarks/streams_bench.rb"
42
55
  end
43
56
 
44
- desc "Run all benchmarks (unit-level, no DB required)"
45
- task all: %i[serialization client executor]
57
+ desc "Run a single benchmark: rake bench:one[client_bench]"
58
+ task :one, [:name] do |_t, args|
59
+ name = args[:name] or abort "Usage: rake bench:one[serialization_bench|client_bench|...]"
60
+ available = Dir["#{bench_dir}/*_bench.rb"].map { |f| File.basename(f, ".rb") }
61
+ abort "No such benchmark: #{name}. Available: #{available.sort.join(", ")}" unless available.include?(name)
62
+ ruby "#{bench_dir}/#{name}.rb"
63
+ end
64
+
65
+ desc "Run all unit-level benchmarks, save report to tmp/benchmarks/"
66
+ task :all do
67
+ require "fileutils"
68
+ require "open3"
69
+ require "rbconfig"
70
+ FileUtils.mkdir_p("tmp/benchmarks")
71
+
72
+ # Run each bench under the SAME Ruby + gemset as this Rake process, not a
73
+ # bare `ruby` from PATH — otherwise before/after numbers are unreliable and
74
+ # gem loading can break under a different interpreter.
75
+ ruby = RbConfig.ruby
76
+ failed = []
77
+
78
+ File.open("tmp/benchmarks/unit.txt", "w") do |report|
79
+ unit_benches.each do |name|
80
+ file = "#{bench_dir}/#{name}.rb"
81
+ header = "\n### #{file} ###"
82
+ puts "\e[1;35m#{header}\e[0m"
83
+ report.puts header
84
+
85
+ result, status = Open3.capture2e(ruby, file)
86
+ puts result
87
+ report.puts result.gsub(/\e\[[0-9;]*m/, "")
88
+
89
+ unless status.success?
90
+ failed << file
91
+ report.puts "!!! FAILED (exit #{status.exitstatus})"
92
+ end
93
+ end
94
+ end
95
+
96
+ puts "\nSaved report to tmp/benchmarks/unit.txt"
97
+ abort "\nBenchmark(s) failed: #{failed.join(", ")}" if failed.any?
98
+ end
46
99
  end
47
100
 
48
- desc "Run all benchmarks"
101
+ desc "Run all benchmarks (alias for bench:all)"
49
102
  task bench: "bench:all"
50
103
 
51
104
  desc "Build gem and verify contents"
@@ -3,7 +3,11 @@
3
3
  module Pgbus
4
4
  class DeadLetterController < ApplicationController
5
5
  def index
6
- @messages = data_source.dlq_messages(page: page_param, per_page: per_page)
6
+ @page = page_param
7
+ @per_page = per_page
8
+ @messages = data_source.dlq_messages(page: @page, per_page: @per_page)
9
+ @total_count = data_source.dlq_total_count
10
+ @total_pages = (@total_count.to_f / @per_page).ceil
7
11
  render_frame("pgbus/dead_letter/messages_table") if params[:frame] == "list"
8
12
  end
9
13
 
@@ -29,10 +29,20 @@ module Pgbus
29
29
  end
30
30
  end
31
31
 
32
- def pgbus_status_badge(healthy)
33
- if healthy
32
+ def pgbus_status_badge(healthy_or_status)
33
+ status = case healthy_or_status
34
+ when true then :healthy
35
+ when Symbol, String then healthy_or_status.to_sym
36
+ else :stale
37
+ end
38
+
39
+ case status
40
+ when :healthy
34
41
  tag.span(I18n.t("pgbus.helpers.status_badge.healthy"),
35
42
  class: "inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800")
43
+ when :stalled
44
+ tag.span(I18n.t("pgbus.helpers.status_badge.stalled"),
45
+ class: "inline-flex items-center rounded-full bg-yellow-100 px-2.5 py-0.5 text-xs font-medium text-yellow-800")
36
46
  else
37
47
  tag.span(I18n.t("pgbus.helpers.status_badge.stale"),
38
48
  class: "inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800")
@@ -17,7 +17,7 @@
17
17
  <td data-label="Kind" class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white"><%= p[:kind] %></td>
18
18
  <td data-label="Host" class="px-4 py-3 text-sm text-gray-500"><%= p[:hostname] %></td>
19
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[:healthy]) %></td>
20
+ <td data-label="Status" class="px-4 py-3 text-sm"><%= pgbus_status_badge(p[:status] || p[:healthy]) %></td>
21
21
  </tr>
22
22
  <% end %>
23
23
  <% if @processes.empty? %>
@@ -19,7 +19,7 @@
19
19
  <% @recent_failures.each do |f| %>
20
20
  <tr>
21
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"><%= f["error_class"] %>: <%= truncate(f["error_message"].to_s, length: 60) %></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
23
  <td data-label="Time" class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(f["failed_at"]) %></td>
24
24
  </tr>
25
25
  <% end %>
@@ -31,6 +31,7 @@
31
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
32
  <%= @stats[:failed_count] %> / <%= @stats[:dlq_depth] %>
33
33
  </p>
34
+ <p class="text-xs text-gray-400 dark:text-gray-500"><%= t("pgbus.dashboard.stats_cards.failed_dlq_labels") %></p>
34
35
  </div>
35
36
 
36
37
  <div class="rounded-lg bg-white dark:bg-gray-800 p-5 shadow ring-1 ring-gray-200 dark:ring-gray-700">
@@ -1,4 +1,11 @@
1
- <turbo-frame id="dlq-messages" data-auto-refresh data-src="<%= pgbus.dead_letter_index_path(frame: 'list') %>">
1
+ <turbo-frame id="dlq-messages" data-auto-refresh data-src="<%= pgbus.dead_letter_index_path(frame: 'list', page: @page) %>">
2
+ <%# Count badge lives inside the frame so it refreshes in lockstep with the table. %>
3
+ <% if @total_count && @total_count > 0 %>
4
+ <p class="mb-3 text-sm text-gray-500 dark:text-gray-400">
5
+ <span class="inline-flex items-center rounded-full bg-red-100 dark:bg-red-900 px-2.5 py-0.5 text-sm font-medium text-red-800 dark:text-red-200"><%= @total_count %></span>
6
+ <%= t("pgbus.dead_letter.messages_table.total_label") %>
7
+ </p>
8
+ <% end %>
2
9
  <%# Bulk form outside table to avoid nested form issues %>
3
10
  <form id="bulk-discard-dlq-form" action="<%= pgbus.discard_selected_dead_letter_index_path %>" method="post" data-turbo-frame="_top" class="hidden">
4
11
  <%= hidden_field_tag :authenticity_token, form_authenticity_token %>
@@ -91,5 +98,30 @@
91
98
  <% end %>
92
99
  </tbody>
93
100
  </table>
101
+ <% if @total_count && @total_count > 0 %>
102
+ <div class="flex items-center justify-between border-t border-gray-200 dark:border-gray-700 px-4 py-3">
103
+ <p class="text-sm text-gray-500 dark:text-gray-400">
104
+ <% from = (@page - 1) * @per_page + 1 %>
105
+ <% to = [from + @messages.size - 1, @total_count].min %>
106
+ <%= t("pgbus.helpers.pagination.showing", from: from, to: to, total: @total_count) %>
107
+ </p>
108
+ <% if @total_pages > 1 %>
109
+ <nav class="inline-flex rounded-lg shadow-sm" role="navigation" aria-label="Pagination">
110
+ <% if @page > 1 %>
111
+ <%= link_to t("pgbus.helpers.pagination.previous"), pgbus.dead_letter_index_path(page: @page - 1),
112
+ class: "px-3 py-1.5 text-xs font-medium border border-gray-300 dark:border-gray-600 rounded-l-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700" %>
113
+ <% else %>
114
+ <span class="px-3 py-1.5 text-xs font-medium border border-gray-300 dark:border-gray-600 rounded-l-lg bg-gray-100 dark:bg-gray-900 text-gray-400 dark:text-gray-600 cursor-not-allowed"><%= t("pgbus.helpers.pagination.previous") %></span>
115
+ <% end %>
116
+ <% if @page < @total_pages %>
117
+ <%= link_to t("pgbus.helpers.pagination.next"), pgbus.dead_letter_index_path(page: @page + 1),
118
+ class: "-ml-px px-3 py-1.5 text-xs font-medium border border-gray-300 dark:border-gray-600 rounded-r-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700" %>
119
+ <% else %>
120
+ <span class="-ml-px px-3 py-1.5 text-xs font-medium border border-gray-300 dark:border-gray-600 rounded-r-lg bg-gray-100 dark:bg-gray-900 text-gray-400 dark:text-gray-600 cursor-not-allowed"><%= t("pgbus.helpers.pagination.next") %></span>
121
+ <% end %>
122
+ </nav>
123
+ <% end %>
124
+ </div>
125
+ <% end %>
94
126
  </div>
95
127
  </turbo-frame>
@@ -4,7 +4,7 @@
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
6
  <nav class="inline-flex rounded-lg shadow-sm" role="group" aria-label="Time range">
7
- <% ranges = [["1h", 60], ["24h", 1440], ["7d", 10_080], ["30d", 43_200]] %>
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
  <%
10
10
  active = @minutes == mins
@@ -51,7 +51,7 @@
51
51
  <%= link_to f["id"], pgbus.job_path(f["id"]), class: "text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
52
52
  </td>
53
53
  <td data-label="Queue" class="px-4 py-3 text-sm text-gray-700"><%= f["queue_name"] %></td>
54
- <td data-label="Error" class="px-4 py-3 text-sm text-red-600 max-w-sm truncate">
54
+ <td data-label="Error" class="px-4 py-3 text-sm text-red-600 max-w-sm truncate" title="<%= f["error_class"] %>: <%= f["error_message"] %>">
55
55
  <span class="font-medium"><%= f["error_class"] %></span>: <%= truncate(f["error_message"].to_s, length: 80) %>
56
56
  </td>
57
57
  <td data-label="Retries" class="px-4 py-3 text-sm text-gray-500"><%= f["retry_count"] %></td>
@@ -17,7 +17,7 @@
17
17
  <td data-label="Kind" class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white"><%= p[:kind] %></td>
18
18
  <td data-label="Host" class="px-4 py-3 text-sm text-gray-700"><%= p[:hostname] %></td>
19
19
  <td data-label="PID" class="px-4 py-3 text-sm font-mono text-gray-700"><%= p[:pid] %></td>
20
- <td data-label="Status" class="px-4 py-3 text-sm"><%= pgbus_status_badge(p[:healthy]) %></td>
20
+ <td data-label="Status" class="px-4 py-3 text-sm"><%= pgbus_status_badge(p[:status] || p[:healthy]) %></td>
21
21
  <td data-label="Last Heartbeat" class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(p[:last_heartbeat_at]) %></td>
22
22
  <td data-label="Metadata" class="px-4 py-3 text-sm text-gray-500 font-mono text-xs max-w-xs truncate">
23
23
  <% if p[:metadata].is_a?(Hash) %>
@@ -1,3 +1,6 @@
1
+ <div class="mb-3">
2
+ <%= link_to "← #{t("pgbus.queues.show.back")}", pgbus.queues_path, class: "text-sm text-indigo-600 hover:text-indigo-500 dark:text-indigo-400" %>
3
+ </div>
1
4
  <div class="flex items-center justify-between mb-6">
2
5
  <div>
3
6
  <h1 class="text-2xl font-bold text-gray-900 dark:text-white">
@@ -84,6 +84,7 @@ da:
84
84
  stats_cards:
85
85
  enqueued: Sat i kø
86
86
  failed_dlq: Mislykkedes / DLQ
87
+ failed_dlq_labels: Fejlet / Dead Lettered
87
88
  processes: Processer
88
89
  queues: Køer
89
90
  recurring: Gentagende
@@ -126,6 +127,7 @@ da:
126
127
  queue: 'Kø:'
127
128
  visible_at: 'Synlig ved:'
128
129
  retry: Forsøg igen
130
+ total_label: beskeder i dead letter-kø
129
131
  show:
130
132
  back: Tilbage til DLQ
131
133
  discard: Kassér
@@ -225,6 +227,10 @@ da:
225
227
  bulk_select_all: Vælg alle
226
228
  bulk_select_row: Vælg %{id}
227
229
  bulk_selected: valgt
230
+ pagination:
231
+ next: Næste
232
+ previous: Forrige
233
+ showing: Viser %{from}–%{to} af %{total}
228
234
  paused_badge: Pause
229
235
  queue_badge:
230
236
  dlq: DLQ
@@ -235,6 +241,7 @@ da:
235
241
  status_badge:
236
242
  healthy: Sund
237
243
  stale: Forældet
244
+ stalled: Gået i stå
238
245
  insights:
239
246
  show:
240
247
  charts:
@@ -476,6 +483,7 @@ da:
476
483
  resume: Genoptag
477
484
  show:
478
485
  arguments: Argumenter
486
+ back: Tilbage til køer
479
487
  delete_confirm: Slet denne kø permanent? Dette kan ikke fortrydes.
480
488
  delete_queue: Slet kø
481
489
  depth: 'Dybde:'
@@ -84,6 +84,7 @@ de:
84
84
  stats_cards:
85
85
  enqueued: Eingereiht
86
86
  failed_dlq: Fehlgeschlagen / DLQ
87
+ failed_dlq_labels: Fehlgeschlagen / Dead Lettered
87
88
  processes: Prozesse
88
89
  queues: Warteschlangen
89
90
  recurring: Wiederkehrend
@@ -126,6 +127,7 @@ de:
126
127
  queue: 'Warteschlange:'
127
128
  visible_at: 'Sichtbar um:'
128
129
  retry: Erneut versuchen
130
+ total_label: Nachrichten in der Dead-Letter-Warteschlange
129
131
  show:
130
132
  back: Zurück zur DLQ
131
133
  discard: Verwerfen
@@ -225,6 +227,10 @@ de:
225
227
  bulk_select_all: Alle auswählen
226
228
  bulk_select_row: "%{id} auswählen"
227
229
  bulk_selected: ausgewählt
230
+ pagination:
231
+ next: Weiter
232
+ previous: Zurück
233
+ showing: Zeige %{from}–%{to} von %{total}
228
234
  paused_badge: Pausiert
229
235
  queue_badge:
230
236
  dlq: DLQ
@@ -235,6 +241,7 @@ de:
235
241
  status_badge:
236
242
  healthy: Gesund
237
243
  stale: Veraltet
244
+ stalled: Blockiert
238
245
  insights:
239
246
  show:
240
247
  charts:
@@ -476,6 +483,7 @@ de:
476
483
  resume: Fortsetzen
477
484
  show:
478
485
  arguments: Argumente
486
+ back: Zurück zu Warteschlangen
479
487
  delete_confirm: Diese Warteschlange dauerhaft löschen? Dies kann nicht rückgängig gemacht werden.
480
488
  delete_queue: Warteschlange löschen
481
489
  depth: 'Tiefe:'
@@ -84,6 +84,7 @@ en:
84
84
  stats_cards:
85
85
  enqueued: Enqueued
86
86
  failed_dlq: Failed / DLQ
87
+ failed_dlq_labels: Failed / Dead Lettered
87
88
  processes: Processes
88
89
  queues: Queues
89
90
  recurring: Recurring
@@ -126,6 +127,7 @@ en:
126
127
  queue: 'Queue:'
127
128
  visible_at: 'Visible at:'
128
129
  retry: Retry
130
+ total_label: messages in dead letter queue
129
131
  show:
130
132
  back: Back to DLQ
131
133
  discard: Discard
@@ -225,6 +227,10 @@ en:
225
227
  bulk_select_all: Select all
226
228
  bulk_select_row: Select %{id}
227
229
  bulk_selected: selected
230
+ pagination:
231
+ next: Next
232
+ previous: Previous
233
+ showing: Showing %{from}–%{to} of %{total}
228
234
  paused_badge: Paused
229
235
  queue_badge:
230
236
  dlq: DLQ
@@ -235,6 +241,7 @@ en:
235
241
  status_badge:
236
242
  healthy: Healthy
237
243
  stale: Stale
244
+ stalled: Stalled
238
245
  insights:
239
246
  show:
240
247
  charts:
@@ -476,6 +483,7 @@ en:
476
483
  resume: Resume
477
484
  show:
478
485
  arguments: Arguments
486
+ back: Back to Queues
479
487
  delete_confirm: Permanently delete this queue? This cannot be undone.
480
488
  delete_queue: Delete Queue
481
489
  depth: 'Depth:'
@@ -84,6 +84,7 @@ es:
84
84
  stats_cards:
85
85
  enqueued: Encolado
86
86
  failed_dlq: Fallidos / DLQ
87
+ failed_dlq_labels: Fallidos / Dead Lettered
87
88
  processes: Procesos
88
89
  queues: Colas
89
90
  recurring: Recurrente
@@ -126,6 +127,7 @@ es:
126
127
  queue: 'Cola:'
127
128
  visible_at: 'Visible en:'
128
129
  retry: Reintentar
130
+ total_label: mensajes en la cola de mensajes fallidos
129
131
  show:
130
132
  back: Volver a DLQ
131
133
  discard: Descartar
@@ -225,6 +227,10 @@ es:
225
227
  bulk_select_all: Seleccionar todo
226
228
  bulk_select_row: Seleccionar %{id}
227
229
  bulk_selected: seleccionado
230
+ pagination:
231
+ next: Siguiente
232
+ previous: Anterior
233
+ showing: Mostrando %{from}–%{to} de %{total}
228
234
  paused_badge: Pausado
229
235
  queue_badge:
230
236
  dlq: DLQ
@@ -235,6 +241,7 @@ es:
235
241
  status_badge:
236
242
  healthy: Saludable
237
243
  stale: Obsoleto
244
+ stalled: Bloqueado
238
245
  insights:
239
246
  show:
240
247
  charts:
@@ -476,6 +483,7 @@ es:
476
483
  resume: Reanudar
477
484
  show:
478
485
  arguments: Argumentos
486
+ back: Volver a colas
479
487
  delete_confirm: "¿Eliminar permanentemente esta cola? Esto no se puede deshacer."
480
488
  delete_queue: Eliminar cola
481
489
  depth: 'Profundidad:'
@@ -84,6 +84,7 @@ fi:
84
84
  stats_cards:
85
85
  enqueued: Lisätty jonoon
86
86
  failed_dlq: Epäonnistuneet / DLQ
87
+ failed_dlq_labels: Epäonnistuneet / Dead Lettered
87
88
  processes: Prosessit
88
89
  queues: Jonot
89
90
  recurring: Toistuva
@@ -126,6 +127,7 @@ fi:
126
127
  queue: 'Jono:'
127
128
  visible_at: 'Näkyvissä klo:'
128
129
  retry: Yritä uudelleen
130
+ total_label: viestiä dead letter -jonossa
129
131
  show:
130
132
  back: Takaisin DLQ:hon
131
133
  discard: Hylkää
@@ -225,6 +227,10 @@ fi:
225
227
  bulk_select_all: Valitse kaikki
226
228
  bulk_select_row: Valitse %{id}
227
229
  bulk_selected: valittu
230
+ pagination:
231
+ next: Seuraava
232
+ previous: Edellinen
233
+ showing: Näytetään %{from}–%{to} / %{total}
228
234
  paused_badge: Tauolla
229
235
  queue_badge:
230
236
  dlq: DLQ
@@ -235,6 +241,7 @@ fi:
235
241
  status_badge:
236
242
  healthy: Toimiva
237
243
  stale: Vanha
244
+ stalled: Juuttunut
238
245
  insights:
239
246
  show:
240
247
  charts:
@@ -476,6 +483,7 @@ fi:
476
483
  resume: Jatka
477
484
  show:
478
485
  arguments: Argumentit
486
+ back: Takaisin jonoihin
479
487
  delete_confirm: Poista tämä jono pysyvästi? Tätä ei voi kumota.
480
488
  delete_queue: Poista jono
481
489
  depth: 'Syvyys:'
@@ -84,6 +84,7 @@ fr:
84
84
  stats_cards:
85
85
  enqueued: Enregistré
86
86
  failed_dlq: Échoué / DLQ
87
+ failed_dlq_labels: Échoués / Dead Lettered
87
88
  processes: Processus
88
89
  queues: Files d'attente
89
90
  recurring: Récurrent
@@ -126,6 +127,7 @@ fr:
126
127
  queue: 'File d''attente :'
127
128
  visible_at: 'Visible à :'
128
129
  retry: Réessayer
130
+ total_label: messages dans la file de lettres mortes
129
131
  show:
130
132
  back: Retour à la DLQ
131
133
  discard: Ignorer
@@ -225,6 +227,10 @@ fr:
225
227
  bulk_select_all: Tout sélectionner
226
228
  bulk_select_row: Sélectionner %{id}
227
229
  bulk_selected: sélectionné
230
+ pagination:
231
+ next: Suivant
232
+ previous: Précédent
233
+ showing: Affichage de %{from}–%{to} sur %{total}
228
234
  paused_badge: En pause
229
235
  queue_badge:
230
236
  dlq: DLQ
@@ -235,6 +241,7 @@ fr:
235
241
  status_badge:
236
242
  healthy: Sain
237
243
  stale: Obsolète
244
+ stalled: Bloqué
238
245
  insights:
239
246
  show:
240
247
  charts:
@@ -476,6 +483,7 @@ fr:
476
483
  resume: Reprendre
477
484
  show:
478
485
  arguments: Arguments
486
+ back: Retour aux files d'attente
479
487
  delete_confirm: Supprimer définitivement cette file d'attente ? Cette action est irréversible.
480
488
  delete_queue: Supprimer la file d'attente
481
489
  depth: 'Profondeur :'
@@ -84,6 +84,7 @@ it:
84
84
  stats_cards:
85
85
  enqueued: In coda
86
86
  failed_dlq: Falliti / DLQ
87
+ failed_dlq_labels: Falliti / Dead Lettered
87
88
  processes: Processi
88
89
  queues: Code
89
90
  recurring: Ricorrente
@@ -126,6 +127,7 @@ it:
126
127
  queue: 'Coda:'
127
128
  visible_at: 'Visibile alle:'
128
129
  retry: Riprova
130
+ total_label: messaggi nella coda dei messaggi non recapitati
129
131
  show:
130
132
  back: Torna a DLQ
131
133
  discard: Scarta
@@ -225,6 +227,10 @@ it:
225
227
  bulk_select_all: Seleziona tutto
226
228
  bulk_select_row: Seleziona %{id}
227
229
  bulk_selected: selezionato
230
+ pagination:
231
+ next: Avanti
232
+ previous: Indietro
233
+ showing: Visualizzazione %{from}–%{to} di %{total}
228
234
  paused_badge: In pausa
229
235
  queue_badge:
230
236
  dlq: DLQ
@@ -235,6 +241,7 @@ it:
235
241
  status_badge:
236
242
  healthy: Sano
237
243
  stale: Obsoleto
244
+ stalled: Bloccato
238
245
  insights:
239
246
  show:
240
247
  charts:
@@ -476,6 +483,7 @@ it:
476
483
  resume: Riprendi
477
484
  show:
478
485
  arguments: Argomenti
486
+ back: Torna alle code
479
487
  delete_confirm: Eliminare permanentemente questa coda? Questa azione non può essere annullata.
480
488
  delete_queue: Elimina coda
481
489
  depth: 'Profondità:'