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.
- checksums.yaml +7 -0
- data/.bun-version +1 -0
- data/.claude/commands/architect.md +100 -0
- data/.claude/commands/github-review-comments.md +237 -0
- data/.claude/commands/lfg.md +271 -0
- data/.claude/commands/review-pr.md +69 -0
- data/.claude/commands/security.md +122 -0
- data/.claude/commands/tdd.md +148 -0
- data/.claude/rules/agents.md +49 -0
- data/.claude/rules/coding-style.md +91 -0
- data/.claude/rules/git-workflow.md +56 -0
- data/.claude/rules/performance.md +73 -0
- data/.claude/rules/testing.md +67 -0
- data/CHANGELOG.md +5 -0
- data/CLAUDE.md +80 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +417 -0
- data/Rakefile +14 -0
- data/app/controllers/pgbus/api/stats_controller.rb +11 -0
- data/app/controllers/pgbus/application_controller.rb +35 -0
- data/app/controllers/pgbus/dashboard_controller.rb +27 -0
- data/app/controllers/pgbus/dead_letter_controller.rb +50 -0
- data/app/controllers/pgbus/events_controller.rb +23 -0
- data/app/controllers/pgbus/jobs_controller.rb +48 -0
- data/app/controllers/pgbus/processes_controller.rb +10 -0
- data/app/controllers/pgbus/queues_controller.rb +21 -0
- data/app/helpers/pgbus/application_helper.rb +69 -0
- data/app/views/layouts/pgbus/application.html.erb +76 -0
- data/app/views/pgbus/dashboard/_processes_table.html.erb +30 -0
- data/app/views/pgbus/dashboard/_queues_table.html.erb +39 -0
- data/app/views/pgbus/dashboard/_recent_failures.html.erb +33 -0
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +28 -0
- data/app/views/pgbus/dashboard/show.html.erb +10 -0
- data/app/views/pgbus/dead_letter/_messages_table.html.erb +40 -0
- data/app/views/pgbus/dead_letter/index.html.erb +15 -0
- data/app/views/pgbus/dead_letter/show.html.erb +52 -0
- data/app/views/pgbus/events/index.html.erb +57 -0
- data/app/views/pgbus/events/show.html.erb +28 -0
- data/app/views/pgbus/jobs/_enqueued_table.html.erb +34 -0
- data/app/views/pgbus/jobs/_failed_table.html.erb +45 -0
- data/app/views/pgbus/jobs/index.html.erb +16 -0
- data/app/views/pgbus/jobs/show.html.erb +57 -0
- data/app/views/pgbus/processes/_processes_table.html.erb +37 -0
- data/app/views/pgbus/processes/index.html.erb +3 -0
- data/app/views/pgbus/queues/_queues_list.html.erb +41 -0
- data/app/views/pgbus/queues/index.html.erb +3 -0
- data/app/views/pgbus/queues/show.html.erb +49 -0
- data/bun.lock +18 -0
- data/config/routes.rb +45 -0
- data/docs/README.md +28 -0
- data/docs/switch_from_good_job.md +279 -0
- data/docs/switch_from_sidekiq.md +226 -0
- data/docs/switch_from_solid_queue.md +247 -0
- data/exe/pgbus +7 -0
- data/lib/generators/pgbus/install_generator.rb +56 -0
- data/lib/generators/pgbus/templates/migration.rb.erb +114 -0
- data/lib/generators/pgbus/templates/pgbus.yml.erb +74 -0
- data/lib/generators/pgbus/templates/pgbus_binstub.erb +7 -0
- data/lib/pgbus/active_job/adapter.rb +109 -0
- data/lib/pgbus/active_job/executor.rb +107 -0
- data/lib/pgbus/batch.rb +153 -0
- data/lib/pgbus/cli.rb +84 -0
- data/lib/pgbus/client.rb +162 -0
- data/lib/pgbus/concurrency/blocked_execution.rb +74 -0
- data/lib/pgbus/concurrency/semaphore.rb +66 -0
- data/lib/pgbus/concurrency.rb +65 -0
- data/lib/pgbus/config_loader.rb +27 -0
- data/lib/pgbus/configuration.rb +99 -0
- data/lib/pgbus/engine.rb +31 -0
- data/lib/pgbus/event.rb +31 -0
- data/lib/pgbus/event_bus/handler.rb +76 -0
- data/lib/pgbus/event_bus/publisher.rb +42 -0
- data/lib/pgbus/event_bus/registry.rb +54 -0
- data/lib/pgbus/event_bus/subscriber.rb +30 -0
- data/lib/pgbus/process/consumer.rb +113 -0
- data/lib/pgbus/process/dispatcher.rb +154 -0
- data/lib/pgbus/process/heartbeat.rb +71 -0
- data/lib/pgbus/process/signal_handler.rb +49 -0
- data/lib/pgbus/process/supervisor.rb +198 -0
- data/lib/pgbus/process/worker.rb +153 -0
- data/lib/pgbus/serializer.rb +43 -0
- data/lib/pgbus/version.rb +5 -0
- data/lib/pgbus/web/authentication.rb +24 -0
- data/lib/pgbus/web/data_source.rb +406 -0
- data/lib/pgbus.rb +49 -0
- data/package.json +9 -0
- data/sig/pgbus.rbs +4 -0
- 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 "← 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 "← 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" %>
|