pg_reports 0.1.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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +22 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +335 -0
  5. data/app/controllers/pg_reports/dashboard_controller.rb +133 -0
  6. data/app/views/layouts/pg_reports/application.html.erb +594 -0
  7. data/app/views/pg_reports/dashboard/index.html.erb +435 -0
  8. data/app/views/pg_reports/dashboard/show.html.erb +481 -0
  9. data/config/routes.rb +13 -0
  10. data/lib/pg_reports/annotation_parser.rb +114 -0
  11. data/lib/pg_reports/configuration.rb +83 -0
  12. data/lib/pg_reports/dashboard/reports_registry.rb +89 -0
  13. data/lib/pg_reports/engine.rb +22 -0
  14. data/lib/pg_reports/error.rb +15 -0
  15. data/lib/pg_reports/executor.rb +51 -0
  16. data/lib/pg_reports/modules/connections.rb +106 -0
  17. data/lib/pg_reports/modules/indexes.rb +111 -0
  18. data/lib/pg_reports/modules/queries.rb +140 -0
  19. data/lib/pg_reports/modules/system.rb +148 -0
  20. data/lib/pg_reports/modules/tables.rb +113 -0
  21. data/lib/pg_reports/report.rb +228 -0
  22. data/lib/pg_reports/sql/connections/active_connections.sql +20 -0
  23. data/lib/pg_reports/sql/connections/blocking_queries.sql +35 -0
  24. data/lib/pg_reports/sql/connections/connection_stats.sql +13 -0
  25. data/lib/pg_reports/sql/connections/idle_connections.sql +19 -0
  26. data/lib/pg_reports/sql/connections/locks.sql +20 -0
  27. data/lib/pg_reports/sql/connections/long_running_queries.sql +21 -0
  28. data/lib/pg_reports/sql/indexes/bloated_indexes.sql +36 -0
  29. data/lib/pg_reports/sql/indexes/duplicate_indexes.sql +38 -0
  30. data/lib/pg_reports/sql/indexes/index_sizes.sql +14 -0
  31. data/lib/pg_reports/sql/indexes/index_usage.sql +19 -0
  32. data/lib/pg_reports/sql/indexes/invalid_indexes.sql +15 -0
  33. data/lib/pg_reports/sql/indexes/missing_indexes.sql +27 -0
  34. data/lib/pg_reports/sql/indexes/unused_indexes.sql +18 -0
  35. data/lib/pg_reports/sql/queries/all_queries.sql +20 -0
  36. data/lib/pg_reports/sql/queries/expensive_queries.sql +22 -0
  37. data/lib/pg_reports/sql/queries/heavy_queries.sql +17 -0
  38. data/lib/pg_reports/sql/queries/low_cache_hit_queries.sql +19 -0
  39. data/lib/pg_reports/sql/queries/missing_index_queries.sql +25 -0
  40. data/lib/pg_reports/sql/queries/slow_queries.sql +17 -0
  41. data/lib/pg_reports/sql/system/activity_overview.sql +29 -0
  42. data/lib/pg_reports/sql/system/cache_stats.sql +19 -0
  43. data/lib/pg_reports/sql/system/database_sizes.sql +10 -0
  44. data/lib/pg_reports/sql/system/extensions.sql +12 -0
  45. data/lib/pg_reports/sql/system/settings.sql +33 -0
  46. data/lib/pg_reports/sql/tables/bloated_tables.sql +23 -0
  47. data/lib/pg_reports/sql/tables/cache_hit_ratios.sql +24 -0
  48. data/lib/pg_reports/sql/tables/recently_modified.sql +20 -0
  49. data/lib/pg_reports/sql/tables/row_counts.sql +18 -0
  50. data/lib/pg_reports/sql/tables/seq_scans.sql +26 -0
  51. data/lib/pg_reports/sql/tables/table_sizes.sql +16 -0
  52. data/lib/pg_reports/sql/tables/vacuum_needed.sql +22 -0
  53. data/lib/pg_reports/sql_loader.rb +35 -0
  54. data/lib/pg_reports/telegram_sender.rb +83 -0
  55. data/lib/pg_reports/version.rb +5 -0
  56. data/lib/pg_reports.rb +114 -0
  57. metadata +184 -0
