pgbus 0.0.1

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 (89) hide show
  1. checksums.yaml +7 -0
  2. data/.bun-version +1 -0
  3. data/.claude/commands/architect.md +100 -0
  4. data/.claude/commands/github-review-comments.md +237 -0
  5. data/.claude/commands/lfg.md +271 -0
  6. data/.claude/commands/review-pr.md +69 -0
  7. data/.claude/commands/security.md +122 -0
  8. data/.claude/commands/tdd.md +148 -0
  9. data/.claude/rules/agents.md +49 -0
  10. data/.claude/rules/coding-style.md +91 -0
  11. data/.claude/rules/git-workflow.md +56 -0
  12. data/.claude/rules/performance.md +73 -0
  13. data/.claude/rules/testing.md +67 -0
  14. data/CHANGELOG.md +5 -0
  15. data/CLAUDE.md +80 -0
  16. data/CODE_OF_CONDUCT.md +10 -0
  17. data/LICENSE.txt +21 -0
  18. data/README.md +417 -0
  19. data/Rakefile +14 -0
  20. data/app/controllers/pgbus/api/stats_controller.rb +11 -0
  21. data/app/controllers/pgbus/application_controller.rb +35 -0
  22. data/app/controllers/pgbus/dashboard_controller.rb +27 -0
  23. data/app/controllers/pgbus/dead_letter_controller.rb +50 -0
  24. data/app/controllers/pgbus/events_controller.rb +23 -0
  25. data/app/controllers/pgbus/jobs_controller.rb +48 -0
  26. data/app/controllers/pgbus/processes_controller.rb +10 -0
  27. data/app/controllers/pgbus/queues_controller.rb +21 -0
  28. data/app/helpers/pgbus/application_helper.rb +69 -0
  29. data/app/views/layouts/pgbus/application.html.erb +76 -0
  30. data/app/views/pgbus/dashboard/_processes_table.html.erb +30 -0
  31. data/app/views/pgbus/dashboard/_queues_table.html.erb +39 -0
  32. data/app/views/pgbus/dashboard/_recent_failures.html.erb +33 -0
  33. data/app/views/pgbus/dashboard/_stats_cards.html.erb +28 -0
  34. data/app/views/pgbus/dashboard/show.html.erb +10 -0
  35. data/app/views/pgbus/dead_letter/_messages_table.html.erb +40 -0
  36. data/app/views/pgbus/dead_letter/index.html.erb +15 -0
  37. data/app/views/pgbus/dead_letter/show.html.erb +52 -0
  38. data/app/views/pgbus/events/index.html.erb +57 -0
  39. data/app/views/pgbus/events/show.html.erb +28 -0
  40. data/app/views/pgbus/jobs/_enqueued_table.html.erb +34 -0
  41. data/app/views/pgbus/jobs/_failed_table.html.erb +45 -0
  42. data/app/views/pgbus/jobs/index.html.erb +16 -0
  43. data/app/views/pgbus/jobs/show.html.erb +57 -0
  44. data/app/views/pgbus/processes/_processes_table.html.erb +37 -0
  45. data/app/views/pgbus/processes/index.html.erb +3 -0
  46. data/app/views/pgbus/queues/_queues_list.html.erb +41 -0
  47. data/app/views/pgbus/queues/index.html.erb +3 -0
  48. data/app/views/pgbus/queues/show.html.erb +49 -0
  49. data/bun.lock +18 -0
  50. data/config/routes.rb +45 -0
  51. data/docs/README.md +28 -0
  52. data/docs/switch_from_good_job.md +279 -0
  53. data/docs/switch_from_sidekiq.md +226 -0
  54. data/docs/switch_from_solid_queue.md +247 -0
  55. data/exe/pgbus +7 -0
  56. data/lib/generators/pgbus/install_generator.rb +56 -0
  57. data/lib/generators/pgbus/templates/migration.rb.erb +114 -0
  58. data/lib/generators/pgbus/templates/pgbus.yml.erb +74 -0
  59. data/lib/generators/pgbus/templates/pgbus_binstub.erb +7 -0
  60. data/lib/pgbus/active_job/adapter.rb +109 -0
  61. data/lib/pgbus/active_job/executor.rb +107 -0
  62. data/lib/pgbus/batch.rb +153 -0
  63. data/lib/pgbus/cli.rb +84 -0
  64. data/lib/pgbus/client.rb +162 -0
  65. data/lib/pgbus/concurrency/blocked_execution.rb +74 -0
  66. data/lib/pgbus/concurrency/semaphore.rb +66 -0
  67. data/lib/pgbus/concurrency.rb +65 -0
  68. data/lib/pgbus/config_loader.rb +27 -0
  69. data/lib/pgbus/configuration.rb +99 -0
  70. data/lib/pgbus/engine.rb +31 -0
  71. data/lib/pgbus/event.rb +31 -0
  72. data/lib/pgbus/event_bus/handler.rb +76 -0
  73. data/lib/pgbus/event_bus/publisher.rb +42 -0
  74. data/lib/pgbus/event_bus/registry.rb +54 -0
  75. data/lib/pgbus/event_bus/subscriber.rb +30 -0
  76. data/lib/pgbus/process/consumer.rb +113 -0
  77. data/lib/pgbus/process/dispatcher.rb +154 -0
  78. data/lib/pgbus/process/heartbeat.rb +71 -0
  79. data/lib/pgbus/process/signal_handler.rb +49 -0
  80. data/lib/pgbus/process/supervisor.rb +198 -0
  81. data/lib/pgbus/process/worker.rb +153 -0
  82. data/lib/pgbus/serializer.rb +43 -0
  83. data/lib/pgbus/version.rb +5 -0
  84. data/lib/pgbus/web/authentication.rb +24 -0
  85. data/lib/pgbus/web/data_source.rb +406 -0
  86. data/lib/pgbus.rb +49 -0
  87. data/package.json +9 -0
  88. data/sig/pgbus.rbs +4 -0
  89. metadata +198 -0
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ module Pgbus
6
+ module ApplicationHelper
7
+ def pgbus_time_ago(time)
8
+ return "—" unless time
9
+
10
+ time = Time.parse(time) if time.is_a?(String)
11
+ seconds = (Time.now - time).to_i
12
+
13
+ case seconds
14
+ when 0..59 then "#{seconds}s ago"
15
+ when 60..3599 then "#{seconds / 60}m ago"
16
+ when 3600..86_399 then "#{seconds / 3600}h ago"
17
+ else "#{seconds / 86_400}d ago"
18
+ end
19
+ end
20
+
21
+ def pgbus_number(n)
22
+ return "0" unless n
23
+
24
+ n = n.to_i
25
+ case n
26
+ when 0..999 then n.to_s
27
+ when 1_000..999_999 then "#{(n / 1_000.0).round(1)}K"
28
+ else "#{(n / 1_000_000.0).round(1)}M"
29
+ end
30
+ end
31
+
32
+ def pgbus_status_badge(healthy)
33
+ if healthy
34
+ tag.span("Healthy", class: "inline-flex items-center rounded-full bg-green-100 px-2.5 py-0.5 text-xs font-medium text-green-800")
35
+ else
36
+ tag.span("Stale", class: "inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800")
37
+ end
38
+ end
39
+
40
+ def pgbus_queue_badge(name)
41
+ if name.to_s.end_with?("_dlq")
42
+ tag.span("DLQ", class: "inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700")
43
+ else
44
+ tag.span("Queue", class: "inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700")
45
+ end
46
+ end
47
+
48
+ def pgbus_json_preview(json_string, max_length: 120)
49
+ return "—" unless json_string
50
+
51
+ text = json_string.is_a?(String) ? json_string : JSON.generate(json_string)
52
+ text.length > max_length ? "#{text[0...max_length]}..." : text
53
+ end
54
+
55
+ def pgbus_refresh_interval
56
+ Pgbus.configuration.web_refresh_interval
57
+ end
58
+
59
+ def pgbus_nav_link(label, path)
60
+ active = request.path == path || (path != pgbus.root_path && request.path.start_with?(path))
61
+ css = if active
62
+ "rounded-md px-3 py-2 text-sm font-medium text-white bg-gray-800"
63
+ else
64
+ "rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:text-white hover:bg-gray-700"
65
+ end
66
+ link_to label, path, class: css
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,76 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" class="h-full bg-gray-50">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Pgbus Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script type="module">
9
+ import * as Turbo from "https://cdn.jsdelivr.net/npm/@hotwired/turbo@8/dist/turbo.es2017.esm.js";
10
+
11
+ <% if Pgbus.configuration.web_live_updates %>
12
+ // Auto-refresh turbo frames, pausing when tab is hidden
13
+ const interval = <%= Pgbus.configuration.web_refresh_interval %>;
14
+ if (interval > 0) {
15
+ let timer;
16
+ function refreshFrames() {
17
+ if (document.hidden) return;
18
+ document.querySelectorAll("turbo-frame[data-auto-refresh]")
19
+ .forEach(frame => frame.reload());
20
+ }
21
+ function start() { timer = setInterval(refreshFrames, interval); }
22
+ function stop() { clearInterval(timer); }
23
+ document.addEventListener("visibilitychange", () => document.hidden ? stop() : start());
24
+ start();
25
+ }
26
+ <% end %>
27
+ </script>
28
+ </head>
29
+ <body class="h-full">
30
+ <div class="min-h-full">
31
+ <!-- Top nav -->
32
+ <nav class="bg-gray-900">
33
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
34
+ <div class="flex h-14 items-center justify-between">
35
+ <div class="flex items-center space-x-8">
36
+ <%= link_to pgbus.root_path, class: "flex items-center space-x-2" do %>
37
+ <span class="text-lg font-bold text-white">Pgbus</span>
38
+ <span class="rounded bg-gray-700 px-1.5 py-0.5 text-xs text-gray-300"><%= Pgbus::VERSION %></span>
39
+ <% end %>
40
+
41
+ <div class="flex space-x-1">
42
+ <%= pgbus_nav_link "Dashboard", pgbus.root_path %>
43
+ <%= pgbus_nav_link "Queues", pgbus.queues_path %>
44
+ <%= pgbus_nav_link "Jobs", pgbus.jobs_path %>
45
+ <%= pgbus_nav_link "Processes", pgbus.processes_path %>
46
+ <%= pgbus_nav_link "Events", pgbus.events_path %>
47
+ <%= pgbus_nav_link "DLQ", pgbus.dead_letter_index_path %>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ </div>
52
+ </nav>
53
+
54
+ <!-- Flash messages -->
55
+ <% if notice %>
56
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 mt-4">
57
+ <div class="rounded-md bg-green-50 p-3">
58
+ <p class="text-sm text-green-800"><%= notice %></p>
59
+ </div>
60
+ </div>
61
+ <% end %>
62
+ <% if alert %>
63
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 mt-4">
64
+ <div class="rounded-md bg-red-50 p-3">
65
+ <p class="text-sm text-red-800"><%= alert %></p>
66
+ </div>
67
+ </div>
68
+ <% end %>
69
+
70
+ <!-- Content -->
71
+ <main class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-6">
72
+ <%= yield %>
73
+ </main>
74
+ </div>
75
+ </body>
76
+ </html>
@@ -0,0 +1,30 @@
1
+ <turbo-frame id="dashboard-processes" data-auto-refresh src="<%= pgbus.root_path(frame: 'processes') %>">
2
+ <div>
3
+ <h2 class="text-lg font-semibold text-gray-900 mb-3">Active Processes</h2>
4
+ <div class="overflow-hidden rounded-lg bg-white shadow ring-1 ring-gray-200">
5
+ <table class="min-w-full divide-y divide-gray-200">
6
+ <thead class="bg-gray-50">
7
+ <tr>
8
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Kind</th>
9
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Host</th>
10
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">PID</th>
11
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Status</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody class="divide-y divide-gray-100">
15
+ <% @processes.each do |p| %>
16
+ <tr>
17
+ <td class="px-4 py-3 text-sm font-medium text-gray-900"><%= p[:kind] %></td>
18
+ <td class="px-4 py-3 text-sm text-gray-500"><%= p[:hostname] %></td>
19
+ <td class="px-4 py-3 text-sm text-gray-500"><%= p[:pid] %></td>
20
+ <td class="px-4 py-3 text-sm"><%= pgbus_status_badge(p[:healthy]) %></td>
21
+ </tr>
22
+ <% end %>
23
+ <% if @processes.empty? %>
24
+ <tr><td colspan="4" class="px-4 py-8 text-center text-sm text-gray-400">No processes running</td></tr>
25
+ <% end %>
26
+ </tbody>
27
+ </table>
28
+ </div>
29
+ </div>
30
+ </turbo-frame>
@@ -0,0 +1,39 @@
1
+ <turbo-frame id="dashboard-queues" data-auto-refresh src="<%= pgbus.root_path(frame: 'queues') %>">
2
+ <div class="mb-8">
3
+ <div class="flex items-center justify-between mb-3">
4
+ <h2 class="text-lg font-semibold text-gray-900">Queues</h2>
5
+ <%= link_to "View all", pgbus.queues_path, class: "text-sm text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
6
+ </div>
7
+
8
+ <div class="overflow-hidden rounded-lg bg-white shadow ring-1 ring-gray-200">
9
+ <table class="min-w-full divide-y divide-gray-200">
10
+ <thead class="bg-gray-50">
11
+ <tr>
12
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Queue</th>
13
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Depth</th>
14
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Visible</th>
15
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Oldest (s)</th>
16
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Total</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody class="divide-y divide-gray-100">
20
+ <% @queues.each do |q| %>
21
+ <tr class="hover:bg-gray-50">
22
+ <td class="px-4 py-3 text-sm">
23
+ <%= link_to q[:name], pgbus.queue_path(name: q[:name]), class: "font-medium text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
24
+ <%= pgbus_queue_badge(q[:name]) %>
25
+ </td>
26
+ <td class="px-4 py-3 text-sm text-right text-gray-700"><%= pgbus_number(q[:queue_length]) %></td>
27
+ <td class="px-4 py-3 text-sm text-right text-gray-700"><%= pgbus_number(q[:queue_visible_length]) %></td>
28
+ <td class="px-4 py-3 text-sm text-right text-gray-500"><%= q[:oldest_msg_age_sec] || "—" %></td>
29
+ <td class="px-4 py-3 text-sm text-right text-gray-500"><%= pgbus_number(q[:total_messages]) %></td>
30
+ </tr>
31
+ <% end %>
32
+ <% if @queues.empty? %>
33
+ <tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400">No queues found</td></tr>
34
+ <% end %>
35
+ </tbody>
36
+ </table>
37
+ </div>
38
+ </div>
39
+ </turbo-frame>
@@ -0,0 +1,33 @@
1
+ <turbo-frame id="dashboard-failures" data-auto-refresh src="<%= pgbus.root_path(frame: 'failures') %>">
2
+ <div>
3
+ <div class="flex items-center justify-between mb-3">
4
+ <h2 class="text-lg font-semibold text-gray-900">Recent Failures</h2>
5
+ <% if @recent_failures.any? %>
6
+ <%= link_to "View all", pgbus.jobs_path(status: "failed"), class: "text-sm text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
7
+ <% end %>
8
+ </div>
9
+ <div class="overflow-hidden rounded-lg bg-white shadow ring-1 ring-gray-200">
10
+ <table class="min-w-full divide-y divide-gray-200">
11
+ <thead class="bg-gray-50">
12
+ <tr>
13
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Queue</th>
14
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Error</th>
15
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">When</th>
16
+ </tr>
17
+ </thead>
18
+ <tbody class="divide-y divide-gray-100">
19
+ <% @recent_failures.each do |f| %>
20
+ <tr>
21
+ <td class="px-4 py-3 text-sm text-gray-700"><%= f["queue_name"] %></td>
22
+ <td 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>
23
+ <td class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(f["failed_at"]) %></td>
24
+ </tr>
25
+ <% end %>
26
+ <% if @recent_failures.empty? %>
27
+ <tr><td colspan="3" class="px-4 py-8 text-center text-sm text-gray-400">No failures</td></tr>
28
+ <% end %>
29
+ </tbody>
30
+ </table>
31
+ </div>
32
+ </div>
33
+ </turbo-frame>
@@ -0,0 +1,28 @@
1
+ <turbo-frame id="dashboard-stats" data-auto-refresh src="<%= pgbus.root_path(frame: 'stats') %>">
2
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4 mb-8">
3
+ <div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
4
+ <p class="text-sm font-medium text-gray-500">Queues</p>
5
+ <p class="mt-1 text-3xl font-semibold text-gray-900"><%= @stats[:total_queues] %></p>
6
+ </div>
7
+
8
+ <div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
9
+ <p class="text-sm font-medium text-gray-500">Enqueued</p>
10
+ <p class="mt-1 text-3xl font-semibold text-gray-900"><%= pgbus_number(@stats[:total_depth]) %></p>
11
+ <p class="text-xs text-gray-400"><%= pgbus_number(@stats[:total_visible]) %> visible</p>
12
+ </div>
13
+
14
+ <div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
15
+ <p class="text-sm font-medium text-gray-500">Processes</p>
16
+ <p class="mt-1 text-3xl font-semibold <%= @stats[:active_processes] > 0 ? 'text-green-600' : 'text-gray-400' %>">
17
+ <%= @stats[:active_processes] %>
18
+ </p>
19
+ </div>
20
+
21
+ <div class="rounded-lg bg-white p-5 shadow ring-1 ring-gray-200">
22
+ <p class="text-sm font-medium text-gray-500">Failed / DLQ</p>
23
+ <p class="mt-1 text-3xl font-semibold <%= (@stats[:failed_count] + @stats[:dlq_depth]) > 0 ? 'text-red-600' : 'text-gray-900' %>">
24
+ <%= @stats[:failed_count] %> / <%= @stats[:dlq_depth] %>
25
+ </p>
26
+ </div>
27
+ </div>
28
+ </turbo-frame>
@@ -0,0 +1,10 @@
1
+ <h1 class="text-2xl font-bold text-gray-900 mb-6">Dashboard</h1>
2
+
3
+ <%= render "pgbus/dashboard/stats_cards" %>
4
+
5
+ <%= render "pgbus/dashboard/queues_table" %>
6
+
7
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
8
+ <%= render "pgbus/dashboard/processes_table" %>
9
+ <%= render "pgbus/dashboard/recent_failures" %>
10
+ </div>
@@ -0,0 +1,40 @@
1
+ <turbo-frame id="dlq-messages" data-auto-refresh src="<%= pgbus.dead_letter_index_path(frame: 'list') %>">
2
+ <div class="overflow-hidden rounded-lg bg-white shadow ring-1 ring-gray-200">
3
+ <table class="min-w-full divide-y divide-gray-200">
4
+ <thead class="bg-gray-50">
5
+ <tr>
6
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">ID</th>
7
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Source Queue</th>
8
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Enqueued</th>
9
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Reads</th>
10
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Payload</th>
11
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Actions</th>
12
+ </tr>
13
+ </thead>
14
+ <tbody class="divide-y divide-gray-100">
15
+ <% @messages.each do |m| %>
16
+ <tr class="hover:bg-gray-50">
17
+ <td class="px-4 py-3 text-sm font-mono text-gray-900"><%= m[:msg_id] %></td>
18
+ <td class="px-4 py-3 text-sm text-gray-700"><%= m[:queue_name] %></td>
19
+ <td class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(m[:enqueued_at]) %></td>
20
+ <td class="px-4 py-3 text-sm text-gray-500"><%= m[:read_ct] %></td>
21
+ <td class="px-4 py-3 text-sm text-gray-600 font-mono text-xs max-w-md truncate">
22
+ <%= pgbus_json_preview(m[:message]) %>
23
+ </td>
24
+ <td class="px-4 py-3 text-sm text-right space-x-2">
25
+ <%= button_to "Retry", pgbus.retry_dead_letter_path(m[:msg_id], queue_name: m[:queue_name]), method: :post,
26
+ class: "text-xs text-indigo-600 hover:text-indigo-800 font-medium",
27
+ data: { turbo_frame: "_top" } %>
28
+ <%= button_to "Discard", pgbus.discard_dead_letter_path(m[:msg_id], queue_name: m[:queue_name]), method: :post,
29
+ class: "text-xs text-red-600 hover:text-red-800 font-medium",
30
+ data: { turbo_confirm: "Permanently discard?", turbo_frame: "_top" } %>
31
+ </td>
32
+ </tr>
33
+ <% end %>
34
+ <% if @messages.empty? %>
35
+ <tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400">Dead letter queue is empty</td></tr>
36
+ <% end %>
37
+ </tbody>
38
+ </table>
39
+ </div>
40
+ </turbo-frame>
@@ -0,0 +1,15 @@
1
+ <div class="flex items-center justify-between mb-6">
2
+ <h1 class="text-2xl font-bold text-gray-900">Dead Letter Queue</h1>
3
+ <% if @messages.any? %>
4
+ <div class="flex space-x-2">
5
+ <%= button_to "Retry All", pgbus.retry_all_dead_letter_index_path, method: :post,
6
+ class: "rounded-md bg-indigo-600 px-3 py-2 text-sm font-medium text-white hover:bg-indigo-500",
7
+ data: { turbo_confirm: "Retry all DLQ messages?" } %>
8
+ <%= button_to "Discard All", pgbus.discard_all_dead_letter_index_path, method: :post,
9
+ class: "rounded-md bg-red-600 px-3 py-2 text-sm font-medium text-white hover:bg-red-500",
10
+ data: { turbo_confirm: "Permanently discard all DLQ messages?" } %>
11
+ </div>
12
+ <% end %>
13
+ </div>
14
+
15
+ <%= render "pgbus/dead_letter/messages_table" %>
@@ -0,0 +1,52 @@
1
+ <div class="mb-6">
2
+ <%= link_to "&larr; Back to DLQ".html_safe, pgbus.dead_letter_index_path, class: "text-sm text-indigo-600 hover:text-indigo-500" %>
3
+ </div>
4
+
5
+ <% if @message %>
6
+ <h1 class="text-2xl font-bold text-gray-900 mb-6">DLQ Message #<%= @message[:msg_id] %></h1>
7
+
8
+ <div class="rounded-lg bg-white shadow ring-1 ring-gray-200 p-6 mb-6">
9
+ <dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
10
+ <div>
11
+ <dt class="text-sm font-medium text-gray-500">Source Queue</dt>
12
+ <dd class="text-sm text-gray-900"><%= @message[:queue_name] %></dd>
13
+ </div>
14
+ <div>
15
+ <dt class="text-sm font-medium text-gray-500">Enqueued At</dt>
16
+ <dd class="text-sm text-gray-900"><%= @message[:enqueued_at] %></dd>
17
+ </div>
18
+ <div>
19
+ <dt class="text-sm font-medium text-gray-500">Read Count</dt>
20
+ <dd class="text-sm text-gray-900"><%= @message[:read_ct] %></dd>
21
+ </div>
22
+ <div>
23
+ <dt class="text-sm font-medium text-gray-500">Visibility Timeout</dt>
24
+ <dd class="text-sm text-gray-900"><%= @message[:vt] %></dd>
25
+ </div>
26
+ </dl>
27
+ </div>
28
+
29
+ <div class="rounded-lg bg-white shadow ring-1 ring-gray-200 p-6 mb-6">
30
+ <h2 class="text-sm font-medium text-gray-500 mb-2">Payload</h2>
31
+ <pre class="text-xs text-gray-600 bg-gray-50 rounded p-4 overflow-x-auto"><%= JSON.pretty_generate(JSON.parse(@message[:message])) rescue @message[:message] %></pre>
32
+ </div>
33
+
34
+ <% if @message[:headers] %>
35
+ <div class="rounded-lg bg-white shadow ring-1 ring-gray-200 p-6 mb-6">
36
+ <h2 class="text-sm font-medium text-gray-500 mb-2">Headers</h2>
37
+ <pre class="text-xs text-gray-600 bg-gray-50 rounded p-4 overflow-x-auto"><%= JSON.pretty_generate(JSON.parse(@message[:headers])) rescue @message[:headers] %></pre>
38
+ </div>
39
+ <% end %>
40
+
41
+ <div class="flex space-x-2">
42
+ <%= button_to "Retry", pgbus.retry_dead_letter_path(@message[:msg_id], queue_name: @message[:queue_name]), method: :post,
43
+ class: "rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-500" %>
44
+ <%= button_to "Discard", pgbus.discard_dead_letter_path(@message[:msg_id], queue_name: @message[:queue_name]), method: :post,
45
+ class: "rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white hover:bg-red-500",
46
+ data: { turbo_confirm: "Permanently discard?" } %>
47
+ </div>
48
+ <% else %>
49
+ <div class="text-center py-12">
50
+ <p class="text-gray-400">Message not found</p>
51
+ </div>
52
+ <% end %>
@@ -0,0 +1,57 @@
1
+ <h1 class="text-2xl font-bold text-gray-900 mb-6">Events</h1>
2
+
3
+ <!-- Registered Subscribers -->
4
+ <div class="mb-8">
5
+ <h2 class="text-lg font-semibold text-gray-900 mb-3">Registered Subscribers</h2>
6
+ <div class="overflow-hidden rounded-lg bg-white shadow ring-1 ring-gray-200">
7
+ <table class="min-w-full divide-y divide-gray-200">
8
+ <thead class="bg-gray-50">
9
+ <tr>
10
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Pattern</th>
11
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Handler</th>
12
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Queue</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody class="divide-y divide-gray-100">
16
+ <% @subscribers.each do |s| %>
17
+ <tr class="hover:bg-gray-50">
18
+ <td class="px-4 py-3 text-sm font-mono text-indigo-600"><%= s[:pattern] %></td>
19
+ <td class="px-4 py-3 text-sm text-gray-900"><%= s[:handler_class] %></td>
20
+ <td class="px-4 py-3 text-sm text-gray-500"><%= s[:queue_name] %></td>
21
+ </tr>
22
+ <% end %>
23
+ <% if @subscribers.empty? %>
24
+ <tr><td colspan="3" class="px-4 py-8 text-center text-sm text-gray-400">No subscribers registered</td></tr>
25
+ <% end %>
26
+ </tbody>
27
+ </table>
28
+ </div>
29
+ </div>
30
+
31
+ <!-- Processed Events (Audit Trail) -->
32
+ <div>
33
+ <h2 class="text-lg font-semibold text-gray-900 mb-3">Processed Events</h2>
34
+ <div class="overflow-hidden rounded-lg bg-white shadow ring-1 ring-gray-200">
35
+ <table class="min-w-full divide-y divide-gray-200">
36
+ <thead class="bg-gray-50">
37
+ <tr>
38
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Event ID</th>
39
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Handler</th>
40
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Processed At</th>
41
+ </tr>
42
+ </thead>
43
+ <tbody class="divide-y divide-gray-100">
44
+ <% @events.each do |e| %>
45
+ <tr class="hover:bg-gray-50">
46
+ <td class="px-4 py-3 text-sm font-mono text-gray-900"><%= e["event_id"] %></td>
47
+ <td class="px-4 py-3 text-sm text-gray-700"><%= e["handler_class"] %></td>
48
+ <td class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(e["processed_at"]) %></td>
49
+ </tr>
50
+ <% end %>
51
+ <% if @events.empty? %>
52
+ <tr><td colspan="3" class="px-4 py-8 text-center text-sm text-gray-400">No events processed yet</td></tr>
53
+ <% end %>
54
+ </tbody>
55
+ </table>
56
+ </div>
57
+ </div>
@@ -0,0 +1,28 @@
1
+ <div class="mb-6">
2
+ <%= link_to "&larr; Back to Events".html_safe, pgbus.events_path, class: "text-sm text-indigo-600 hover:text-indigo-500" %>
3
+ </div>
4
+
5
+ <% if @event %>
6
+ <h1 class="text-2xl font-bold text-gray-900 mb-6">Event <%= @event["event_id"] %></h1>
7
+
8
+ <div class="rounded-lg bg-white shadow ring-1 ring-gray-200 p-6">
9
+ <dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
10
+ <div>
11
+ <dt class="text-sm font-medium text-gray-500">Event ID</dt>
12
+ <dd class="text-sm font-mono text-gray-900"><%= @event["event_id"] %></dd>
13
+ </div>
14
+ <div>
15
+ <dt class="text-sm font-medium text-gray-500">Handler</dt>
16
+ <dd class="text-sm text-gray-900"><%= @event["handler_class"] %></dd>
17
+ </div>
18
+ <div>
19
+ <dt class="text-sm font-medium text-gray-500">Processed At</dt>
20
+ <dd class="text-sm text-gray-900"><%= @event["processed_at"] %></dd>
21
+ </div>
22
+ </dl>
23
+ </div>
24
+ <% else %>
25
+ <div class="text-center py-12">
26
+ <p class="text-gray-400">Event not found</p>
27
+ </div>
28
+ <% end %>
@@ -0,0 +1,34 @@
1
+ <turbo-frame id="jobs-enqueued" data-auto-refresh src="<%= pgbus.jobs_path(request.query_parameters.merge(frame: 'enqueued')) %>">
2
+ <div>
3
+ <h2 class="text-lg font-semibold text-gray-900 mb-3">Enqueued Jobs</h2>
4
+ <div class="overflow-hidden rounded-lg bg-white shadow ring-1 ring-gray-200">
5
+ <table class="min-w-full divide-y divide-gray-200">
6
+ <thead class="bg-gray-50">
7
+ <tr>
8
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">ID</th>
9
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Queue</th>
10
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Enqueued</th>
11
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Reads</th>
12
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Payload</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody class="divide-y divide-gray-100">
16
+ <% @jobs.each do |j| %>
17
+ <tr class="hover:bg-gray-50">
18
+ <td class="px-4 py-3 text-sm font-mono text-gray-900"><%= j[:msg_id] %></td>
19
+ <td class="px-4 py-3 text-sm text-gray-700"><%= j[:queue_name] %></td>
20
+ <td class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(j[:enqueued_at]) %></td>
21
+ <td class="px-4 py-3 text-sm text-gray-500"><%= j[:read_ct] %></td>
22
+ <td class="px-4 py-3 text-sm text-gray-600 font-mono text-xs max-w-md truncate">
23
+ <%= pgbus_json_preview(j[:message]) %>
24
+ </td>
25
+ </tr>
26
+ <% end %>
27
+ <% if @jobs.empty? %>
28
+ <tr><td colspan="5" class="px-4 py-8 text-center text-sm text-gray-400">No enqueued jobs</td></tr>
29
+ <% end %>
30
+ </tbody>
31
+ </table>
32
+ </div>
33
+ </div>
34
+ </turbo-frame>
@@ -0,0 +1,45 @@
1
+ <turbo-frame id="jobs-failed" data-auto-refresh src="<%= pgbus.jobs_path(frame: 'failed') %>">
2
+ <div class="mb-8">
3
+ <h2 class="text-lg font-semibold text-gray-900 mb-3">Failed Jobs</h2>
4
+ <div class="overflow-hidden rounded-lg bg-white shadow ring-1 ring-gray-200">
5
+ <table class="min-w-full divide-y divide-gray-200">
6
+ <thead class="bg-gray-50">
7
+ <tr>
8
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">ID</th>
9
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Queue</th>
10
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Error</th>
11
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Retries</th>
12
+ <th class="px-4 py-3 text-left text-xs font-medium uppercase text-gray-500">Failed</th>
13
+ <th class="px-4 py-3 text-right text-xs font-medium uppercase text-gray-500">Actions</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody class="divide-y divide-gray-100">
17
+ <% @failed.each do |f| %>
18
+ <tr class="hover:bg-gray-50">
19
+ <td class="px-4 py-3 text-sm font-mono text-gray-900">
20
+ <%= link_to f["id"], pgbus.job_path(f["id"]), class: "text-indigo-600 hover:text-indigo-500", data: { turbo_frame: "_top" } %>
21
+ </td>
22
+ <td class="px-4 py-3 text-sm text-gray-700"><%= f["queue_name"] %></td>
23
+ <td class="px-4 py-3 text-sm text-red-600 max-w-sm truncate">
24
+ <span class="font-medium"><%= f["error_class"] %></span>: <%= truncate(f["error_message"].to_s, length: 80) %>
25
+ </td>
26
+ <td class="px-4 py-3 text-sm text-gray-500"><%= f["retry_count"] %></td>
27
+ <td class="px-4 py-3 text-sm text-gray-500"><%= pgbus_time_ago(f["failed_at"]) %></td>
28
+ <td class="px-4 py-3 text-sm text-right space-x-2">
29
+ <%= button_to "Retry", pgbus.retry_job_path(f["id"]), method: :post,
30
+ class: "text-xs text-indigo-600 hover:text-indigo-800 font-medium",
31
+ data: { turbo_frame: "_top" } %>
32
+ <%= button_to "Discard", pgbus.discard_job_path(f["id"]), method: :post,
33
+ class: "text-xs text-red-600 hover:text-red-800 font-medium",
34
+ data: { turbo_confirm: "Discard this job?", turbo_frame: "_top" } %>
35
+ </td>
36
+ </tr>
37
+ <% end %>
38
+ <% if @failed.empty? %>
39
+ <tr><td colspan="6" class="px-4 py-8 text-center text-sm text-gray-400">No failed jobs</td></tr>
40
+ <% end %>
41
+ </tbody>
42
+ </table>
43
+ </div>
44
+ </div>
45
+ </turbo-frame>
@@ -0,0 +1,16 @@
1
+ <div class="flex items-center justify-between mb-6">
2
+ <h1 class="text-2xl font-bold text-gray-900">Jobs</h1>
3
+ <% if @failed.any? %>
4
+ <div class="flex space-x-2">
5
+ <%= button_to "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",
7
+ data: { turbo_confirm: "Retry all failed jobs?" } %>
8
+ <%= button_to "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",
10
+ data: { turbo_confirm: "Discard all failed jobs?" } %>
11
+ </div>
12
+ <% end %>
13
+ </div>
14
+
15
+ <%= render "pgbus/jobs/failed_table" %>
16
+ <%= render "pgbus/jobs/enqueued_table" %>