pgbus 0.17.1 → 0.17.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -0
- data/README.md +83 -13
- data/Rakefile +26 -0
- data/app/assets/javascripts/pgbus/stream_source_element.js +34 -2
- data/app/assets/javascripts/pgbus/stream_source_element.min.js +6 -0
- data/app/assets/javascripts/pgbus/stream_source_element.min.js.map +1 -0
- data/app/controllers/concerns/pgbus/job_listing.rb +37 -0
- data/app/controllers/concerns/pgbus/process_listing.rb +17 -0
- data/app/controllers/pgbus/application_controller.rb +5 -2
- data/app/controllers/pgbus/batches_controller.rb +4 -1
- data/app/controllers/pgbus/dashboard_controller.rb +20 -6
- data/app/controllers/pgbus/dead_letter_controller.rb +27 -3
- data/app/controllers/pgbus/events_controller.rb +52 -3
- data/app/controllers/pgbus/frontends_controller.rb +25 -11
- data/app/controllers/pgbus/insights_controller.rb +19 -1
- data/app/controllers/pgbus/jobs_controller.rb +45 -26
- data/app/controllers/pgbus/locks_controller.rb +7 -2
- data/app/controllers/pgbus/outbox_controller.rb +4 -1
- data/app/controllers/pgbus/processes_controller.rb +3 -1
- data/app/controllers/pgbus/queues_controller.rb +16 -4
- data/app/controllers/pgbus/recurring_tasks_controller.rb +5 -7
- data/app/frontend/pgbus/application.min.js +2 -0
- data/app/frontend/pgbus/application.min.js.map +1 -0
- data/app/frontend/pgbus/modules/charts.js +137 -77
- data/app/frontend/pgbus/modules/charts.min.js +2 -0
- data/app/frontend/pgbus/modules/charts.min.js.map +1 -0
- data/app/frontend/pgbus/style.css +1 -1
- data/app/frontend/pgbus/tailwind.css +18 -0
- data/app/frontend/pgbus/vendor/turbo.min.js +54 -0
- data/app/frontend/pgbus/vendor/turbo.min.js.map +1 -0
- data/app/helpers/pgbus/application_helper.rb +155 -113
- data/app/helpers/pgbus/button_helper.rb +88 -0
- data/app/helpers/pgbus/dashboard_helper.rb +157 -0
- data/app/helpers/pgbus/events_helper.rb +40 -0
- data/app/helpers/pgbus/insights_helper.rb +39 -0
- data/app/helpers/pgbus/processes_helper.rb +79 -0
- data/app/views/layouts/pgbus/application.html.erb +0 -1
- data/app/views/pgbus/batches/_batches_table.html.erb +16 -13
- data/app/views/pgbus/batches/show.html.erb +3 -4
- data/app/views/pgbus/dashboard/_processes_table.html.erb +11 -11
- data/app/views/pgbus/dashboard/_queue_health.html.erb +42 -51
- data/app/views/pgbus/dashboard/_queues_table.html.erb +18 -16
- data/app/views/pgbus/dashboard/_recent_failures.html.erb +16 -12
- data/app/views/pgbus/dashboard/_stats_cards.html.erb +70 -36
- data/app/views/pgbus/dashboard/show.html.erb +1 -1
- data/app/views/pgbus/dead_letter/_error.html.erb +13 -0
- data/app/views/pgbus/dead_letter/_messages_table.html.erb +37 -96
- data/app/views/pgbus/dead_letter/_row.html.erb +75 -0
- data/app/views/pgbus/dead_letter/index.html.erb +6 -9
- data/app/views/pgbus/dead_letter/show.html.erb +37 -21
- data/app/views/pgbus/events/_list.html.erb +55 -0
- data/app/views/pgbus/events/_processed_table.html.erb +45 -0
- data/app/views/pgbus/events/_replay.html.erb +10 -0
- data/app/views/pgbus/events/_row.html.erb +162 -0
- data/app/views/pgbus/events/_tabs.html.erb +13 -0
- data/app/views/pgbus/events/index.html.erb +27 -48
- data/app/views/pgbus/events/show.html.erb +25 -7
- data/app/views/pgbus/insights/_chart_card.html.erb +21 -0
- data/app/views/pgbus/insights/_latency_table.html.erb +20 -0
- data/app/views/pgbus/insights/_status_table.html.erb +19 -0
- data/app/views/pgbus/insights/_throughput_table.html.erb +18 -0
- data/app/views/pgbus/insights/show.html.erb +83 -100
- data/app/views/pgbus/jobs/_list.html.erb +64 -0
- data/app/views/pgbus/jobs/_row.html.erb +107 -0
- data/app/views/pgbus/jobs/_tabs.html.erb +14 -0
- data/app/views/pgbus/jobs/index.html.erb +10 -10
- data/app/views/pgbus/jobs/show.html.erb +18 -20
- data/app/views/pgbus/locks/_concurrency.html.erb +25 -23
- data/app/views/pgbus/locks/_uniqueness.html.erb +20 -22
- data/app/views/pgbus/outbox/index.html.erb +30 -27
- data/app/views/pgbus/processes/_processes_table.html.erb +6 -25
- data/app/views/pgbus/processes/_row.html.erb +48 -0
- data/app/views/pgbus/processes/_status.html.erb +4 -0
- data/app/views/pgbus/queues/_queues_list.html.erb +28 -32
- data/app/views/pgbus/queues/show.html.erb +69 -118
- data/app/views/pgbus/recurring_tasks/_tasks_table.html.erb +75 -76
- data/app/views/pgbus/recurring_tasks/index.html.erb +1 -1
- data/app/views/pgbus/recurring_tasks/show.html.erb +45 -51
- data/app/views/pgbus/shared/_empty_row.html.erb +2 -0
- data/app/views/pgbus/shared/_pager.html.erb +34 -0
- data/config/i18n-tasks.yml +11 -2
- data/config/locales/da.yml +405 -113
- data/config/locales/de.yml +405 -113
- data/config/locales/en.yml +405 -113
- data/config/locales/es.yml +404 -112
- data/config/locales/fi.yml +404 -112
- data/config/locales/fr.yml +406 -114
- data/config/locales/it.yml +405 -113
- data/config/locales/ja.yml +405 -113
- data/config/locales/nb.yml +405 -113
- data/config/locales/nl.yml +405 -113
- data/config/locales/pt.yml +406 -114
- data/config/locales/sv.yml +404 -112
- data/config/routes.rb +1 -1
- data/lib/generators/pgbus/add_presence_generator.rb +2 -1
- data/lib/generators/pgbus/add_presence_seats_generator.rb +45 -0
- data/lib/generators/pgbus/templates/add_presence.rb.erb +32 -7
- data/lib/generators/pgbus/templates/add_presence_seats.rb.erb +38 -0
- data/lib/generators/pgbus/templates/initializer.rb.erb +4 -0
- data/lib/pgbus/active_job/executor.rb +16 -6
- data/lib/pgbus/cli/dlq.rb +32 -5
- data/lib/pgbus/client.rb +4 -2
- data/lib/pgbus/configuration/presence_settings.rb +103 -0
- data/lib/pgbus/configuration.rb +51 -2
- data/lib/pgbus/dead_letter_header.rb +129 -0
- data/lib/pgbus/doctor.rb +8 -4
- data/lib/pgbus/engine.rb +13 -7
- data/lib/pgbus/event_bus/handler.rb +18 -11
- data/lib/pgbus/event_bus/registry.rb +6 -0
- data/lib/pgbus/failed_event_recorder.rb +34 -0
- data/lib/pgbus/generators/migration_detector.rb +9 -1
- data/lib/pgbus/instrumentation.rb +2 -1
- data/lib/pgbus/mcp/base_tool.rb +25 -0
- data/lib/pgbus/mcp/tools/concurrency_tool.rb +23 -4
- data/lib/pgbus/mcp/tools/dlq_detail_tool.rb +4 -2
- data/lib/pgbus/mcp/tools/dlq_tool.rb +10 -4
- data/lib/pgbus/mcp/tools/locks_tool.rb +23 -5
- data/lib/pgbus/mcp/tools/recurring_tool.rb +33 -4
- data/lib/pgbus/outbox/poller.rb +1 -1
- data/lib/pgbus/process/consumer.rb +53 -3
- data/lib/pgbus/process/dispatcher.rb +1 -1
- data/lib/pgbus/process/supervisor.rb +32 -11
- data/lib/pgbus/process/worker.rb +45 -8
- data/lib/pgbus/recurring/scheduler.rb +2 -1
- data/lib/pgbus/streams/presence.rb +317 -81
- data/lib/pgbus/version.rb +1 -1
- data/lib/pgbus/web/data_source/dashboard_signals.rb +170 -0
- data/lib/pgbus/web/data_source/dead_letter.rb +142 -0
- data/lib/pgbus/web/data_source/event_list.rb +161 -0
- data/lib/pgbus/web/data_source/job_list.rb +323 -0
- data/lib/pgbus/web/data_source/list_counts.rb +80 -0
- data/lib/pgbus/web/data_source/process_list.rb +69 -0
- data/lib/pgbus/web/data_source/queue_summary.rb +122 -0
- data/lib/pgbus/web/data_source.rb +132 -254
- data/lib/pgbus/web/dead_letter_reason.rb +81 -0
- data/lib/pgbus/web/event_state.rb +71 -0
- data/lib/pgbus/web/health_app.rb +7 -3
- data/lib/pgbus/web/health_verdict.rb +78 -0
- data/lib/pgbus/web/insights_charts.rb +84 -0
- data/lib/pgbus/web/job_state.rb +111 -0
- data/lib/pgbus/web/process_activity.rb +97 -0
- data/lib/pgbus/web/process_state.rb +187 -0
- data/lib/pgbus/web/queue_summary.rb +104 -0
- data/lib/pgbus/web/stream_app.rb +18 -6
- data/lib/pgbus/web/streamer/connection.rb +7 -1
- data/lib/pgbus/web/streamer/falcon_connection.rb +7 -1
- data/lib/pgbus/web/streamer/heartbeat.rb +9 -3
- data/lib/pgbus/web/streamer/instance.rb +2 -1
- data/lib/pgbus/web/streamer/presence_tracker.rb +155 -0
- data/lib/pgbus/web/streamer/stream_event_dispatcher.rb +8 -37
- data/lib/pgbus/web/time_format.rb +128 -0
- metadata +55 -4
- data/app/views/pgbus/events/_pending_table.html.erb +0 -169
- data/app/views/pgbus/jobs/_enqueued_table.html.erb +0 -118
- data/app/views/pgbus/jobs/_failed_table.html.erb +0 -76
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
module Pgbus
|
|
4
4
|
class DashboardController < ApplicationController
|
|
5
|
+
include ProcessListing
|
|
6
|
+
|
|
5
7
|
def show
|
|
6
8
|
case params[:frame]
|
|
7
9
|
when "stats"
|
|
8
|
-
|
|
10
|
+
load_stats
|
|
9
11
|
render_frame("pgbus/dashboard/stats_cards")
|
|
10
12
|
when "queues"
|
|
11
|
-
|
|
13
|
+
load_queues
|
|
12
14
|
render_frame("pgbus/dashboard/queues_table")
|
|
13
15
|
when "processes"
|
|
14
|
-
|
|
16
|
+
load_processes
|
|
15
17
|
render_frame("pgbus/dashboard/processes_table")
|
|
16
18
|
when "failures"
|
|
17
19
|
@recent_failures = data_source.failed_events(page: 1, per_page: 5)
|
|
@@ -20,12 +22,24 @@ module Pgbus
|
|
|
20
22
|
@health = data_source.queue_health_stats
|
|
21
23
|
render_frame("pgbus/dashboard/queue_health")
|
|
22
24
|
else
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
load_stats
|
|
26
|
+
load_queues
|
|
27
|
+
load_processes
|
|
26
28
|
@recent_failures = data_source.failed_events(page: 1, per_page: 5)
|
|
27
29
|
@health = data_source.queue_health_stats
|
|
28
30
|
end
|
|
29
31
|
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def load_stats
|
|
36
|
+
@stats = data_source.summary_stats
|
|
37
|
+
@signals = data_source.dashboard_signals
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def load_queues
|
|
41
|
+
@queues = data_source.queues_with_metrics
|
|
42
|
+
@queue_lines = data_source.queue_status_lines
|
|
43
|
+
end
|
|
30
44
|
end
|
|
31
45
|
end
|
|
@@ -2,17 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module Pgbus
|
|
4
4
|
class DeadLetterController < ApplicationController
|
|
5
|
+
ERROR_CLASS_MAX = 200
|
|
6
|
+
|
|
5
7
|
def index
|
|
6
8
|
@page = page_param
|
|
7
9
|
@per_page = per_page
|
|
8
|
-
@
|
|
9
|
-
@
|
|
10
|
+
@dlq = dlq_param
|
|
11
|
+
@error_class = error_class_param
|
|
12
|
+
filters = { dlq: @dlq, error_class: @error_class }
|
|
13
|
+
@messages = data_source.dlq_messages(page: @page, per_page: @per_page, **filters)
|
|
14
|
+
@total_count = data_source.dlq_total_count(**filters)
|
|
10
15
|
@total_pages = (@total_count.to_f / @per_page).ceil
|
|
16
|
+
@dlq_counts = data_source.dlq_counts_by_queue
|
|
17
|
+
@dlq_rows = @messages.map { |m| [m, Pgbus::Web::DeadLetterReason.present(m)] }
|
|
18
|
+
# One place builds every list URL (frame source, pager, chips), so the
|
|
19
|
+
# filters survive refresh and paging.
|
|
20
|
+
@list_path = ->(extra = {}) { pgbus.dead_letter_index_path(filters.merge(extra).compact) }
|
|
11
21
|
render_frame("pgbus/dead_letter/messages_table") if params[:frame] == "list"
|
|
12
22
|
end
|
|
13
23
|
|
|
14
24
|
def show
|
|
15
|
-
@message = data_source.dlq_message_detail(params[:id].to_i)
|
|
25
|
+
@message = data_source.dlq_message_detail(params[:id].to_i, queue_name: dlq_param(:queue_name))
|
|
26
|
+
@reason = Pgbus::Web::DeadLetterReason.present(@message) if @message
|
|
16
27
|
end
|
|
17
28
|
|
|
18
29
|
def retry
|
|
@@ -63,5 +74,18 @@ module Pgbus
|
|
|
63
74
|
end
|
|
64
75
|
redirect_to dead_letter_index_path, notice: t("pgbus.dead_letter.index.discarded_selected", count: count)
|
|
65
76
|
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# A full DLQ name or nil; the data source also checks it is a known DLQ.
|
|
81
|
+
def dlq_param(key = :dlq)
|
|
82
|
+
name = params[key].to_s
|
|
83
|
+
name if name.end_with?(Pgbus::DEAD_LETTER_SUFFIX) && name.match?(/\A\w+\z/)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def error_class_param
|
|
87
|
+
value = params[:error_class].to_s
|
|
88
|
+
value if value.present? && value.length <= ERROR_CLASS_MAX
|
|
89
|
+
end
|
|
66
90
|
end
|
|
67
91
|
end
|
|
@@ -2,14 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
module Pgbus
|
|
4
4
|
class EventsController < ApplicationController
|
|
5
|
+
# Tabs of the pending list; "all" is no filter.
|
|
6
|
+
STATES = ["all", *Web::EventState::STATES].freeze
|
|
7
|
+
|
|
8
|
+
# One pending-events list (the job list scoped to the handler queues) and
|
|
9
|
+
# the processed-events audit, each in its own auto-refreshing frame with
|
|
10
|
+
# its own pager (page / processed_page), issue #494.
|
|
5
11
|
def index
|
|
6
|
-
@
|
|
7
|
-
@
|
|
8
|
-
@
|
|
12
|
+
@state = STATES.include?(params[:state]) ? params[:state] : "all"
|
|
13
|
+
@queue = params[:queue].presence
|
|
14
|
+
@page = page_param
|
|
15
|
+
@processed_page = page_param(:processed_page)
|
|
16
|
+
@per_page = per_page
|
|
17
|
+
if params[:frame] == "processed"
|
|
18
|
+
load_processed_events
|
|
19
|
+
return render_frame("pgbus/events/processed_table")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
load_pending_events
|
|
23
|
+
return render_frame("pgbus/events/list") if params[:frame] == "list"
|
|
24
|
+
|
|
25
|
+
load_processed_events
|
|
9
26
|
end
|
|
10
27
|
|
|
11
28
|
def show
|
|
12
29
|
@event = data_source.processed_event(params[:id])
|
|
30
|
+
return unless @event
|
|
31
|
+
|
|
32
|
+
@claim = Web::EventState.processed(@event, now: Time.now, claim_window: EventBus::Handler.claim_ownership_window)
|
|
33
|
+
@replay_state = data_source.event_replay_states([@event])[@event["id"]]
|
|
13
34
|
end
|
|
14
35
|
|
|
15
36
|
def replay
|
|
@@ -53,6 +74,10 @@ module Pgbus
|
|
|
53
74
|
return reject_unknown_queue(:payload_update_failed) unless registered_queue?(queue_name)
|
|
54
75
|
|
|
55
76
|
new_payload = params[:payload].to_s
|
|
77
|
+
# The editor shows the payload through PayloadFilter: saving a marker
|
|
78
|
+
# back would overwrite the redacted secret for good.
|
|
79
|
+
return redirect_to(events_path, alert: t("pgbus.events.flash.payload_redacted")) if new_payload.include?(Web::PayloadFilter::FILTERED)
|
|
80
|
+
|
|
56
81
|
if data_source.edit_event_payload(queue_name, params[:id], new_payload)
|
|
57
82
|
redirect_to events_path, notice: t("pgbus.events.flash.payload_updated")
|
|
58
83
|
else
|
|
@@ -104,6 +129,30 @@ module Pgbus
|
|
|
104
129
|
|
|
105
130
|
private
|
|
106
131
|
|
|
132
|
+
def load_pending_events
|
|
133
|
+
@rows = data_source.event_rows(state: (@state unless @state == "all"), queue_name: @queue,
|
|
134
|
+
page: @page, per_page: @per_page)
|
|
135
|
+
@counts = data_source.event_state_counts(queue_name: @queue)
|
|
136
|
+
@ahead = data_source.events_ahead(@rows)
|
|
137
|
+
@state_context = data_source.event_list_context
|
|
138
|
+
@subscribers = data_source.registered_subscribers
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def load_processed_events
|
|
142
|
+
@events = data_source.processed_events(page: @processed_page, per_page: @per_page)
|
|
143
|
+
@processed_count = data_source.list_count(:processed_events)
|
|
144
|
+
@replay_states = data_source.event_replay_states(@events)
|
|
145
|
+
@claim_window = EventBus::Handler.claim_ownership_window
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Every list link keeps the other list's page, so paging one frame never
|
|
149
|
+
# resets the other.
|
|
150
|
+
def events_list_path(extra = {})
|
|
151
|
+
events_path({ state: (@state unless @state == "all"), queue: @queue, page: (@page if @page > 1),
|
|
152
|
+
processed_page: (@processed_page if @processed_page > 1) }.merge(extra).compact)
|
|
153
|
+
end
|
|
154
|
+
helper_method :events_list_path
|
|
155
|
+
|
|
107
156
|
def registered_queues
|
|
108
157
|
@registered_queues ||= data_source.handler_queue_physical_names
|
|
109
158
|
end
|
|
@@ -10,20 +10,21 @@ module Pgbus
|
|
|
10
10
|
STATIC_ASSETS = {
|
|
11
11
|
css: {
|
|
12
12
|
style: FRONTEND_ROOT.join("style.css")
|
|
13
|
-
},
|
|
14
|
-
js: {
|
|
15
|
-
apexcharts: FRONTEND_ROOT.join("vendor", "apexcharts.js")
|
|
16
13
|
}
|
|
17
14
|
}.freeze
|
|
18
15
|
|
|
16
|
+
# Modules are served minified (rake frontend:js), each with its .min.js.map
|
|
17
|
+
# beside it. ApexCharts ships minified already; charts.js imports it on
|
|
18
|
+
# demand, so only Insights downloads it.
|
|
19
19
|
MODULE_OVERRIDES = {
|
|
20
|
-
application: FRONTEND_ROOT.join("application.js"),
|
|
21
|
-
turbo: FRONTEND_ROOT.join("vendor", "turbo.js")
|
|
20
|
+
application: FRONTEND_ROOT.join("application.min.js"),
|
|
21
|
+
turbo: FRONTEND_ROOT.join("vendor", "turbo.min.js"),
|
|
22
|
+
apexcharts: FRONTEND_ROOT.join("vendor", "apexcharts.js")
|
|
22
23
|
}.freeze
|
|
23
24
|
|
|
24
25
|
def self.js_modules
|
|
25
|
-
@js_modules ||= FRONTEND_ROOT.join("modules").
|
|
26
|
-
.to_h { |f| [
|
|
26
|
+
@js_modules ||= FRONTEND_ROOT.join("modules").glob("*.min.js").sort
|
|
27
|
+
.to_h { |f| [f.basename(".min.js").to_s.to_sym, f] }
|
|
27
28
|
.merge(MODULE_OVERRIDES)
|
|
28
29
|
end
|
|
29
30
|
|
|
@@ -43,18 +44,31 @@ module Pgbus
|
|
|
43
44
|
render file: file
|
|
44
45
|
end
|
|
45
46
|
|
|
47
|
+
# GET modules/:version/<id>.js serves the module; <id>.map its sourcemap,
|
|
48
|
+
# the URL each minified module's sourceMappingURL comment resolves to.
|
|
46
49
|
def module
|
|
47
50
|
validate_version!
|
|
48
|
-
raise ActionController::RoutingError, "Not Found" if params[:format] != "js"
|
|
49
|
-
|
|
50
51
|
file = self.class.js_modules[params[:id]&.to_sym]
|
|
51
|
-
raise ActionController::RoutingError, "Not Found" unless file&.exist?
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
case params[:format]
|
|
54
|
+
when "js" then serve(file)
|
|
55
|
+
when "map" then serve(sourcemap_for(file), content_type: "application/json")
|
|
56
|
+
else raise ActionController::RoutingError, "Not Found"
|
|
57
|
+
end
|
|
54
58
|
end
|
|
55
59
|
|
|
56
60
|
private
|
|
57
61
|
|
|
62
|
+
def serve(file, **)
|
|
63
|
+
raise ActionController::RoutingError, "Not Found" unless file&.exist?
|
|
64
|
+
|
|
65
|
+
render(file: file, **)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def sourcemap_for(file)
|
|
69
|
+
Pathname.new("#{file}.map") if file.to_s.end_with?(".min.js")
|
|
70
|
+
end
|
|
71
|
+
|
|
58
72
|
def validate_version!
|
|
59
73
|
return if params[:version] == CURRENT_VERSION
|
|
60
74
|
|
|
@@ -7,7 +7,8 @@ module Pgbus
|
|
|
7
7
|
@summary = data_source.job_stats_summary(minutes: @minutes)
|
|
8
8
|
@slowest = data_source.slowest_job_classes(minutes: @minutes)
|
|
9
9
|
@latency_by_queue = data_source.latency_by_queue(minutes: @minutes)
|
|
10
|
-
@latency_available =
|
|
10
|
+
@latency_available = data_source.latency_available?
|
|
11
|
+
load_charts
|
|
11
12
|
|
|
12
13
|
@live_stream_metrics = data_source.live_stream_metrics
|
|
13
14
|
|
|
@@ -17,5 +18,22 @@ module Pgbus
|
|
|
17
18
|
@stream_summary = data_source.stream_stats_summary(minutes: @minutes)
|
|
18
19
|
@top_streams = data_source.top_streams(minutes: @minutes)
|
|
19
20
|
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# The chart series, read once: the view renders their captions and data
|
|
25
|
+
# tables, and embeds the same series for charts.js (issue #492), so the
|
|
26
|
+
# page no longer fetches /pgbus/api/insights to draw them.
|
|
27
|
+
def load_charts
|
|
28
|
+
throughput = data_source.job_throughput(minutes: @minutes)
|
|
29
|
+
status_counts = data_source.job_status_counts(minutes: @minutes)
|
|
30
|
+
latency_trend = @latency_available ? data_source.latency_trend(minutes: @minutes) : []
|
|
31
|
+
@charts = {
|
|
32
|
+
throughput: Web::InsightsCharts.throughput(throughput, minutes: @minutes),
|
|
33
|
+
status: Web::InsightsCharts.status(status_counts),
|
|
34
|
+
latency: Web::InsightsCharts.latency(latency_trend, minutes: @minutes)
|
|
35
|
+
}
|
|
36
|
+
@chart_payload = { throughput: throughput, status_counts: status_counts, latency_trend: latency_trend }
|
|
37
|
+
end
|
|
20
38
|
end
|
|
21
39
|
end
|
|
@@ -2,17 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module Pgbus
|
|
4
4
|
class JobsController < ApplicationController
|
|
5
|
+
include JobListing
|
|
6
|
+
|
|
5
7
|
def index
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
render_frame("pgbus/jobs/enqueued_table")
|
|
12
|
-
else
|
|
13
|
-
@jobs = params[:status] == "failed" ? [] : data_source.jobs(queue_name: params[:queue], page: page_param, per_page: per_page)
|
|
14
|
-
@failed = data_source.failed_events(page: page_param, per_page: per_page)
|
|
15
|
-
end
|
|
8
|
+
@queue = params[:queue].presence
|
|
9
|
+
load_job_list(queue_name: @queue, list_path: ->(extra) { jobs_path({ queue: @queue }.merge(extra)) },
|
|
10
|
+
exclude: (data_source.handler_queue_physical_names unless @queue))
|
|
11
|
+
@failed_total = @queue ? data_source.failed_events_count : @counts["retrying"]
|
|
12
|
+
render_frame("pgbus/jobs/list") if params[:frame] == "list"
|
|
16
13
|
end
|
|
17
14
|
|
|
18
15
|
def show
|
|
@@ -21,17 +18,17 @@ module Pgbus
|
|
|
21
18
|
|
|
22
19
|
def retry
|
|
23
20
|
if data_source.retry_failed_event(params[:id])
|
|
24
|
-
|
|
21
|
+
redirect_after_failed_event_action notice: "Job re-enqueued."
|
|
25
22
|
else
|
|
26
|
-
|
|
23
|
+
redirect_after_failed_event_action alert: "Could not retry job."
|
|
27
24
|
end
|
|
28
25
|
end
|
|
29
26
|
|
|
30
27
|
def discard
|
|
31
28
|
if data_source.discard_failed_event(params[:id])
|
|
32
|
-
|
|
29
|
+
redirect_after_failed_event_action notice: "Job discarded."
|
|
33
30
|
else
|
|
34
|
-
|
|
31
|
+
redirect_after_failed_event_action alert: "Could not discard job."
|
|
35
32
|
end
|
|
36
33
|
end
|
|
37
34
|
|
|
@@ -47,7 +44,7 @@ module Pgbus
|
|
|
47
44
|
|
|
48
45
|
def discard_all_enqueued
|
|
49
46
|
count = data_source.discard_all_enqueued
|
|
50
|
-
|
|
47
|
+
redirect_back fallback_location: jobs_path, notice: t("pgbus.jobs.index.discard_all_enqueued_notice", count: count)
|
|
51
48
|
end
|
|
52
49
|
|
|
53
50
|
def discard_selected_failed
|
|
@@ -64,8 +61,40 @@ module Pgbus
|
|
|
64
61
|
redirect_to jobs_path, notice: t("pgbus.jobs.index.discarded_selected", count: count)
|
|
65
62
|
end
|
|
66
63
|
|
|
64
|
+
# The unified list posts every selected row here: queue messages as
|
|
65
|
+
# messages[], rows backed by a failed event (retrying, orphaned) as ids[]
|
|
66
|
+
# so discarding them also clears the failed-event row.
|
|
67
67
|
def discard_selected_enqueued
|
|
68
|
-
selections =
|
|
68
|
+
selections = selected_messages
|
|
69
|
+
ids = Array(params[:ids]).map(&:to_i).reject(&:zero?)
|
|
70
|
+
if selections.empty? && ids.empty?
|
|
71
|
+
redirect_back fallback_location: jobs_path, alert: t("pgbus.jobs.index.none_selected")
|
|
72
|
+
return
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
count = ids.count { |id| data_source.discard_failed_event(id) }
|
|
76
|
+
count += selections.count { |sel| data_source.discard_job(sel[:queue_name], sel[:msg_id]) }
|
|
77
|
+
redirect_back fallback_location: jobs_path, notice: t("pgbus.jobs.index.discarded_selected", count: count)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Back to the page the action came from (a queue page, the Jobs list),
|
|
83
|
+
# except the job's own detail page: its failed event is gone now.
|
|
84
|
+
def redirect_after_failed_event_action(**flash)
|
|
85
|
+
return redirect_to(jobs_path, **flash) if referer_path == job_path(params[:id])
|
|
86
|
+
|
|
87
|
+
redirect_back(fallback_location: jobs_path, **flash)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def referer_path
|
|
91
|
+
URI.parse(request.referer.to_s).path
|
|
92
|
+
rescue URI::InvalidURIError
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def selected_messages
|
|
97
|
+
Array(params[:messages]).filter_map do |s|
|
|
69
98
|
next unless s.respond_to?(:[])
|
|
70
99
|
|
|
71
100
|
queue_name = s[:queue_name]
|
|
@@ -74,16 +103,6 @@ module Pgbus
|
|
|
74
103
|
|
|
75
104
|
{ queue_name: queue_name, msg_id: msg_id }
|
|
76
105
|
end
|
|
77
|
-
if selections.empty?
|
|
78
|
-
redirect_to jobs_path, alert: t("pgbus.jobs.index.none_selected")
|
|
79
|
-
return
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
count = 0
|
|
83
|
-
selections.each do |sel|
|
|
84
|
-
count += 1 if data_source.discard_job(sel[:queue_name], sel[:msg_id])
|
|
85
|
-
end
|
|
86
|
-
redirect_to jobs_path, notice: t("pgbus.jobs.index.discarded_selected", count: count)
|
|
87
106
|
end
|
|
88
107
|
end
|
|
89
108
|
end
|
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
module Pgbus
|
|
4
4
|
class LocksController < ApplicationController
|
|
5
5
|
def index
|
|
6
|
-
@
|
|
6
|
+
@per_page = per_page
|
|
7
|
+
@page = page_param
|
|
8
|
+
@keys_page = page_param(:keys_page)
|
|
9
|
+
@concurrency = data_source.concurrency_stats(page: @keys_page, per_page: @per_page)
|
|
10
|
+
@keys_count = data_source.list_count(:concurrency_keys)
|
|
7
11
|
return render_frame("pgbus/locks/concurrency") if params[:frame] == "concurrency"
|
|
8
12
|
|
|
9
|
-
@locks = data_source.job_locks
|
|
13
|
+
@locks = data_source.job_locks(page: @page, per_page: @per_page)
|
|
14
|
+
@locks_count = data_source.list_count(:job_locks)
|
|
10
15
|
end
|
|
11
16
|
|
|
12
17
|
def discard
|
|
@@ -4,7 +4,10 @@ module Pgbus
|
|
|
4
4
|
class OutboxController < ApplicationController
|
|
5
5
|
def index
|
|
6
6
|
@stats = data_source.outbox_stats
|
|
7
|
-
@
|
|
7
|
+
@page = page_param
|
|
8
|
+
@per_page = per_page
|
|
9
|
+
@entries = data_source.outbox_entries(page: @page, per_page: @per_page)
|
|
10
|
+
@entries_count = data_source.list_count(:outbox)
|
|
8
11
|
end
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -6,13 +6,25 @@ module Pgbus
|
|
|
6
6
|
@queues = data_source.queues_with_metrics
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
+
include JobListing
|
|
10
|
+
|
|
9
11
|
def show
|
|
10
|
-
|
|
12
|
+
name = params[:name]
|
|
13
|
+
# The unified list leaves DLQ tables out (they are the Dead Letter page's).
|
|
14
|
+
@dlq = name.end_with?(Pgbus::DEAD_LETTER_SUFFIX)
|
|
15
|
+
load_job_list(queue_name: name, list_path: ->(extra) { queue_path({ name: name }.merge(extra)) }) unless @dlq
|
|
16
|
+
# The auto-refresh poll needs only the list: skip the queue's metrics scan.
|
|
17
|
+
render_frame("pgbus/jobs/list") and return if params[:frame] == "list" && !@dlq
|
|
18
|
+
|
|
19
|
+
@queue = data_source.queue_detail(name)
|
|
11
20
|
redirect_to queues_path, alert: "Queue not found." and return unless @queue
|
|
12
21
|
|
|
13
|
-
@paused = data_source.queue_paused?(
|
|
14
|
-
@
|
|
15
|
-
@
|
|
22
|
+
@paused = data_source.queue_paused?(name)
|
|
23
|
+
@pause_state = data_source.queue_pause_state(name)
|
|
24
|
+
@drainers = data_source.queue_drainers(name)
|
|
25
|
+
@summary = Web::QueueSummary.present(@queue, @pause_state, @drainers,
|
|
26
|
+
max_retries: Pgbus.configuration.max_retries)
|
|
27
|
+
@health = data_source.queue_health_detail(name)
|
|
16
28
|
end
|
|
17
29
|
|
|
18
30
|
def purge
|
|
@@ -3,13 +3,11 @@
|
|
|
3
3
|
module Pgbus
|
|
4
4
|
class RecurringTasksController < ApplicationController
|
|
5
5
|
def index
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@recurring_tasks = data_source.recurring_tasks
|
|
12
|
-
end
|
|
6
|
+
@page = page_param
|
|
7
|
+
@per_page = per_page
|
|
8
|
+
@recurring_tasks = data_source.recurring_tasks(page: @page, per_page: @per_page)
|
|
9
|
+
@recurring_tasks_count = data_source.list_count(:recurring_tasks)
|
|
10
|
+
render_frame("pgbus/recurring_tasks/tasks_table") if params[:frame] == "recurring_tasks"
|
|
13
11
|
end
|
|
14
12
|
|
|
15
13
|
def show
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import*as m from"turbo";m.config.forms.confirm=(e,o)=>{let a=document.getElementById("pgbus-confirm-dialog"),n=document.getElementById("pgbus-confirm-message"),d=document.getElementById("pgbus-confirm-title"),t=document.getElementById("pgbus-confirm-btn"),c=document.getElementById("pgbus-confirm-icon"),r=o.getAttribute("data-turbo-method")==="delete",s=document.getElementById("pgbus-i18n");if(d.textContent=r?s?.dataset.deleteTitle||"Delete":s?.dataset.confirmTitle||"Are you sure?",n.textContent=e,t.className="rounded-md px-4 py-2 text-sm font-medium text-white focus:outline-none focus:ring-2",r)t.classList.add("bg-red-600","hover:bg-red-500","focus:ring-red-500"),t.textContent=s?.dataset.deleteLabel||"Delete",c.className="flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-red-100 dark:bg-red-900/30";else t.classList.add("bg-yellow-500","hover:bg-yellow-400","focus:ring-yellow-500"),t.textContent=s?.dataset.confirmLabel||"Confirm",c.className="flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-yellow-100 dark:bg-yellow-900/30";return a.showModal(),new Promise((g)=>{a.addEventListener("close",()=>{g(a.returnValue==="confirm")},{once:!0})})};function f(e,o="success"){let a=document.getElementById("pgbus-toast-container");if(!a)return;let n=document.createElement("div"),d={success:"bg-green-50 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-200 dark:border-green-800",error:"bg-red-50 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-800",info:"bg-blue-50 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 border-blue-200 dark:border-blue-800"};n.className=`rounded-md border p-3 text-sm shadow-lg transition-all duration-300 ${d[o]||d.info}`,n.textContent=e,a.appendChild(n),setTimeout(()=>{n.style.opacity="0",n.style.transform="translateX(100%)",setTimeout(()=>n.remove(),300)},5000)}function b(){document.querySelectorAll("template[data-pgbus-toast]").forEach((e)=>{f(e.content.textContent.trim(),e.dataset.pgbusToast),e.remove()})}b();document.addEventListener("turbo:load",b);function u(){document.querySelectorAll("[data-bulk-select-all]").forEach((e)=>{if(e.dataset.bulkInitialized==="true")return;e.dataset.bulkInitialized="true";let o=e.closest("[data-bulk-scope]")||document,a=()=>o.querySelectorAll("input[data-bulk-item]"),n=o.closest("[data-bulk-page]")||o.closest("turbo-frame")||o.parentElement?.closest("div")||document,d=n.querySelector("[data-bulk-count]"),t=n.querySelector("[data-bulk-actions]");function c(){let r=o.querySelectorAll("input[data-bulk-item]:checked");if(d)d.textContent=r.length;if(t)t.classList.toggle("hidden",r.length===0);let s=a();e.checked=s.length>0&&r.length===s.length,e.indeterminate=r.length>0&&r.length<s.length}let l=!1;e.addEventListener("change",()=>{l=!0,a().forEach((r)=>{r.checked=e.checked,r.dispatchEvent(new Event("change",{bubbles:!1}))}),l=!1,c()}),o.addEventListener("change",(r)=>{if(r.target.matches("input[data-bulk-item]"))c()})})}u();document.addEventListener("turbo:load",u);document.addEventListener("turbo:frame-load",u);var i=parseInt(document.body?.dataset.pgbusRefreshInterval||"0",10);if(i>0){let o=function(){if(document.querySelector("input[data-bulk-item]:checked, input[data-bulk-select-all]:checked"))return!0;if(document.querySelector("turbo-frame[data-auto-refresh] details[open]"))return!0;return!1},a=function(){if(document.hidden)return;if(o())return;document.querySelectorAll("turbo-frame[data-auto-refresh]").forEach((t)=>{try{if(!t.src&&t.dataset.src)t.src=t.dataset.src;if(t.src)t.reload()}catch(c){}})},n=function(){e=setInterval(a,i)},d=function(){clearInterval(e)},e;document.addEventListener("visibilitychange",()=>document.hidden?d():n()),n()}
|
|
2
|
+
//# sourceMappingURL=application.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["src/application.js"],"sourcesContent":["import * as Turbo from \"turbo\";\n\n// -- Custom confirm dialog (replaces browser confirm) --\nTurbo.config.forms.confirm = (message, element) => {\n const dialog = document.getElementById(\"pgbus-confirm-dialog\");\n const messageEl = document.getElementById(\"pgbus-confirm-message\");\n const titleEl = document.getElementById(\"pgbus-confirm-title\");\n const confirmBtn = document.getElementById(\"pgbus-confirm-btn\");\n const iconEl = document.getElementById(\"pgbus-confirm-icon\");\n\n const turboMethod = element.getAttribute(\"data-turbo-method\");\n const isDelete = turboMethod === \"delete\";\n\n const i18n = document.getElementById(\"pgbus-i18n\");\n titleEl.textContent = isDelete\n ? (i18n?.dataset.deleteTitle || \"Delete\")\n : (i18n?.dataset.confirmTitle || \"Are you sure?\");\n messageEl.textContent = message;\n\n confirmBtn.className = \"rounded-md px-4 py-2 text-sm font-medium text-white focus:outline-none focus:ring-2\";\n if (isDelete) {\n confirmBtn.classList.add(\"bg-red-600\", \"hover:bg-red-500\", \"focus:ring-red-500\");\n confirmBtn.textContent = i18n?.dataset.deleteLabel || \"Delete\";\n iconEl.className = \"flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-red-100 dark:bg-red-900/30\";\n } else {\n confirmBtn.classList.add(\"bg-yellow-500\", \"hover:bg-yellow-400\", \"focus:ring-yellow-500\");\n confirmBtn.textContent = i18n?.dataset.confirmLabel || \"Confirm\";\n iconEl.className = \"flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-yellow-100 dark:bg-yellow-900/30\";\n }\n\n dialog.showModal();\n\n return new Promise((resolve) => {\n dialog.addEventListener(\"close\", () => {\n resolve(dialog.returnValue === \"confirm\");\n }, { once: true });\n });\n};\n\n// -- Toast notifications --\nfunction showToast(message, type = \"success\") {\n const container = document.getElementById(\"pgbus-toast-container\");\n if (!container) return;\n const toast = document.createElement(\"div\");\n\n const colors = {\n success: \"bg-green-50 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-200 dark:border-green-800\",\n error: \"bg-red-50 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-800\",\n info: \"bg-blue-50 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 border-blue-200 dark:border-blue-800\",\n };\n\n toast.className = `rounded-md border p-3 text-sm shadow-lg transition-all duration-300 ${colors[type] || colors.info}`;\n toast.textContent = message;\n container.appendChild(toast);\n\n setTimeout(() => {\n toast.style.opacity = \"0\";\n toast.style.transform = \"translateX(100%)\";\n setTimeout(() => toast.remove(), 300);\n }, 5000);\n}\n\nfunction renderFlashToasts() {\n document.querySelectorAll(\"template[data-pgbus-toast]\").forEach(tpl => {\n showToast(tpl.content.textContent.trim(), tpl.dataset.pgbusToast);\n tpl.remove();\n });\n}\nrenderFlashToasts();\ndocument.addEventListener(\"turbo:load\", renderFlashToasts);\n\n// -- Bulk checkbox selection --\nfunction initBulkSelect() {\n document.querySelectorAll(\"[data-bulk-select-all]\").forEach(selectAll => {\n if (selectAll.dataset.bulkInitialized === \"true\") return;\n selectAll.dataset.bulkInitialized = \"true\";\n\n const scope = selectAll.closest(\"[data-bulk-scope]\") || document;\n const checkboxes = () => scope.querySelectorAll(\"input[data-bulk-item]\");\n // Look for bulk-actions in the parent page container (outside the table scope)\n const pageScope = scope.closest(\"[data-bulk-page]\") || scope.closest(\"turbo-frame\") || scope.parentElement?.closest(\"div\") || document;\n const countEl = pageScope.querySelector(\"[data-bulk-count]\");\n const actions = pageScope.querySelector(\"[data-bulk-actions]\");\n\n function updateUI() {\n const checked = scope.querySelectorAll(\"input[data-bulk-item]:checked\");\n if (countEl) countEl.textContent = checked.length;\n if (actions) actions.classList.toggle(\"hidden\", checked.length === 0);\n\n const all = checkboxes();\n selectAll.checked = all.length > 0 && checked.length === all.length;\n selectAll.indeterminate = checked.length > 0 && checked.length < all.length;\n }\n\n let inSelectAll = false;\n selectAll.addEventListener(\"change\", () => {\n inSelectAll = true;\n checkboxes().forEach(cb => {\n cb.checked = selectAll.checked;\n // Dispatch change event so inline onchange handlers fire (e.g., hidden input sync)\n cb.dispatchEvent(new Event(\"change\", { bubbles: false }));\n });\n inSelectAll = false;\n updateUI();\n });\n\n scope.addEventListener(\"change\", (e) => {\n if (e.target.matches(\"input[data-bulk-item]\")) updateUI();\n });\n });\n\n}\ninitBulkSelect();\ndocument.addEventListener(\"turbo:load\", initBulkSelect);\ndocument.addEventListener(\"turbo:frame-load\", initBulkSelect);\n\n// -- Auto-refresh (pauses when user is interacting) --\nconst refreshInterval = parseInt(document.body?.dataset.pgbusRefreshInterval || \"0\", 10);\nif (refreshInterval > 0) {\n let timer;\n\n function hasUserInteraction() {\n // Pause when any checkbox is checked\n const checked = document.querySelector(\"input[data-bulk-item]:checked, input[data-bulk-select-all]:checked\");\n if (checked) return true;\n // Pause when any job detail row is expanded\n const openDetails = document.querySelector(\"turbo-frame[data-auto-refresh] details[open]\");\n if (openDetails) return true;\n return false;\n }\n\n function refreshFrames() {\n if (document.hidden) return;\n if (hasUserInteraction()) return;\n document.querySelectorAll(\"turbo-frame[data-auto-refresh]\")\n .forEach(frame => {\n try {\n if (!frame.src && frame.dataset.src) frame.src = frame.dataset.src;\n if (frame.src) frame.reload();\n } catch (_) { /* Turbo may abort in-flight fetches during navigation */ }\n });\n }\n function start() { timer = setInterval(refreshFrames, refreshInterval); }\n function stop() { clearInterval(timer); }\n document.addEventListener(\"visibilitychange\", () => document.hidden ? stop() : start());\n start();\n}\n"],"mappings":"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","names":[]}
|