@@ -0,0 +1,435 @@
1
+ <%= csrf_meta_tags %>
2
+
3
+ <header class="header">
4
+ <div class="logo">
5
+ <div class="logo-icon">🐘</div>
6
+ <div class="logo-text">
7
+ <h1>PgReports</h1>
8
+ <span>PostgreSQL Analysis Dashboard</span>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="header-actions">
13
+ <% if @pg_stat_status[:ready] %>
14
+ <button class="btn btn-small btn-muted" onclick="showResetConfirmModal()" id="reset-btn">
15
+ 🗑️ Reset Statistics
16
+ </button>
17
+ <% else %>
18
+ <button class="btn btn-small btn-primary" onclick="enablePgStatStatements(this)" id="enable-btn">
19
+ ⚡ Create Extension
20
+ </button>
21
+ <button class="btn-info" onclick="showPgStatInfo()">?</button>
22
+ <% end %>
23
+ <div class="header-badge" id="pg-stat-badge">
24
+ <% if @pg_stat_status[:ready] %>
25
+ <span class="badge-dot"></span>
26
+ <span>pg_stat_statements ready</span>
27
+ <% elsif @pg_stat_status[:extension_installed] %>
28
+ <span class="badge-dot warning"></span>
29
+ <span>Extension installed, not preloaded</span>
30
+ <% elsif @pg_stat_status[:preloaded] %>
31
+ <span class="badge-dot warning"></span>
32
+ <span>Preloaded, extension not created</span>
33
+ <% else %>
34
+ <span class="badge-dot error"></span>
35
+ <span>Not configured</span>
36
+ <% end %>
37
+ </div>
38
+ </div>
39
+ </header>
40
+
41
+ <!-- pg_stat_statements info modal -->
42
+ <div id="pg-stat-modal" class="modal" style="display: none;">
43
+ <div class="modal-content">
44
+ <div class="modal-header">
45
+ <h3>Enable pg_stat_statements</h3>
46
+ <button class="modal-close" onclick="closePgStatModal()">×</button>
47
+ </div>
48
+ <div class="modal-body">
49
+ <p>To enable pg_stat_statements, follow these steps:</p>
50
+ <ol>
51
+ <li>
52
+ <strong>Edit postgresql.conf:</strong>
53
+ <pre>shared_preload_libraries = 'pg_stat_statements'
54
+ pg_stat_statements.track = all</pre>
55
+ </li>
56
+ <li>
57
+ <strong>Restart PostgreSQL:</strong>
58
+ <pre>sudo systemctl restart postgresql</pre>
59
+ </li>
60
+ <li>
61
+ <strong>Create extension:</strong>
62
+ <pre>CREATE EXTENSION IF NOT EXISTS pg_stat_statements;</pre>
63
+ <p>Or click "Enable" button after restart.</p>
64
+ </li>
65
+ </ol>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ <!-- Reset statistics confirmation modal -->
71
+ <div id="reset-confirm-modal" class="modal" style="display: none;">
72
+ <div class="modal-content modal-small">
73
+ <div class="modal-header modal-header-danger">
74
+ <h3>⚠️ Reset Statistics</h3>
75
+ <button class="modal-close" onclick="closeResetConfirmModal()">×</button>
76
+ </div>
77
+ <div class="modal-body">
78
+ <p class="warning-text">Are you sure you want to reset pg_stat_statements statistics?</p>
79
+ <p class="warning-subtext">This action will clear all collected query statistics and cannot be undone.</p>
80
+ <div class="modal-actions">
81
+ <button class="btn btn-secondary" onclick="closeResetConfirmModal()">Cancel</button>
82
+ <button class="btn btn-danger" onclick="resetStatistics()" id="confirm-reset-btn">Yes, Reset</button>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <div class="categories-grid">
89
+ <% @categories.each do |category_key, category| %>
90
+ <div class="category-card<%= ' disabled' if category_key == :queries && !@pg_stat_status[:ready] %>">
91
+ <% if category_key == :queries && !@pg_stat_status[:ready] %>
92
+ <div class="category-warning">
93
+ <span>🔒</span> Requires pg_stat_statements
94
+ </div>
95
+ <% end %>
96
+ <div class="category-header">
97
+ <div class="category-icon" style="background: <%= category[:color] %>20; color: <%= category[:color] %>;">
98
+ <%= category[:icon] %>
99
+ </div>
100
+ <span class="category-title"><%= category[:name] %></span>
101
+ <span class="category-count"><%= category[:reports].size %> reports</span>
102
+ </div>
103
+
104
+ <div class="reports-list">
105
+ <% category[:reports].each do |report_key, report| %>
106
+ <% if category_key == :queries && !@pg_stat_status[:ready] %>
107
+ <div class="report-link disabled">
108
+ <span><%= report[:name] %></span>
109
+ <span class="lock">🔒</span>
110
+ </div>
111
+ <% else %>
112
+ <%= link_to report_path(category: category_key, report: report_key), class: "report-link" do %>
113
+ <span><%= report[:name] %></span>
114
+ <span class="arrow">→</span>
115
+ <% end %>
116
+ <% end %>
117
+ <% end %>
118
+ </div>
119
+ </div>
120
+ <% end %>
121
+ </div>
122
+
123
+ <style>
124
+ .header-actions {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 0.5rem;
128
+ }
129
+
130
+ .btn.btn-small {
131
+ padding: 0.5rem 1rem;
132
+ font-size: 0.8rem;
133
+ }
134
+
135
+ .btn-info {
136
+ width: 24px;
137
+ height: 24px;
138
+ padding: 0;
139
+ background: var(--bg-tertiary);
140
+ color: var(--text-muted);
141
+ border: 1px solid var(--border-color);
142
+ border-radius: 50%;
143
+ font-size: 0.8rem;
144
+ font-weight: 600;
145
+ cursor: pointer;
146
+ transition: all 0.15s;
147
+ }
148
+ .btn-info:hover {
149
+ background: var(--accent-purple);
150
+ color: white;
151
+ border-color: var(--accent-purple);
152
+ }
153
+
154
+ .btn.btn-danger {
155
+ background: var(--accent-rose);
156
+ border-color: var(--accent-rose);
157
+ }
158
+ .btn.btn-danger:hover {
159
+ background: #c45a6e;
160
+ border-color: #c45a6e;
161
+ }
162
+
163
+ .btn.btn-secondary {
164
+ background: var(--bg-tertiary);
165
+ border-color: var(--border-color);
166
+ color: var(--text-secondary);
167
+ }
168
+ .btn.btn-secondary:hover {
169
+ background: var(--bg-card);
170
+ color: var(--text-primary);
171
+ }
172
+
173
+ .btn.btn-muted {
174
+ background: var(--bg-tertiary);
175
+ border-color: var(--accent-rose);
176
+ color: var(--text-secondary);
177
+ }
178
+ .btn.btn-muted:hover {
179
+ background: var(--bg-card);
180
+ color: var(--text-primary);
181
+ }
182
+
183
+ .badge-dot.error { background: var(--accent-rose); }
184
+
185
+ .category-card.disabled {
186
+ opacity: 0.7;
187
+ }
188
+
189
+ .category-warning {
190
+ padding: 0.625rem 1rem;
191
+ margin: -1.5rem -1.5rem 1rem -1.5rem;
192
+ background: rgba(245, 158, 11, 0.1);
193
+ border-bottom: 1px solid rgba(245, 158, 11, 0.2);
194
+ border-radius: 16px 16px 0 0;
195
+ color: var(--accent-amber);
196
+ font-size: 0.8rem;
197
+ font-weight: 500;
198
+ text-align: center;
199
+ }
200
+
201
+ .report-link.disabled {
202
+ display: flex;
203
+ align-items: center;
204
+ justify-content: space-between;
205
+ padding: 0.75rem 1rem;
206
+ background: var(--bg-tertiary);
207
+ border: 1px solid transparent;
208
+ border-radius: 10px;
209
+ color: var(--text-muted);
210
+ font-size: 0.9rem;
211
+ cursor: not-allowed;
212
+ }
213
+
214
+ .report-link .lock {
215
+ font-size: 0.75rem;
216
+ opacity: 0.5;
217
+ }
218
+
219
+ /* Modal */
220
+ .modal {
221
+ position: fixed;
222
+ inset: 0;
223
+ background: rgba(0, 0, 0, 0.7);
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ z-index: 1000;
228
+ backdrop-filter: blur(4px);
229
+ }
230
+
231
+ .modal-content {
232
+ background: var(--bg-card);
233
+ border: 1px solid var(--border-color);
234
+ border-radius: 16px;
235
+ max-width: 600px;
236
+ width: 90%;
237
+ max-height: 80vh;
238
+ overflow: auto;
239
+ }
240
+
241
+ .modal-header {
242
+ display: flex;
243
+ align-items: center;
244
+ justify-content: space-between;
245
+ padding: 1.25rem 1.5rem;
246
+ border-bottom: 1px solid var(--border-color);
247
+ }
248
+
249
+ .modal-header h3 {
250
+ font-size: 1.125rem;
251
+ font-weight: 600;
252
+ }
253
+
254
+ .modal-close {
255
+ width: 32px;
256
+ height: 32px;
257
+ background: var(--bg-tertiary);
258
+ border: 1px solid var(--border-color);
259
+ border-radius: 8px;
260
+ color: var(--text-muted);
261
+ font-size: 1.25rem;
262
+ cursor: pointer;
263
+ transition: all 0.15s;
264
+ }
265
+
266
+ .modal-close:hover {
267
+ background: var(--accent-rose);
268
+ border-color: var(--accent-rose);
269
+ color: white;
270
+ }
271
+
272
+ .modal-body {
273
+ padding: 1.5rem;
274
+ }
275
+
276
+ .modal-body p {
277
+ color: var(--text-secondary);
278
+ margin-bottom: 1rem;
279
+ }
280
+
281
+ .modal-body ol {
282
+ list-style-position: inside;
283
+ color: var(--text-secondary);
284
+ }
285
+
286
+ .modal-body li {
287
+ margin-bottom: 1.25rem;
288
+ }
289
+
290
+ .modal-body strong {
291
+ color: var(--text-primary);
292
+ }
293
+
294
+ .modal-body pre {
295
+ margin-top: 0.5rem;
296
+ padding: 1rem;
297
+ background: var(--bg-primary);
298
+ border: 1px solid var(--border-color);
299
+ border-radius: 8px;
300
+ font-family: 'JetBrains Mono', monospace;
301
+ font-size: 0.8rem;
302
+ color: var(--accent-green);
303
+ overflow-x: auto;
304
+ }
305
+
306
+ .modal-small {
307
+ max-width: 420px;
308
+ }
309
+
310
+ .modal-header-danger {
311
+ background: rgba(244, 63, 94, 0.1);
312
+ border-bottom-color: rgba(244, 63, 94, 0.2);
313
+ }
314
+
315
+ .modal-header-danger h3 {
316
+ color: var(--accent-rose);
317
+ }
318
+
319
+ .warning-text {
320
+ font-size: 1rem;
321
+ font-weight: 500;
322
+ color: var(--text-primary) !important;
323
+ margin-bottom: 0.5rem !important;
324
+ }
325
+
326
+ .warning-subtext {
327
+ font-size: 0.875rem;
328
+ color: var(--text-muted) !important;
329
+ margin-bottom: 1.5rem !important;
330
+ }
331
+
332
+ .modal-actions {
333
+ display: flex;
334
+ gap: 0.75rem;
335
+ justify-content: flex-end;
336
+ }
337
+
338
+ .modal-actions .btn {
339
+ padding: 0.625rem 1.25rem;
340
+ font-size: 0.875rem;
341
+ }
342
+ </style>
343
+
344
+ <script>
345
+ function showPgStatInfo() {
346
+ document.getElementById('pg-stat-modal').style.display = 'flex';
347
+ }
348
+
349
+ function closePgStatModal() {
350
+ document.getElementById('pg-stat-modal').style.display = 'none';
351
+ }
352
+
353
+ // Close modal on backdrop click
354
+ document.getElementById('pg-stat-modal')?.addEventListener('click', function(e) {
355
+ if (e.target === this) closePgStatModal();
356
+ });
357
+
358
+ function showResetConfirmModal() {
359
+ document.getElementById('reset-confirm-modal').style.display = 'flex';
360
+ }
361
+
362
+ function closeResetConfirmModal() {
363
+ document.getElementById('reset-confirm-modal').style.display = 'none';
364
+ }
365
+
366
+ // Close reset modal on backdrop click
367
+ document.getElementById('reset-confirm-modal')?.addEventListener('click', function(e) {
368
+ if (e.target === this) closeResetConfirmModal();
369
+ });
370
+
371
+ async function resetStatistics() {
372
+ const button = document.getElementById('confirm-reset-btn');
373
+ button.disabled = true;
374
+ button.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px;"></span> Resetting...';
375
+
376
+ try {
377
+ const response = await fetch(`${pgReportsRoot}/reset_statistics`, {
378
+ method: 'POST',
379
+ headers: {
380
+ 'Content-Type': 'application/json',
381
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
382
+ }
383
+ });
384
+
385
+ const data = await response.json();
386
+
387
+ if (data.success) {
388
+ closeResetConfirmModal();
389
+ showToast(data.message);
390
+ } else {
391
+ showToast(data.error || 'Failed to reset statistics', 'error');
392
+ button.disabled = false;
393
+ button.innerHTML = 'Yes, Reset';
394
+ }
395
+ } catch (error) {
396
+ showToast('Network error: ' + error.message, 'error');
397
+ button.disabled = false;
398
+ button.innerHTML = 'Yes, Reset';
399
+ }
400
+ }
401
+
402
+ async function enablePgStatStatements(button) {
403
+ button.disabled = true;
404
+ button.innerHTML = '<span class="spinner" style="width:14px;height:14px;border-width:2px;display:inline-block;vertical-align:middle;margin-right:6px;"></span> Creating...';
405
+
406
+ try {
407
+ const response = await fetch(`${pgReportsRoot}/enable_pg_stat_statements`, {
408
+ method: 'POST',
409
+ headers: {
410
+ 'Content-Type': 'application/json',
411
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
412
+ }
413
+ });
414
+
415
+ const data = await response.json();
416
+
417
+ if (data.success) {
418
+ showToast(data.message);
419
+ // Reload page to update status
420
+ setTimeout(() => location.reload(), 1000);
421
+ } else {
422
+ showToast(data.message, 'error');
423
+ if (data.requires_restart) {
424
+ showPgStatInfo();
425
+ }
426
+ button.disabled = false;
427
+ button.innerHTML = '⚡ Create Extension';
428
+ }
429
+ } catch (error) {
430
+ showToast('Network error: ' + error.message, 'error');
431
+ button.disabled = false;
432
+ button.innerHTML = '⚡ Create Extension';
433
+ }
434
+ }
435
+ </script>