pg_reports 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +55 -3
- data/app/controllers/pg_reports/dashboard_controller.rb +90 -1
- data/app/views/layouts/pg_reports/application.html.erb +2 -1
- data/app/views/pg_reports/dashboard/index.html.erb +33 -2
- data/app/views/pg_reports/dashboard/show.html.erb +1920 -74
- data/config/locales/en.yml +310 -0
- data/config/locales/ru.yml +310 -0
- data/config/locales/uk.yml +310 -0
- data/config/routes.rb +2 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +170 -0
- data/lib/pg_reports/engine.rb +5 -0
- data/lib/pg_reports/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76df6e762c2183af15f4409d137e5705f97249922d6c5c5521ba6a7903d3e3f9
|
|
4
|
+
data.tar.gz: 7e52e17a4fa3b3961a1b3d9c5c64174376e38e0d97732017d3a248df8c19528f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eed06fbb5e836b8ecd1452319e4a0af13ffe750525953fec4ee4aed49e9d04e47df6f8404afe29162950885b41cccdaf4cd1e0ec22f2b87917ee64d931245fc4
|
|
7
|
+
data.tar.gz: 3c6d3b1f7c04781428eaf8d8460177ef4e243d114ad88c83b4419ab15817214804184247a0074dd5f69775d7484205969830dd77169b6de52dc35cda8bda7e24
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.2.1] - 2026-01-28
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Cursor (WSL) IDE support
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Removed test data from production code
|
|
17
|
+
- Copy Query button now works correctly with special characters
|
|
18
|
+
|
|
19
|
+
## [0.2.0] - 2026-01-28
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Sortable table columns - click on column header to sort ascending/descending
|
|
24
|
+
- Top scrollbar for wide tables (synchronized with bottom scroll)
|
|
25
|
+
- Report descriptions in dashboard cards on the main page
|
|
26
|
+
- IDE integration for source code links:
|
|
27
|
+
- VS Code (WSL) - for Windows Subsystem for Linux
|
|
28
|
+
- VS Code - direct path for native Linux
|
|
29
|
+
- RubyMine
|
|
30
|
+
- IntelliJ IDEA
|
|
31
|
+
- Cursor (WSL) - for Windows Subsystem for Linux
|
|
32
|
+
- Cursor
|
|
33
|
+
- IDE settings modal to choose default IDE (skip menu and open directly)
|
|
34
|
+
- Save records for comparison - save query results to compare before/after optimizations
|
|
35
|
+
- EXPLAIN ANALYZE for queries - run EXPLAIN ANALYZE directly from the dashboard
|
|
36
|
+
- Migration generator for unused/broken indexes - generate and create migration files
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Reduced spacing between report description and results table
|
|
41
|
+
- Dropdown menus now use fixed positioning to prevent clipping by table rows
|
|
42
|
+
|
|
8
43
|
## [0.1.0] - 2026-01-17
|
|
9
44
|
|
|
10
45
|
### Added
|
data/README.md
CHANGED
|
@@ -15,9 +15,13 @@ A comprehensive PostgreSQL monitoring and analysis library for Rails application
|
|
|
15
15
|
- 📋 **Table Statistics** - Monitor table sizes, bloat, vacuum needs, and cache hit ratios
|
|
16
16
|
- 🔌 **Connection Monitoring** - Track active connections, locks, and blocking queries
|
|
17
17
|
- 🖥️ **System Overview** - Database sizes, PostgreSQL settings, installed extensions
|
|
18
|
-
- 🌐 **Web Dashboard** - Beautiful dark-themed UI with expandable rows
|
|
18
|
+
- 🌐 **Web Dashboard** - Beautiful dark-themed UI with sortable tables and expandable rows
|
|
19
19
|
- 📨 **Telegram Integration** - Send reports directly to Telegram
|
|
20
20
|
- 📥 **Export** - Download reports in TXT, CSV, or JSON format
|
|
21
|
+
- 🔗 **IDE Integration** - Open source locations in VS Code, Cursor, RubyMine, or IntelliJ (with WSL support)
|
|
22
|
+
- 📌 **Comparison Mode** - Save records to compare before/after optimization
|
|
23
|
+
- 📊 **EXPLAIN ANALYZE** - Run query plans directly from the dashboard
|
|
24
|
+
- 🗑️ **Migration Generator** - Generate Rails migrations to drop unused indexes
|
|
21
25
|
|
|
22
26
|
## Installation
|
|
23
27
|
|
|
@@ -255,13 +259,61 @@ report.select { |row| row["calls"] > 100 }
|
|
|
255
259
|
|
|
256
260
|
The dashboard provides:
|
|
257
261
|
|
|
258
|
-
- 📊 Overview of all report categories
|
|
262
|
+
- 📊 Overview of all report categories with descriptions
|
|
259
263
|
- ⚡ One-click report execution
|
|
260
264
|
- 🔍 Expandable rows for full query text
|
|
261
265
|
- 📋 Copy query to clipboard
|
|
262
|
-
- 📥 Download in multiple formats
|
|
266
|
+
- 📥 Download in multiple formats (TXT, CSV, JSON)
|
|
263
267
|
- 📨 Send to Telegram
|
|
264
268
|
- 🔧 pg_stat_statements management
|
|
269
|
+
- 🔄 Sortable columns - click headers to sort ascending/descending
|
|
270
|
+
- 📌 Save records for comparison - track before/after optimization results
|
|
271
|
+
- 📊 EXPLAIN ANALYZE - run query plans directly from the dashboard
|
|
272
|
+
- 🗑️ Migration generator - create Rails migrations to drop unused indexes
|
|
273
|
+
- 🔗 IDE integration - click source locations to open in your IDE
|
|
274
|
+
|
|
275
|
+
### IDE Integration
|
|
276
|
+
|
|
277
|
+
Click on source locations in reports to open the file directly in your IDE. Supported IDEs:
|
|
278
|
+
|
|
279
|
+
- **VS Code (WSL)** - for Windows Subsystem for Linux
|
|
280
|
+
- **VS Code** - direct path for native Linux/macOS
|
|
281
|
+
- **RubyMine**
|
|
282
|
+
- **IntelliJ IDEA**
|
|
283
|
+
- **Cursor (WSL)** - for Windows Subsystem for Linux
|
|
284
|
+
- **Cursor**
|
|
285
|
+
|
|
286
|
+
Use the ⚙️ button to set your default IDE and skip the selection menu.
|
|
287
|
+
|
|
288
|
+
### Save Records for Comparison
|
|
289
|
+
|
|
290
|
+
When optimizing queries, you can save records to compare before/after results:
|
|
291
|
+
|
|
292
|
+
1. Expand a row and click "📌 Save for Comparison"
|
|
293
|
+
2. Saved records appear above the results table
|
|
294
|
+
3. Click saved records to expand and see all details
|
|
295
|
+
4. Clear all or remove individual saved records
|
|
296
|
+
|
|
297
|
+
Records are stored in browser localStorage per report type.
|
|
298
|
+
|
|
299
|
+
### EXPLAIN ANALYZE
|
|
300
|
+
|
|
301
|
+
For query reports, you can run EXPLAIN ANALYZE directly:
|
|
302
|
+
|
|
303
|
+
1. Expand a row with a query
|
|
304
|
+
2. Click "📊 EXPLAIN ANALYZE"
|
|
305
|
+
3. View the execution plan with timing statistics
|
|
306
|
+
|
|
307
|
+
> Note: Queries with parameter placeholders ($1, $2) from pg_stat_statements cannot be analyzed directly. Copy the query and replace parameters with actual values.
|
|
308
|
+
|
|
309
|
+
### Migration Generator
|
|
310
|
+
|
|
311
|
+
For unused or invalid indexes, generate Rails migrations:
|
|
312
|
+
|
|
313
|
+
1. Go to Indexes → Unused Indexes
|
|
314
|
+
2. Expand a row and click "🗑️ Generate Migration"
|
|
315
|
+
3. Copy the code or create the file directly
|
|
316
|
+
4. The file opens automatically in your configured IDE
|
|
265
317
|
|
|
266
318
|
### Authentication
|
|
267
319
|
|
|
@@ -33,6 +33,11 @@ module PgReports
|
|
|
33
33
|
return
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# Get documentation for the report
|
|
37
|
+
@documentation = Dashboard::ReportsRegistry.documentation(@report_key)
|
|
38
|
+
@thresholds = Dashboard::ReportsRegistry.thresholds(@report_key)
|
|
39
|
+
@problem_fields = Dashboard::ReportsRegistry.problem_fields(@report_key)
|
|
40
|
+
|
|
36
41
|
@report = execute_report(@category, @report_key)
|
|
37
42
|
rescue => e
|
|
38
43
|
@error = e.message
|
|
@@ -44,6 +49,8 @@ module PgReports
|
|
|
44
49
|
report_key = params[:report].to_sym
|
|
45
50
|
|
|
46
51
|
report = execute_report(category, report_key)
|
|
52
|
+
thresholds = Dashboard::ReportsRegistry.thresholds(report_key)
|
|
53
|
+
problem_fields = Dashboard::ReportsRegistry.problem_fields(report_key)
|
|
47
54
|
|
|
48
55
|
render json: {
|
|
49
56
|
success: true,
|
|
@@ -51,7 +58,9 @@ module PgReports
|
|
|
51
58
|
columns: report.columns,
|
|
52
59
|
data: report.data.first(100),
|
|
53
60
|
total: report.size,
|
|
54
|
-
generated_at: report.generated_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
61
|
+
generated_at: report.generated_at.strftime("%Y-%m-%d %H:%M:%S"),
|
|
62
|
+
thresholds: thresholds,
|
|
63
|
+
problem_fields: problem_fields
|
|
55
64
|
}
|
|
56
65
|
rescue => e
|
|
57
66
|
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
@@ -103,6 +112,86 @@ module PgReports
|
|
|
103
112
|
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
104
113
|
end
|
|
105
114
|
|
|
115
|
+
def explain_analyze
|
|
116
|
+
query = params[:query]
|
|
117
|
+
|
|
118
|
+
if query.blank?
|
|
119
|
+
render json: {success: false, error: "Query is required"}, status: :unprocessable_entity
|
|
120
|
+
return
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Security: Only allow SELECT queries for EXPLAIN ANALYZE
|
|
124
|
+
normalized = query.strip.gsub(/\s+/, " ").downcase
|
|
125
|
+
unless normalized.start_with?("select")
|
|
126
|
+
render json: {success: false, error: "Only SELECT queries are allowed for EXPLAIN ANALYZE"}, status: :unprocessable_entity
|
|
127
|
+
return
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Check for parameterized queries (from pg_stat_statements normalization)
|
|
131
|
+
if query.match?(/\$\d+/)
|
|
132
|
+
render json: {
|
|
133
|
+
success: false,
|
|
134
|
+
error: "This query contains parameter placeholders ($1, $2, etc.) from pg_stat_statements normalization. " \
|
|
135
|
+
"EXPLAIN ANALYZE cannot be run on parameterized queries without actual values. " \
|
|
136
|
+
"Copy the query and replace parameters with real values to analyze it manually."
|
|
137
|
+
}, status: :unprocessable_entity
|
|
138
|
+
return
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
result = ActiveRecord::Base.connection.execute("EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) #{query}")
|
|
142
|
+
explain_output = result.map { |r| r["QUERY PLAN"] }.join("\n")
|
|
143
|
+
|
|
144
|
+
# Extract stats from the output
|
|
145
|
+
stats = {}
|
|
146
|
+
if (match = explain_output.match(/Planning Time: ([\d.]+) ms/))
|
|
147
|
+
stats[:planning_time] = match[1].to_f
|
|
148
|
+
end
|
|
149
|
+
if (match = explain_output.match(/Execution Time: ([\d.]+) ms/))
|
|
150
|
+
stats[:execution_time] = match[1].to_f
|
|
151
|
+
end
|
|
152
|
+
if (match = explain_output.match(/cost=[\d.]+\.\.([\d.]+)/))
|
|
153
|
+
stats[:total_cost] = match[1].to_f
|
|
154
|
+
end
|
|
155
|
+
if (match = explain_output.match(/rows=(\d+)/))
|
|
156
|
+
stats[:rows] = match[1].to_i
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
render json: {success: true, explain: explain_output, stats: stats}
|
|
160
|
+
rescue => e
|
|
161
|
+
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def create_migration
|
|
165
|
+
file_name = params[:file_name]
|
|
166
|
+
code = params[:code]
|
|
167
|
+
|
|
168
|
+
if file_name.blank? || code.blank?
|
|
169
|
+
render json: {success: false, error: "File name and code are required"}, status: :unprocessable_entity
|
|
170
|
+
return
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Sanitize file name
|
|
174
|
+
safe_file_name = file_name.gsub(/[^a-z0-9_.]/, "")
|
|
175
|
+
unless safe_file_name.match?(/\A\d{14}_\w+\.rb\z/)
|
|
176
|
+
render json: {success: false, error: "Invalid migration file name format"}, status: :unprocessable_entity
|
|
177
|
+
return
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Find migrations directory
|
|
181
|
+
migrations_path = Rails.root.join("db", "migrate")
|
|
182
|
+
unless migrations_path.exist?
|
|
183
|
+
render json: {success: false, error: "Migrations directory not found"}, status: :unprocessable_entity
|
|
184
|
+
return
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
file_path = migrations_path.join(safe_file_name)
|
|
188
|
+
File.write(file_path, code)
|
|
189
|
+
|
|
190
|
+
render json: {success: true, file_path: file_path.to_s, message: "Migration created successfully"}
|
|
191
|
+
rescue => e
|
|
192
|
+
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
193
|
+
end
|
|
194
|
+
|
|
106
195
|
private
|
|
107
196
|
|
|
108
197
|
def authenticate_dashboard!
|
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<meta name="pg-reports-root" content="<%=
|
|
6
|
+
<meta name="pg-reports-root" content="<%= request.script_name.presence || PgReports::Engine.routes.url_helpers.root_path %>">
|
|
7
7
|
<title>PgReports Dashboard</title>
|
|
8
|
+
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%234f46e5'/%3E%3Crect x='5' y='18' width='5' height='9' rx='1' fill='%23fff'/%3E%3Crect x='13.5' y='12' width='5' height='15' rx='1' fill='%23fff'/%3E%3Crect x='22' y='6' width='5' height='21' rx='1' fill='%23fff'/%3E%3C/svg%3E">
|
|
8
9
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
9
10
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
10
11
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Plus+Jakarta+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
@@ -105,12 +105,18 @@ pg_stat_statements.track = all</pre>
|
|
|
105
105
|
<% category[:reports].each do |report_key, report| %>
|
|
106
106
|
<% if category_key == :queries && !@pg_stat_status[:ready] %>
|
|
107
107
|
<div class="report-link disabled">
|
|
108
|
-
<
|
|
108
|
+
<div class="report-link-info">
|
|
109
|
+
<span class="report-link-name"><%= report[:name] %></span>
|
|
110
|
+
<span class="report-link-desc"><%= report[:description] %></span>
|
|
111
|
+
</div>
|
|
109
112
|
<span class="lock">🔒</span>
|
|
110
113
|
</div>
|
|
111
114
|
<% else %>
|
|
112
115
|
<%= link_to report_path(category: category_key, report: report_key), class: "report-link" do %>
|
|
113
|
-
<
|
|
116
|
+
<div class="report-link-info">
|
|
117
|
+
<span class="report-link-name"><%= report[:name] %></span>
|
|
118
|
+
<span class="report-link-desc"><%= report[:description] %></span>
|
|
119
|
+
</div>
|
|
114
120
|
<span class="arrow">→</span>
|
|
115
121
|
<% end %>
|
|
116
122
|
<% end %>
|
|
@@ -216,6 +222,31 @@ pg_stat_statements.track = all</pre>
|
|
|
216
222
|
opacity: 0.5;
|
|
217
223
|
}
|
|
218
224
|
|
|
225
|
+
.report-link-info {
|
|
226
|
+
display: flex;
|
|
227
|
+
flex-direction: column;
|
|
228
|
+
gap: 0.125rem;
|
|
229
|
+
flex: 1;
|
|
230
|
+
min-width: 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.report-link-name {
|
|
234
|
+
font-weight: 500;
|
|
235
|
+
color: inherit;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.report-link-desc {
|
|
239
|
+
font-size: 0.75rem;
|
|
240
|
+
color: var(--text-muted);
|
|
241
|
+
white-space: nowrap;
|
|
242
|
+
overflow: hidden;
|
|
243
|
+
text-overflow: ellipsis;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.report-link:hover .report-link-desc {
|
|
247
|
+
color: var(--text-secondary);
|
|
248
|
+
}
|
|
249
|
+
|
|
219
250
|
/* Modal */
|
|
220
251
|
.modal {
|
|
221
252
|
position: fixed;
|