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,594 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="pg-reports-root" content="<%= pg_reports.root_path.chomp('/') %>">
7
+ <title>PgReports Dashboard</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <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">
11
+ <style>
12
+ :root {
13
+ --bg-primary: #0f1114;
14
+ --bg-secondary: #161a1e;
15
+ --bg-tertiary: #1e2328;
16
+ --bg-card: #1a1e23;
17
+ --border-color: #2d3339;
18
+ --text-primary: #d4d7dc;
19
+ --text-secondary: #9ca3ab;
20
+ --text-muted: #6b7280;
21
+ --accent-purple: #9d8cd6;
22
+ --accent-blue: #6b9fe8;
23
+ --accent-green: #5fb89a;
24
+ --accent-amber: #d4a056;
25
+ --accent-rose: #d97084;
26
+ --accent-indigo: #8b8fd9;
27
+ --gradient-start: #6b9fe8;
28
+ --gradient-end: #8b8fd9;
29
+ }
30
+
31
+ * {
32
+ margin: 0;
33
+ padding: 0;
34
+ box-sizing: border-box;
35
+ }
36
+
37
+ body {
38
+ font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
39
+ background: var(--bg-primary);
40
+ color: var(--text-primary);
41
+ min-height: 100vh;
42
+ line-height: 1.6;
43
+ }
44
+
45
+ .container {
46
+ max-width: 1400px;
47
+ margin: 0 auto;
48
+ padding: 2rem;
49
+ }
50
+
51
+ /* Header */
52
+ .header {
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: space-between;
56
+ margin-bottom: 2.5rem;
57
+ padding-bottom: 1.5rem;
58
+ border-bottom: 1px solid var(--border-color);
59
+ }
60
+
61
+ .logo {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 1rem;
65
+ }
66
+
67
+ .logo-icon {
68
+ width: 48px;
69
+ height: 48px;
70
+ background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
71
+ border-radius: 12px;
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ font-size: 1.5rem;
76
+ }
77
+
78
+ .logo-text h1 {
79
+ font-size: 1.5rem;
80
+ font-weight: 700;
81
+ background: linear-gradient(135deg, var(--text-primary), var(--text-secondary));
82
+ -webkit-background-clip: text;
83
+ -webkit-text-fill-color: transparent;
84
+ }
85
+
86
+ .logo-text span {
87
+ font-size: 0.875rem;
88
+ color: var(--text-muted);
89
+ }
90
+
91
+ .header-badge {
92
+ display: flex;
93
+ align-items: center;
94
+ gap: 0.5rem;
95
+ padding: 0.5rem 1rem;
96
+ background: var(--bg-tertiary);
97
+ border: 1px solid var(--border-color);
98
+ border-radius: 9999px;
99
+ font-size: 0.875rem;
100
+ }
101
+
102
+ .badge-dot {
103
+ width: 8px;
104
+ height: 8px;
105
+ border-radius: 50%;
106
+ background: var(--accent-green);
107
+ animation: pulse 2s infinite;
108
+ }
109
+
110
+ .badge-dot.warning {
111
+ background: var(--accent-amber);
112
+ }
113
+
114
+ @keyframes pulse {
115
+ 0%, 100% { opacity: 1; }
116
+ 50% { opacity: 0.5; }
117
+ }
118
+
119
+ /* Categories Grid */
120
+ .categories-grid {
121
+ display: grid;
122
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
123
+ gap: 1.5rem;
124
+ margin-bottom: 2rem;
125
+ }
126
+
127
+ .category-card {
128
+ background: var(--bg-card);
129
+ border: 1px solid var(--border-color);
130
+ border-radius: 16px;
131
+ padding: 1.5rem;
132
+ transition: all 0.2s ease;
133
+ }
134
+
135
+ .category-card:hover {
136
+ border-color: var(--accent-purple);
137
+ transform: translateY(-2px);
138
+ }
139
+
140
+ .category-header {
141
+ display: flex;
142
+ align-items: center;
143
+ gap: 1rem;
144
+ margin-bottom: 1.25rem;
145
+ }
146
+
147
+ .category-icon {
148
+ width: 44px;
149
+ height: 44px;
150
+ border-radius: 12px;
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ font-size: 1.25rem;
155
+ }
156
+
157
+ .category-title {
158
+ font-size: 1.125rem;
159
+ font-weight: 600;
160
+ }
161
+
162
+ .category-count {
163
+ margin-left: auto;
164
+ padding: 0.25rem 0.75rem;
165
+ background: var(--bg-tertiary);
166
+ border-radius: 9999px;
167
+ font-size: 0.75rem;
168
+ color: var(--text-muted);
169
+ }
170
+
171
+ .reports-list {
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 0.5rem;
175
+ }
176
+
177
+ .report-link {
178
+ display: flex;
179
+ align-items: center;
180
+ justify-content: space-between;
181
+ padding: 0.75rem 1rem;
182
+ background: var(--bg-tertiary);
183
+ border: 1px solid transparent;
184
+ border-radius: 10px;
185
+ color: var(--text-secondary);
186
+ text-decoration: none;
187
+ font-size: 0.9rem;
188
+ transition: all 0.15s ease;
189
+ }
190
+
191
+ .report-link:hover {
192
+ background: var(--bg-secondary);
193
+ border-color: var(--border-color);
194
+ color: var(--text-primary);
195
+ }
196
+
197
+ .report-link .arrow {
198
+ opacity: 0;
199
+ transform: translateX(-4px);
200
+ transition: all 0.15s ease;
201
+ }
202
+
203
+ .report-link:hover .arrow {
204
+ opacity: 1;
205
+ transform: translateX(0);
206
+ }
207
+
208
+ /* Report Detail Page */
209
+ .report-page {
210
+ display: flex;
211
+ flex-direction: column;
212
+ gap: 1.5rem;
213
+ }
214
+
215
+ .breadcrumb {
216
+ display: flex;
217
+ align-items: center;
218
+ gap: 0.5rem;
219
+ font-size: 0.875rem;
220
+ color: var(--text-muted);
221
+ }
222
+
223
+ .breadcrumb a {
224
+ color: var(--text-secondary);
225
+ text-decoration: none;
226
+ transition: color 0.15s;
227
+ }
228
+
229
+ .breadcrumb a:hover {
230
+ color: var(--accent-purple);
231
+ }
232
+
233
+ .report-header {
234
+ display: flex;
235
+ align-items: flex-start;
236
+ justify-content: space-between;
237
+ gap: 2rem;
238
+ }
239
+
240
+ .report-info h2 {
241
+ font-size: 1.75rem;
242
+ font-weight: 700;
243
+ margin-bottom: 0.5rem;
244
+ }
245
+
246
+ .report-info p {
247
+ color: var(--text-muted);
248
+ }
249
+
250
+ .report-actions {
251
+ display: flex;
252
+ gap: 0.75rem;
253
+ }
254
+
255
+ .btn {
256
+ display: inline-flex;
257
+ align-items: center;
258
+ justify-content: center;
259
+ gap: 0.5rem;
260
+ height: 40px;
261
+ padding: 0 1.25rem;
262
+ border: 1px solid transparent;
263
+ border-radius: 10px;
264
+ font-family: inherit;
265
+ font-size: 0.9rem;
266
+ font-weight: 500;
267
+ cursor: pointer;
268
+ transition: all 0.15s ease;
269
+ white-space: nowrap;
270
+ text-decoration: none;
271
+ }
272
+
273
+ .btn-primary {
274
+ background: linear-gradient(135deg, var(--gradient-start), var(--gradient-end));
275
+ color: white;
276
+ border-color: transparent;
277
+ }
278
+
279
+ .btn-primary:hover {
280
+ opacity: 0.9;
281
+ transform: translateY(-1px);
282
+ }
283
+
284
+ .btn-secondary {
285
+ background: var(--bg-tertiary);
286
+ color: var(--text-secondary);
287
+ border: 1px solid var(--border-color);
288
+ }
289
+
290
+ .btn-secondary:hover {
291
+ background: var(--bg-secondary);
292
+ color: var(--text-primary);
293
+ }
294
+
295
+ .btn-telegram {
296
+ background: #4a9ebe;
297
+ color: white;
298
+ border-color: #4a9ebe;
299
+ }
300
+
301
+ .btn-telegram:hover {
302
+ background: #5aaccc;
303
+ border-color: #5aaccc;
304
+ }
305
+
306
+ .btn:disabled {
307
+ opacity: 0.5;
308
+ cursor: not-allowed;
309
+ }
310
+
311
+ /* Results Table */
312
+ .results-container {
313
+ background: var(--bg-card);
314
+ border: 1px solid var(--border-color);
315
+ border-radius: 16px;
316
+ overflow: hidden;
317
+ }
318
+
319
+ .results-header {
320
+ display: flex;
321
+ align-items: center;
322
+ justify-content: space-between;
323
+ padding: 1.25rem 1.5rem;
324
+ border-bottom: 1px solid var(--border-color);
325
+ }
326
+
327
+ .results-title {
328
+ font-weight: 600;
329
+ }
330
+
331
+ .results-meta {
332
+ display: flex;
333
+ align-items: center;
334
+ gap: 1.5rem;
335
+ color: var(--text-muted);
336
+ font-size: 0.875rem;
337
+ }
338
+
339
+ .results-table-wrapper {
340
+ overflow-x: auto;
341
+ }
342
+
343
+ .results-table {
344
+ width: 100%;
345
+ border-collapse: collapse;
346
+ font-family: 'JetBrains Mono', monospace;
347
+ font-size: 0.8rem;
348
+ }
349
+
350
+ .results-table th {
351
+ padding: 1rem 1.25rem;
352
+ text-align: left;
353
+ background: var(--bg-tertiary);
354
+ color: var(--text-muted);
355
+ font-weight: 500;
356
+ text-transform: uppercase;
357
+ font-size: 0.7rem;
358
+ letter-spacing: 0.05em;
359
+ white-space: nowrap;
360
+ border-bottom: 1px solid var(--border-color);
361
+ }
362
+
363
+ .results-table td {
364
+ padding: 0.875rem 1.25rem;
365
+ border-bottom: 1px solid var(--border-color);
366
+ color: var(--text-secondary);
367
+ max-width: 400px;
368
+ overflow: hidden;
369
+ text-overflow: ellipsis;
370
+ white-space: nowrap;
371
+ }
372
+
373
+ .results-table tr:hover td {
374
+ background: var(--bg-tertiary);
375
+ color: var(--text-primary);
376
+ }
377
+
378
+ .results-table td.query-cell {
379
+ max-width: 500px;
380
+ font-size: 0.75rem;
381
+ }
382
+
383
+ .empty-state {
384
+ padding: 4rem 2rem;
385
+ text-align: center;
386
+ color: var(--text-muted);
387
+ }
388
+
389
+ .empty-state-icon {
390
+ font-size: 3rem;
391
+ margin-bottom: 1rem;
392
+ }
393
+
394
+ .loading {
395
+ display: flex;
396
+ align-items: center;
397
+ justify-content: center;
398
+ padding: 4rem 2rem;
399
+ }
400
+
401
+ .spinner {
402
+ width: 40px;
403
+ height: 40px;
404
+ border: 3px solid var(--border-color);
405
+ border-top-color: var(--accent-purple);
406
+ border-radius: 50%;
407
+ animation: spin 0.8s linear infinite;
408
+ }
409
+
410
+ @keyframes spin {
411
+ to { transform: rotate(360deg); }
412
+ }
413
+
414
+ /* Error state */
415
+ .error-message {
416
+ padding: 1.5rem;
417
+ background: rgba(244, 63, 94, 0.1);
418
+ border: 1px solid rgba(244, 63, 94, 0.3);
419
+ border-radius: 12px;
420
+ color: var(--accent-rose);
421
+ }
422
+
423
+ /* Toast notification */
424
+ .toast {
425
+ position: fixed;
426
+ bottom: 2rem;
427
+ right: 2rem;
428
+ padding: 1rem 1.5rem;
429
+ background: var(--bg-card);
430
+ border: 1px solid var(--border-color);
431
+ border-radius: 12px;
432
+ color: var(--text-primary);
433
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
434
+ transform: translateY(100px);
435
+ opacity: 0;
436
+ transition: all 0.3s ease;
437
+ z-index: 1000;
438
+ }
439
+
440
+ .toast.show {
441
+ transform: translateY(0);
442
+ opacity: 1;
443
+ }
444
+
445
+ .toast.success {
446
+ border-color: var(--accent-green);
447
+ }
448
+
449
+ .toast.error {
450
+ border-color: var(--accent-rose);
451
+ }
452
+
453
+ /* Modal */
454
+ .modal {
455
+ position: fixed;
456
+ inset: 0;
457
+ background: rgba(0, 0, 0, 0.7);
458
+ display: flex;
459
+ align-items: center;
460
+ justify-content: center;
461
+ z-index: 1000;
462
+ backdrop-filter: blur(4px);
463
+ }
464
+
465
+ .modal-content {
466
+ background: var(--bg-card);
467
+ border: 1px solid var(--border-color);
468
+ border-radius: 16px;
469
+ max-width: 600px;
470
+ width: 90%;
471
+ max-height: 80vh;
472
+ overflow: auto;
473
+ }
474
+
475
+ .modal-large {
476
+ max-width: 900px;
477
+ width: 95%;
478
+ }
479
+
480
+ .modal-header {
481
+ display: flex;
482
+ align-items: center;
483
+ justify-content: space-between;
484
+ padding: 1.25rem 1.5rem;
485
+ border-bottom: 1px solid var(--border-color);
486
+ }
487
+
488
+ .modal-header h3 {
489
+ font-size: 1.125rem;
490
+ font-weight: 600;
491
+ }
492
+
493
+ .modal-close {
494
+ width: 32px;
495
+ height: 32px;
496
+ background: var(--bg-tertiary);
497
+ border: 1px solid var(--border-color);
498
+ border-radius: 8px;
499
+ color: var(--text-muted);
500
+ font-size: 1.25rem;
501
+ cursor: pointer;
502
+ transition: all 0.15s;
503
+ }
504
+
505
+ .modal-close:hover {
506
+ background: var(--accent-rose);
507
+ border-color: var(--accent-rose);
508
+ color: white;
509
+ }
510
+
511
+ .modal-body {
512
+ padding: 1.5rem;
513
+ }
514
+
515
+ /* Responsive */
516
+ @media (max-width: 768px) {
517
+ .container {
518
+ padding: 1rem;
519
+ }
520
+
521
+ .header {
522
+ flex-direction: column;
523
+ gap: 1rem;
524
+ align-items: flex-start;
525
+ }
526
+
527
+ .report-header {
528
+ flex-direction: column;
529
+ }
530
+
531
+ .report-actions {
532
+ width: 100%;
533
+ }
534
+
535
+ .btn {
536
+ flex: 1;
537
+ justify-content: center;
538
+ }
539
+ }
540
+ </style>
541
+ </head>
542
+ <body>
543
+ <div class="container">
544
+ <%= yield %>
545
+ </div>
546
+
547
+ <div id="toast" class="toast"></div>
548
+
549
+ <script>
550
+ const pgReportsRoot = document.querySelector('meta[name="pg-reports-root"]')?.content || '/pg_reports';
551
+
552
+ function showToast(message, type = 'success') {
553
+ const toast = document.getElementById('toast');
554
+ toast.textContent = message;
555
+ toast.className = 'toast show ' + type;
556
+ setTimeout(() => {
557
+ toast.className = 'toast';
558
+ }, 3000);
559
+ }
560
+
561
+ async function sendToTelegram(category, report, button) {
562
+ if (button) {
563
+ button.disabled = true;
564
+ button.innerHTML = '<span class="spinner" style="width:16px;height:16px;border-width:2px;display:inline-block;vertical-align:middle;"></span> Sending...';
565
+ }
566
+
567
+ try {
568
+ const response = await fetch(`${pgReportsRoot}/${category}/${report}/telegram`, {
569
+ method: 'POST',
570
+ headers: {
571
+ 'Content-Type': 'application/json',
572
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
573
+ }
574
+ });
575
+
576
+ const data = await response.json();
577
+
578
+ if (data.success) {
579
+ showToast('Report sent to Telegram');
580
+ } else {
581
+ showToast(data.error || 'Failed to send to Telegram', 'error');
582
+ }
583
+ } catch (error) {
584
+ showToast('Network error: ' + error.message, 'error');
585
+ }
586
+
587
+ if (button) {
588
+ button.disabled = false;
589
+ button.innerHTML = '📨 Telegram';
590
+ }
591
+ }
592
+ </script>
593
+ </body>
594
+ </html>