pgbus 0.2.9 → 0.3.0

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.
@@ -4,63 +4,19 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title><%= t("pgbus.layout.title") %></title>
7
- <style>
8
- /* Prevent white flash during navigation in dark mode.
9
- Applied before Tailwind CDN loads so the background is correct immediately. */
10
- html.dark { background-color: #030712; } /* gray-950 */
11
- html.dark body { background-color: #030712; }
7
+ <%= csrf_meta_tags %>
8
+ <%= csp_meta_tag %>
12
9
 
13
- /* Responsive tables: stack rows as cards on small screens */
14
- @media (max-width: 1023px) {
15
- .pgbus-table thead { display: none; }
16
- .pgbus-table tbody tr {
17
- display: block;
18
- margin-bottom: 0.75rem;
19
- border-radius: 0.5rem;
20
- padding: 0.75rem;
21
- border: 1px solid #e5e7eb;
22
- }
23
- html.dark .pgbus-table tbody tr { border-color: #374151; }
24
- .pgbus-table tbody td {
25
- display: flex;
26
- justify-content: space-between;
27
- align-items: baseline;
28
- padding: 0.25rem 0;
29
- border: none;
30
- text-align: right;
31
- }
32
- .pgbus-table tbody td::before {
33
- content: attr(data-label);
34
- font-weight: 600;
35
- font-size: 0.75rem;
36
- text-transform: uppercase;
37
- color: #6b7280;
38
- text-align: left;
39
- margin-right: 1rem;
40
- flex-shrink: 0;
41
- }
42
- html.dark .pgbus-table tbody td::before { color: #9ca3af; }
43
- .pgbus-table tbody td[colspan] {
44
- display: block;
45
- text-align: center;
46
- }
47
- .pgbus-table tbody td[colspan]::before { display: none; }
48
- }
49
- </style>
50
- <script src="https://cdn.tailwindcss.com"></script>
51
- <script>
52
- tailwind.config = { darkMode: 'class' };
53
- // Restore dark mode preference
10
+ <%# Prevent white flash in dark mode must run before stylesheet loads %>
11
+ <script nonce="<%= content_security_policy_nonce %>">
54
12
  if (localStorage.getItem('pgbus-dark') === 'true' ||
55
13
  (!localStorage.getItem('pgbus-dark') && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
56
14
  document.documentElement.classList.add('dark');
57
15
  }
58
- // Dark mode toggle — must be in non-module script for onclick access
59
16
  function toggleDarkMode() {
60
17
  var isDark = document.documentElement.classList.toggle('dark');
61
18
  localStorage.setItem('pgbus-dark', isDark);
62
19
  }
63
- // Mobile menu toggle
64
20
  function toggleMobileMenu() {
65
21
  var menu = document.getElementById('pgbus-mobile-menu');
66
22
  var openIcon = document.getElementById('pgbus-menu-open');
@@ -69,7 +25,6 @@
69
25
  openIcon.classList.toggle('hidden');
70
26
  closeIcon.classList.toggle('hidden');
71
27
  }
72
- // Close locale dropdown when clicking outside
73
28
  document.addEventListener('click', function(e) {
74
29
  var switcher = document.getElementById('pgbus-locale-switcher');
75
30
  var menu = document.getElementById('pgbus-locale-menu');
@@ -78,103 +33,27 @@
78
33
  }
79
34
  });
80
35
  </script>
81
- <script type="module">
82
- import * as Turbo from "https://esm.sh/@hotwired/turbo@8";
83
-
84
- // -- Custom confirm dialog (replaces browser confirm) --
85
- Turbo.config.forms.confirm = (message, element) => {
86
- const dialog = document.getElementById("pgbus-confirm-dialog");
87
- const messageEl = document.getElementById("pgbus-confirm-message");
88
- const titleEl = document.getElementById("pgbus-confirm-title");
89
- const confirmBtn = document.getElementById("pgbus-confirm-btn");
90
- const iconEl = document.getElementById("pgbus-confirm-icon");
91
-
92
- // Detect action type from the element
93
- const turboMethod = element.getAttribute("data-turbo-method");
94
- const isDelete = turboMethod === "delete";
95
-
96
- // Set title based on action
97
- titleEl.textContent = isDelete ? "<%= t("pgbus.dialogs.delete_title", default: "Delete") %>" : "<%= t("pgbus.dialogs.confirm_title", default: "Are you sure?") %>";
98
- messageEl.textContent = message;
99
-
100
- // Style confirm button based on action severity
101
- confirmBtn.className = "rounded-md px-4 py-2 text-sm font-medium text-white focus:outline-none focus:ring-2";
102
- if (isDelete) {
103
- confirmBtn.classList.add("bg-red-600", "hover:bg-red-500", "focus:ring-red-500");
104
- confirmBtn.textContent = "<%= t("pgbus.dialogs.delete", default: "Delete") %>";
105
- iconEl.className = "flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-red-100 dark:bg-red-900/30";
106
- } else {
107
- confirmBtn.classList.add("bg-yellow-500", "hover:bg-yellow-400", "focus:ring-yellow-500");
108
- confirmBtn.textContent = "<%= t("pgbus.dialogs.confirm", default: "Confirm") %>";
109
- iconEl.className = "flex-shrink-0 flex items-center justify-center h-10 w-10 rounded-full bg-yellow-100 dark:bg-yellow-900/30";
110
- }
111
36
 
112
- dialog.showModal();
37
+ <%# Self-hosted assets — no external CDN dependencies %>
38
+ <%= tag.link rel: "stylesheet", href: frontend_static_path(:style, format: :css, locale: nil), nonce: content_security_policy_nonce %>
113
39
 
114
- return new Promise((resolve) => {
115
- dialog.addEventListener("close", () => {
116
- resolve(dialog.returnValue === "confirm");
117
- }, { once: true });
118
- });
119
- };
40
+ <%# Importmap for ES modules %>
41
+ <% importmaps = Pgbus::FrontendsController.js_modules.keys.index_with { |mod| frontend_module_path(mod, format: :js, locale: nil) } %>
42
+ <%= tag.script({ imports: importmaps }.to_json.html_safe, type: "importmap", nonce: content_security_policy_nonce) %>
43
+ <%= tag.script("", src: frontend_static_path(:apexcharts, format: :js, locale: nil), nonce: content_security_policy_nonce) %>
120
44
 
121
- // -- Toast notifications --
122
- function showToast(message, type = "success") {
123
- const container = document.getElementById("pgbus-toast-container");
124
- const toast = document.createElement("div");
125
-
126
- const colors = {
127
- success: "bg-green-50 dark:bg-green-900/30 text-green-800 dark:text-green-300 border-green-200 dark:border-green-800",
128
- error: "bg-red-50 dark:bg-red-900/30 text-red-800 dark:text-red-300 border-red-200 dark:border-red-800",
129
- info: "bg-blue-50 dark:bg-blue-900/30 text-blue-800 dark:text-blue-300 border-blue-200 dark:border-blue-800",
130
- };
131
-
132
- toast.className = `rounded-md border p-3 text-sm shadow-lg transition-all duration-300 ${colors[type] || colors.info}`;
133
- toast.textContent = message;
134
- container.appendChild(toast);
135
-
136
- setTimeout(() => {
137
- toast.style.opacity = "0";
138
- toast.style.transform = "translateX(100%)";
139
- setTimeout(() => toast.remove(), 300);
140
- }, 5000);
141
- }
142
-
143
- // Render flash toasts from <template> tags.
144
- // Must run on turbo:load as well — module scripts only execute once,
145
- // but Turbo Drive replaces the body on navigation.
146
- function renderFlashToasts() {
147
- document.querySelectorAll("template[data-pgbus-toast]").forEach(tpl => {
148
- showToast(tpl.content.textContent.trim(), tpl.dataset.pgbusToast);
149
- tpl.remove();
150
- });
151
- }
152
- renderFlashToasts();
153
- document.addEventListener("turbo:load", renderFlashToasts);
154
-
155
- <% if Pgbus.configuration.web_live_updates %>
156
- const interval = <%= Pgbus.configuration.web_refresh_interval %>;
157
- if (interval > 0) {
158
- let timer;
159
- function refreshFrames() {
160
- if (document.hidden) return;
161
- document.querySelectorAll("turbo-frame[data-auto-refresh]")
162
- .forEach(frame => {
163
- try {
164
- if (!frame.src && frame.dataset.src) frame.src = frame.dataset.src;
165
- if (frame.src) frame.reload();
166
- } catch (_) { /* Turbo may abort in-flight fetches during navigation */ }
167
- });
168
- }
169
- function start() { timer = setInterval(refreshFrames, interval); }
170
- function stop() { clearInterval(timer); }
171
- document.addEventListener("visibilitychange", () => document.hidden ? stop() : start());
172
- start();
173
- }
174
- <% end %>
45
+ <script type="module" nonce="<%= content_security_policy_nonce %>">
46
+ import "application";
175
47
  </script>
176
48
  </head>
177
- <body class="h-full bg-gray-50 dark:bg-gray-950 transition-colors">
49
+ <body class="h-full bg-gray-50 dark:bg-gray-950 transition-colors"
50
+ <% if Pgbus.configuration.web_live_updates %>data-pgbus-refresh-interval="<%= Pgbus.configuration.web_refresh_interval %>"<% end %>>
51
+ <%# i18n data for JS modules %>
52
+ <div id="pgbus-i18n" class="hidden"
53
+ data-delete-title="<%= t("pgbus.dialogs.delete_title", default: "Delete") %>"
54
+ data-confirm-title="<%= t("pgbus.dialogs.confirm_title", default: "Are you sure?") %>"
55
+ data-delete-label="<%= t("pgbus.dialogs.delete", default: "Delete") %>"
56
+ data-confirm-label="<%= t("pgbus.dialogs.confirm", default: "Confirm") %>"></div>
178
57
  <div class="min-h-full">
179
58
  <!-- Top nav -->
180
59
  <nav class="bg-gray-900 dark:bg-gray-950 border-b border-gray-800">
@@ -93,91 +93,30 @@
93
93
  </table>
94
94
  </div>
95
95
 
96
- <script src="https://cdn.jsdelivr.net/npm/apexcharts@4"></script>
97
- <script>
98
- (function() {
99
- // IIFE prevents "redeclaration of let" when Turbo re-executes on navigation
100
- var throughputChart, statusChart;
96
+ <script type="module" nonce="<%= content_security_policy_nonce %>">
97
+ import { renderCharts, observeThemeChanges } from "charts";
101
98
 
102
- function getThemeColors() {
103
- var isDark = document.documentElement.classList.contains('dark');
104
- return {
105
- isDark: isDark,
106
- text: isDark ? '#9ca3af' : '#6b7280',
107
- grid: isDark ? '#374151' : '#e5e7eb',
108
- tooltip: isDark ? 'dark' : 'light',
109
- dataLabel: isDark ? '#fff' : '#000'
110
- };
111
- }
99
+ const i18n = {
100
+ seriesName: "<%= j(t("pgbus.insights.show.charts.series_name")) %>",
101
+ noData: "<%= j(t("pgbus.insights.show.charts.no_data")) %>",
102
+ failedToLoad: "<%= j(t("pgbus.insights.show.charts.failed_to_load")) %>",
103
+ };
112
104
 
113
- function renderCharts(data) {
114
- var t = getThemeColors();
105
+ let chartData = null;
115
106
 
116
- if (throughputChart) throughputChart.destroy();
117
- if (statusChart) statusChart.destroy();
107
+ observeThemeChanges(() => chartData, i18n);
118
108
 
119
- var throughputData = data.throughput.map(function(p) {
120
- return { x: new Date(p.time).getTime(), y: p.count };
121
- });
122
-
123
- throughputChart = new ApexCharts(document.querySelector('#throughput-chart'), {
124
- series: [{ name: '<%= j(t("pgbus.insights.show.charts.series_name")) %>', data: throughputData }],
125
- chart: { type: 'area', height: 280, toolbar: { show: false }, background: 'transparent', foreColor: t.text },
126
- stroke: { curve: 'smooth', width: 2 },
127
- fill: { type: 'gradient', gradient: { shadeIntensity: 1, opacityFrom: 0.4, opacityTo: 0.05, stops: [0, 100] } },
128
- colors: ['#6366f1'],
129
- xaxis: { type: 'datetime', labels: { style: { colors: t.text } } },
130
- yaxis: { labels: { style: { colors: t.text } } },
131
- grid: { borderColor: t.grid },
132
- tooltip: { theme: t.tooltip },
133
- dataLabels: { enabled: false }
134
- });
135
- throughputChart.render();
136
-
137
- var statusLabels = Object.keys(data.status_counts);
138
- var statusValues = Object.values(data.status_counts);
139
- var statusColors = statusLabels.map(function(s) {
140
- if (s === 'success') return '#10b981';
141
- if (s === 'failed') return '#ef4444';
142
- if (s === 'dead_lettered') return '#f97316';
143
- return '#6b7280';
144
- });
145
-
146
- if (statusLabels.length > 0) {
147
- statusChart = new ApexCharts(document.querySelector('#status-chart'), {
148
- series: statusValues, labels: statusLabels,
149
- chart: { type: 'donut', height: 280, background: 'transparent', foreColor: t.text },
150
- colors: statusColors,
151
- legend: { position: 'bottom', labels: { colors: t.text } },
152
- plotOptions: { pie: { donut: { size: '60%' } } },
153
- dataLabels: { style: { colors: [t.dataLabel] } },
154
- tooltip: { theme: t.tooltip }
155
- });
156
- statusChart.render();
157
- } else {
158
- document.querySelector('#status-chart').innerHTML =
159
- '<p class="text-center text-sm text-gray-400 dark:text-gray-500 pt-24"><%= j(t("pgbus.insights.show.charts.no_data")) %></p>';
160
- }
161
- }
162
-
163
- var chartData = null;
164
- fetch('<%= pgbus.api_insights_path(minutes: @minutes) %>')
165
- .then(function(r) {
166
- if (!r.ok) throw new Error('HTTP ' + r.status);
109
+ fetch("<%= pgbus.api_insights_path(minutes: @minutes) %>")
110
+ .then(r => {
111
+ if (!r.ok) throw new Error("HTTP " + r.status);
167
112
  return r.json();
168
113
  })
169
- .then(function(data) { chartData = data; renderCharts(data); })
170
- .catch(function(err) {
171
- if (err.name === 'AbortError') return;
172
- var msg = '<p class="text-center text-sm text-gray-400 dark:text-gray-500 pt-24"><%= j(t("pgbus.insights.show.charts.failed_to_load")) %></p>';
173
- var el1 = document.querySelector('#throughput-chart');
174
- var el2 = document.querySelector('#status-chart');
114
+ .then(data => { chartData = data; renderCharts(data, i18n); })
115
+ .catch(err => {
116
+ const msg = '<p class="text-center text-sm text-gray-400 dark:text-gray-500 pt-24">' + i18n.failedToLoad + "</p>";
117
+ const el1 = document.querySelector("#throughput-chart");
118
+ const el2 = document.querySelector("#status-chart");
175
119
  if (el1) el1.innerHTML = msg;
176
120
  if (el2) el2.innerHTML = msg;
177
121
  });
178
-
179
- new MutationObserver(function() {
180
- if (chartData) renderCharts(chartData);
181
- }).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
182
- })();
183
122
  </script>
data/config/routes.rb CHANGED
@@ -60,4 +60,9 @@ Pgbus::Engine.routes.draw do
60
60
  get :stats, to: "stats#show"
61
61
  get :insights, to: "insights#show"
62
62
  end
63
+
64
+ scope :frontend, controller: :frontends, defaults: { version: Pgbus::VERSION.tr(".", "-") } do
65
+ get "modules/:version/:id", action: :module, as: :frontend_module, constraints: { format: "js" }
66
+ get "static/:version/:id", action: :static, as: :frontend_static
67
+ end
63
68
  end
data/lib/pgbus/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgbus
4
- VERSION = "0.2.9"
4
+ VERSION = "0.3.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgbus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.9
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -127,6 +127,7 @@ files:
127
127
  - app/controllers/pgbus/dashboard_controller.rb
128
128
  - app/controllers/pgbus/dead_letter_controller.rb
129
129
  - app/controllers/pgbus/events_controller.rb
130
+ - app/controllers/pgbus/frontends_controller.rb
130
131
  - app/controllers/pgbus/insights_controller.rb
131
132
  - app/controllers/pgbus/jobs_controller.rb
132
133
  - app/controllers/pgbus/locale_controller.rb
@@ -135,6 +136,12 @@ files:
135
136
  - app/controllers/pgbus/processes_controller.rb
136
137
  - app/controllers/pgbus/queues_controller.rb
137
138
  - app/controllers/pgbus/recurring_tasks_controller.rb
139
+ - app/frontend/pgbus/application.js
140
+ - app/frontend/pgbus/modules/charts.js
141
+ - app/frontend/pgbus/style.css
142
+ - app/frontend/pgbus/tailwind.css
143
+ - app/frontend/pgbus/vendor/apexcharts.js
144
+ - app/frontend/pgbus/vendor/turbo.js
138
145
  - app/helpers/pgbus/application_helper.rb
139
146
  - app/models/pgbus/application_record.rb
140
147
  - app/models/pgbus/batch_entry.rb