pg_reports 0.3.1 โ†’ 0.5.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +140 -0
  3. data/README.md +129 -4
  4. data/app/controllers/pg_reports/dashboard_controller.rb +246 -28
  5. data/app/views/layouts/pg_reports/application.html.erb +283 -1
  6. data/app/views/pg_reports/dashboard/_show_modals.html.erb +8 -1
  7. data/app/views/pg_reports/dashboard/_show_scripts.html.erb +240 -41
  8. data/app/views/pg_reports/dashboard/_show_styles.html.erb +495 -1
  9. data/app/views/pg_reports/dashboard/index.html.erb +419 -0
  10. data/app/views/pg_reports/dashboard/show.html.erb +89 -47
  11. data/config/locales/en.yml +58 -0
  12. data/config/locales/ru.yml +58 -0
  13. data/config/locales/uk.yml +13 -0
  14. data/config/routes.rb +8 -0
  15. data/lib/pg_reports/configuration.rb +13 -0
  16. data/lib/pg_reports/dashboard/reports_registry.rb +38 -1
  17. data/lib/pg_reports/definitions/connections/active_connections.yml +23 -0
  18. data/lib/pg_reports/definitions/connections/blocking_queries.yml +20 -0
  19. data/lib/pg_reports/definitions/connections/connection_churn.yml +49 -0
  20. data/lib/pg_reports/definitions/connections/connection_stats.yml +18 -0
  21. data/lib/pg_reports/definitions/connections/idle_connections.yml +21 -0
  22. data/lib/pg_reports/definitions/connections/locks.yml +22 -0
  23. data/lib/pg_reports/definitions/connections/long_running_queries.yml +43 -0
  24. data/lib/pg_reports/definitions/connections/pool_saturation.yml +42 -0
  25. data/lib/pg_reports/definitions/connections/pool_usage.yml +43 -0
  26. data/lib/pg_reports/definitions/connections/pool_wait_times.yml +44 -0
  27. data/lib/pg_reports/definitions/indexes/bloated_indexes.yml +43 -0
  28. data/lib/pg_reports/definitions/indexes/duplicate_indexes.yml +19 -0
  29. data/lib/pg_reports/definitions/indexes/index_sizes.yml +29 -0
  30. data/lib/pg_reports/definitions/indexes/index_usage.yml +27 -0
  31. data/lib/pg_reports/definitions/indexes/invalid_indexes.yml +19 -0
  32. data/lib/pg_reports/definitions/indexes/missing_indexes.yml +27 -0
  33. data/lib/pg_reports/definitions/indexes/unused_indexes.yml +41 -0
  34. data/lib/pg_reports/definitions/queries/all_queries.yml +35 -0
  35. data/lib/pg_reports/definitions/queries/expensive_queries.yml +43 -0
  36. data/lib/pg_reports/definitions/queries/heavy_queries.yml +49 -0
  37. data/lib/pg_reports/definitions/queries/low_cache_hit_queries.yml +47 -0
  38. data/lib/pg_reports/definitions/queries/missing_index_queries.yml +31 -0
  39. data/lib/pg_reports/definitions/queries/slow_queries.yml +48 -0
  40. data/lib/pg_reports/definitions/system/activity_overview.yml +17 -0
  41. data/lib/pg_reports/definitions/system/cache_stats.yml +18 -0
  42. data/lib/pg_reports/definitions/system/database_sizes.yml +18 -0
  43. data/lib/pg_reports/definitions/system/extensions.yml +19 -0
  44. data/lib/pg_reports/definitions/system/settings.yml +20 -0
  45. data/lib/pg_reports/definitions/tables/bloated_tables.yml +43 -0
  46. data/lib/pg_reports/definitions/tables/cache_hit_ratios.yml +26 -0
  47. data/lib/pg_reports/definitions/tables/recently_modified.yml +27 -0
  48. data/lib/pg_reports/definitions/tables/row_counts.yml +29 -0
  49. data/lib/pg_reports/definitions/tables/seq_scans.yml +31 -0
  50. data/lib/pg_reports/definitions/tables/table_sizes.yml +31 -0
  51. data/lib/pg_reports/definitions/tables/vacuum_needed.yml +39 -0
  52. data/lib/pg_reports/explain_analyzer.rb +338 -0
  53. data/lib/pg_reports/filter.rb +58 -0
  54. data/lib/pg_reports/module_generator.rb +44 -0
  55. data/lib/pg_reports/modules/connections.rb +8 -73
  56. data/lib/pg_reports/modules/indexes.rb +9 -94
  57. data/lib/pg_reports/modules/queries.rb +9 -100
  58. data/lib/pg_reports/modules/schema_analysis.rb +154 -0
  59. data/lib/pg_reports/modules/system.rb +26 -61
  60. data/lib/pg_reports/modules/tables.rb +9 -96
  61. data/lib/pg_reports/query_monitor.rb +280 -0
  62. data/lib/pg_reports/report_definition.rb +161 -0
  63. data/lib/pg_reports/report_loader.rb +38 -0
  64. data/lib/pg_reports/sql/connections/connection_churn.sql +37 -0
  65. data/lib/pg_reports/sql/connections/pool_saturation.sql +90 -0
  66. data/lib/pg_reports/sql/connections/pool_usage.sql +31 -0
  67. data/lib/pg_reports/sql/connections/pool_wait_times.sql +19 -0
  68. data/lib/pg_reports/sql/queries/all_queries.sql +17 -15
  69. data/lib/pg_reports/sql/queries/expensive_queries.sql +9 -4
  70. data/lib/pg_reports/sql/queries/heavy_queries.sql +14 -12
  71. data/lib/pg_reports/sql/queries/low_cache_hit_queries.sql +16 -14
  72. data/lib/pg_reports/sql/queries/missing_index_queries.sql +18 -16
  73. data/lib/pg_reports/sql/queries/slow_queries.sql +14 -12
  74. data/lib/pg_reports/sql/schema_analysis/unique_indexes.sql +35 -0
  75. data/lib/pg_reports/sql/system/databases_list.sql +8 -0
  76. data/lib/pg_reports/version.rb +1 -1
  77. data/lib/pg_reports.rb +26 -0
  78. metadata +93 -3
@@ -91,6 +91,9 @@ pg_stat_statements.track = all</pre>
91
91
  <div class="live-monitoring-title">
92
92
  <span class="live-indicator"></span>
93
93
  <span>Live Monitoring</span>
94
+ <span class="database-badge">
95
+ Database: <strong><%= @current_database %></strong>
96
+ </span>
94
97
  </div>
95
98
  <div class="live-monitoring-controls">
96
99
  <span class="live-monitoring-interval">Updates every 5s</span>
@@ -202,6 +205,46 @@ pg_stat_statements.track = all</pre>
202
205
  </div>
203
206
  </div>
204
207
 
208
+ <!-- Query Monitoring Panel -->
209
+ <div id="query-monitoring" class="query-monitoring-panel" style="display: none;">
210
+ <div class="query-monitoring-header">
211
+ <div class="query-monitoring-title">
212
+ <span class="monitoring-indicator" id="monitor-indicator"></span>
213
+ <span>SQL Query Monitor</span>
214
+ <span class="session-badge" id="session-badge" style="display: none;">
215
+ Session: <strong id="session-id"></strong>
216
+ </span>
217
+ </div>
218
+ <div class="query-monitoring-controls">
219
+ <button class="btn btn-small btn-primary" onclick="startQueryMonitoring(this)" id="start-monitor-btn">
220
+ โ–ถ Start Monitoring
221
+ </button>
222
+ <button class="btn btn-small btn-danger" onclick="stopQueryMonitoring(this)" id="stop-monitor-btn" style="display: none;">
223
+ โน Stop Monitoring
224
+ </button>
225
+ <div class="download-dropdown" id="monitor-download-dropdown" style="display: none;">
226
+ <button class="btn btn-small btn-secondary" onclick="toggleMonitorDownloadMenu(event)">
227
+ ๐Ÿ“ฅ Download
228
+ </button>
229
+ <div class="download-menu" id="monitor-download-menu" style="display: none;">
230
+ <a href="#" onclick="downloadQueryMonitor('txt'); return false;">๐Ÿ“„ Text (.txt)</a>
231
+ <a href="#" onclick="downloadQueryMonitor('csv'); return false;">๐Ÿ“Š CSV (.csv)</a>
232
+ <a href="#" onclick="downloadQueryMonitor('json'); return false;">๐Ÿ“‹ JSON (.json)</a>
233
+ </div>
234
+ </div>
235
+ <span class="query-monitoring-count">
236
+ Queries: <strong id="query-count">0</strong>
237
+ </span>
238
+ </div>
239
+ </div>
240
+
241
+ <div class="query-feed" id="query-feed">
242
+ <div class="query-feed-empty">
243
+ Click "Start Monitoring" to begin capturing SQL queries
244
+ </div>
245
+ </div>
246
+ </div>
247
+
205
248
  <div class="categories-grid">
206
249
  <% @categories.each do |category_key, category| %>
207
250
  <div class="category-card<%= ' disabled' if category_key == :queries && !@pg_stat_status[:ready] %>">
@@ -514,6 +557,22 @@ pg_stat_statements.track = all</pre>
514
557
  font-size: 1rem;
515
558
  }
516
559
 
560
+ .database-badge {
561
+ margin-left: 0.5rem;
562
+ padding: 0.375rem 0.75rem;
563
+ background: var(--bg-tertiary);
564
+ border: 1px solid var(--border-color);
565
+ border-radius: 8px;
566
+ font-size: 0.75rem;
567
+ font-weight: 500;
568
+ color: var(--text-secondary);
569
+ }
570
+
571
+ .database-badge strong {
572
+ color: var(--text-primary);
573
+ font-weight: 600;
574
+ }
575
+
517
576
  .live-indicator {
518
577
  width: 8px;
519
578
  height: 8px;
@@ -1001,4 +1060,364 @@ pg_stat_statements.track = all</pre>
1001
1060
 
1002
1061
  document.addEventListener('DOMContentLoaded', initLiveMonitoring);
1003
1062
  window.addEventListener('beforeunload', stopPolling);
1063
+
1064
+ // ============================================
1065
+ // Query Monitoring
1066
+ // ============================================
1067
+
1068
+ const QUERY_MONITOR_CONFIG = {
1069
+ pollInterval: 2000,
1070
+ maxQueries: 50
1071
+ };
1072
+
1073
+ let queryMonitorEnabled = false;
1074
+ let queryMonitorPollTimer = null;
1075
+ let currentSessionId = null;
1076
+ let queryCount = 0;
1077
+
1078
+ function initQueryMonitoring() {
1079
+ const panel = document.getElementById('query-monitoring');
1080
+ if (!panel) return;
1081
+
1082
+ // Check initial status
1083
+ checkQueryMonitorStatus();
1084
+ }
1085
+
1086
+ async function checkQueryMonitorStatus() {
1087
+ try {
1088
+ const response = await fetch(`${pgReportsRoot}/query_monitor/status`);
1089
+ const data = await response.json();
1090
+
1091
+ if (data.success && data.enabled) {
1092
+ queryMonitorEnabled = true;
1093
+ currentSessionId = data.session_id;
1094
+ queryCount = data.query_count;
1095
+ updateQueryMonitorUI(true);
1096
+ startQueryMonitorPolling();
1097
+ } else {
1098
+ // Monitoring not active - load history from log file
1099
+ queryMonitorEnabled = false;
1100
+ await loadQueryHistory();
1101
+ updateQueryMonitorUI(false);
1102
+ }
1103
+ } catch (error) {
1104
+ console.error('Failed to check query monitor status:', error);
1105
+ // Show panel anyway even if status check fails
1106
+ updateQueryMonitorUI(false);
1107
+ }
1108
+ }
1109
+
1110
+ async function loadQueryHistory() {
1111
+ try {
1112
+ const response = await fetch(
1113
+ `${pgReportsRoot}/query_monitor/history?limit=${QUERY_MONITOR_CONFIG.maxQueries}`
1114
+ );
1115
+ const data = await response.json();
1116
+
1117
+ if (data.success && data.queries && data.queries.length > 0) {
1118
+ renderQueryFeed(data.queries);
1119
+ }
1120
+ } catch (error) {
1121
+ console.error('Failed to load query history:', error);
1122
+ }
1123
+ }
1124
+
1125
+ async function loadSessionFromLog(sessionId) {
1126
+ try {
1127
+ const response = await fetch(
1128
+ `${pgReportsRoot}/query_monitor/history?session_id=${sessionId}&limit=${QUERY_MONITOR_CONFIG.maxQueries}`
1129
+ );
1130
+ const data = await response.json();
1131
+
1132
+ if (data.success && data.queries && data.queries.length > 0) {
1133
+ renderQueryFeed(data.queries);
1134
+ }
1135
+ } catch (error) {
1136
+ console.error('Failed to load session from log:', error);
1137
+ }
1138
+ }
1139
+
1140
+ async function startQueryMonitoring(button) {
1141
+ button.disabled = true;
1142
+ button.innerHTML = '<span class="spinner"></span> Starting...';
1143
+
1144
+ try {
1145
+ const response = await fetch(`${pgReportsRoot}/query_monitor/start`, {
1146
+ method: 'POST',
1147
+ headers: {
1148
+ 'Content-Type': 'application/json',
1149
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
1150
+ }
1151
+ });
1152
+
1153
+ const data = await response.json();
1154
+
1155
+ if (data.success) {
1156
+ queryMonitorEnabled = true;
1157
+ currentSessionId = data.session_id;
1158
+ updateQueryMonitorUI(true);
1159
+ startQueryMonitorPolling();
1160
+ showToast(data.message);
1161
+ } else {
1162
+ showToast(data.message || 'Failed to start monitoring', 'error');
1163
+ button.disabled = false;
1164
+ button.innerHTML = 'โ–ถ Start Monitoring';
1165
+ }
1166
+ } catch (error) {
1167
+ showToast('Network error: ' + error.message, 'error');
1168
+ button.disabled = false;
1169
+ button.innerHTML = 'โ–ถ Start Monitoring';
1170
+ }
1171
+ }
1172
+
1173
+ async function stopQueryMonitoring(button) {
1174
+ button.disabled = true;
1175
+ button.innerHTML = '<span class="spinner"></span> Stopping...';
1176
+
1177
+ try {
1178
+ const response = await fetch(`${pgReportsRoot}/query_monitor/stop`, {
1179
+ method: 'POST',
1180
+ headers: {
1181
+ 'Content-Type': 'application/json',
1182
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
1183
+ }
1184
+ });
1185
+
1186
+ const data = await response.json();
1187
+
1188
+ if (data.success) {
1189
+ queryMonitorEnabled = false;
1190
+ const stoppedSessionId = data.session_id || currentSessionId;
1191
+ currentSessionId = null;
1192
+ stopQueryMonitorPolling();
1193
+
1194
+ // Load complete session data from log file
1195
+ if (stoppedSessionId) {
1196
+ await loadSessionFromLog(stoppedSessionId);
1197
+ }
1198
+
1199
+ updateQueryMonitorUI(false);
1200
+ showToast(data.message);
1201
+ } else {
1202
+ showToast(data.message || 'Failed to stop monitoring', 'error');
1203
+ button.disabled = false;
1204
+ button.innerHTML = 'โน Stop Monitoring';
1205
+ }
1206
+ } catch (error) {
1207
+ showToast('Network error: ' + error.message, 'error');
1208
+ button.disabled = false;
1209
+ button.innerHTML = 'โน Stop Monitoring';
1210
+ }
1211
+ }
1212
+
1213
+ function updateQueryMonitorUI(enabled) {
1214
+ const panel = document.getElementById('query-monitoring');
1215
+ const indicator = document.getElementById('monitor-indicator');
1216
+ const startBtn = document.getElementById('start-monitor-btn');
1217
+ const stopBtn = document.getElementById('stop-monitor-btn');
1218
+ const sessionBadge = document.getElementById('session-badge');
1219
+ const sessionIdEl = document.getElementById('session-id');
1220
+ const downloadDropdown = document.getElementById('monitor-download-dropdown');
1221
+ const feed = document.getElementById('query-feed');
1222
+
1223
+ panel.style.display = 'block';
1224
+
1225
+ if (enabled) {
1226
+ indicator.classList.add('active');
1227
+ startBtn.style.display = 'none';
1228
+ stopBtn.style.display = 'inline-block';
1229
+ stopBtn.disabled = false;
1230
+ stopBtn.innerHTML = 'โน Stop Monitoring';
1231
+ sessionBadge.style.display = 'inline-block';
1232
+ sessionIdEl.textContent = currentSessionId ? currentSessionId.substring(0, 8) : '';
1233
+ downloadDropdown.style.display = 'inline-block';
1234
+ } else {
1235
+ indicator.classList.remove('active');
1236
+ startBtn.style.display = 'inline-block';
1237
+ startBtn.disabled = false;
1238
+ startBtn.innerHTML = 'โ–ถ Start Monitoring';
1239
+ stopBtn.style.display = 'none';
1240
+ sessionBadge.style.display = 'none';
1241
+
1242
+ // Keep results visible after stopping, only hide download if no queries
1243
+ const hasQueries = queryCount > 0;
1244
+ downloadDropdown.style.display = hasQueries ? 'inline-block' : 'none';
1245
+
1246
+ // Only clear feed if no queries captured
1247
+ if (!hasQueries) {
1248
+ feed.innerHTML = '<div class="query-feed-empty">Click "Start Monitoring" to begin capturing SQL queries</div>';
1249
+ }
1250
+ }
1251
+ }
1252
+
1253
+ function toggleMonitorDownloadMenu(event) {
1254
+ event.stopPropagation();
1255
+ const menu = document.getElementById('monitor-download-menu');
1256
+ menu.style.display = menu.style.display === 'none' ? 'block' : 'none';
1257
+ }
1258
+
1259
+ function downloadQueryMonitor(format) {
1260
+ // Hide dropdown menu
1261
+ document.getElementById('monitor-download-menu').style.display = 'none';
1262
+
1263
+ // Build download URL
1264
+ const url = `${pgReportsRoot}/query_monitor/download?format=${format}`;
1265
+
1266
+ // Use hidden iframe to download without page reload
1267
+ let iframe = document.getElementById('download-iframe');
1268
+ if (!iframe) {
1269
+ iframe = document.createElement('iframe');
1270
+ iframe.id = 'download-iframe';
1271
+ iframe.style.display = 'none';
1272
+ document.body.appendChild(iframe);
1273
+ }
1274
+ iframe.src = url;
1275
+ }
1276
+
1277
+ // Close download menu when clicking outside
1278
+ document.addEventListener('click', function(event) {
1279
+ const menu = document.getElementById('monitor-download-menu');
1280
+ if (menu && menu.style.display === 'block') {
1281
+ const dropdown = document.getElementById('monitor-download-dropdown');
1282
+ if (dropdown && !dropdown.contains(event.target)) {
1283
+ menu.style.display = 'none';
1284
+ }
1285
+ }
1286
+ });
1287
+
1288
+ function startQueryMonitorPolling() {
1289
+ if (queryMonitorPollTimer) clearInterval(queryMonitorPollTimer);
1290
+
1291
+ fetchQueryFeed(); // Immediate fetch
1292
+ queryMonitorPollTimer = setInterval(fetchQueryFeed, QUERY_MONITOR_CONFIG.pollInterval);
1293
+ }
1294
+
1295
+ function stopQueryMonitorPolling() {
1296
+ if (queryMonitorPollTimer) {
1297
+ clearInterval(queryMonitorPollTimer);
1298
+ queryMonitorPollTimer = null;
1299
+ }
1300
+ }
1301
+
1302
+ async function fetchQueryFeed() {
1303
+ if (!queryMonitorEnabled) return;
1304
+
1305
+ try {
1306
+ const response = await fetch(
1307
+ `${pgReportsRoot}/query_monitor/feed?limit=${QUERY_MONITOR_CONFIG.maxQueries}&session_id=${currentSessionId || ''}`
1308
+ );
1309
+ const data = await response.json();
1310
+
1311
+ if (data.success && data.queries) {
1312
+ renderQueryFeed(data.queries);
1313
+ }
1314
+ } catch (error) {
1315
+ console.error('Failed to fetch query feed:', error);
1316
+ }
1317
+ }
1318
+
1319
+ function renderQueryFeed(queries) {
1320
+ const feed = document.getElementById('query-feed');
1321
+
1322
+ if (queries.length === 0) {
1323
+ feed.innerHTML = '<div class="query-feed-empty">No queries captured yet...</div>';
1324
+ queryCount = 0;
1325
+ document.getElementById('query-count').textContent = '0';
1326
+ return;
1327
+ }
1328
+
1329
+ queryCount = queries.length;
1330
+ document.getElementById('query-count').textContent = queryCount;
1331
+
1332
+ // Render queries in reverse chronological order
1333
+ const html = queries.slice().reverse().map(query => renderQueryItem(query)).join('');
1334
+ feed.innerHTML = html;
1335
+ }
1336
+
1337
+ function renderQueryItem(query) {
1338
+ const duration = query.duration_ms;
1339
+ const durationClass = duration < 10 ? 'fast' : duration > 100 ? 'slow' : '';
1340
+ const timestamp = new Date(query.timestamp).toLocaleTimeString();
1341
+ const queryId = `query-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
1342
+
1343
+ let sourceInfo = 'Unknown source';
1344
+ if (query.source_location && query.source_location.file) {
1345
+ const file = query.source_location.file;
1346
+ const line = query.source_location.line;
1347
+ sourceInfo = `<a href="#" onclick="openInIDE('${escapeHtml(file)}', ${line}); return false;">
1348
+ ${escapeHtml(file)}:${line}
1349
+ </a>`;
1350
+ }
1351
+
1352
+ // Prepare SQL: collapse to single line and truncate
1353
+ const fullSql = query.sql.trim();
1354
+ const collapsedSql = fullSql.replace(/\s+/g, ' ').trim();
1355
+ const truncatedSql = collapsedSql.length > 100
1356
+ ? collapsedSql.substring(0, 100) + '...'
1357
+ : collapsedSql;
1358
+
1359
+ return `
1360
+ <div class="query-item">
1361
+ <div class="query-header">
1362
+ <div class="query-meta">
1363
+ <span class="query-timestamp">${timestamp}</span>
1364
+ <span class="query-duration ${durationClass}">${duration.toFixed(2)}ms</span>
1365
+ <span class="query-source">${sourceInfo}</span>
1366
+ </div>
1367
+ <button class="query-expand-btn" onclick="toggleQueryExpand('${queryId}')" title="Expand/Collapse">
1368
+ <span id="${queryId}-icon">โ–ผ</span>
1369
+ </button>
1370
+ </div>
1371
+ <div class="query-sql-wrapper">
1372
+ <div class="query-sql query-sql-collapsed" id="${queryId}-sql">
1373
+ ${escapeHtml(truncatedSql)}
1374
+ </div>
1375
+ <div class="query-sql query-sql-expanded" id="${queryId}-sql-full" style="display: none;">
1376
+ ${escapeHtml(fullSql)}
1377
+ </div>
1378
+ </div>
1379
+ </div>
1380
+ `;
1381
+ }
1382
+
1383
+ function toggleQueryExpand(queryId) {
1384
+ const collapsedEl = document.getElementById(`${queryId}-sql`);
1385
+ const expandedEl = document.getElementById(`${queryId}-sql-full`);
1386
+ const iconEl = document.getElementById(`${queryId}-icon`);
1387
+
1388
+ if (collapsedEl.style.display === 'none') {
1389
+ // Currently expanded, collapse it
1390
+ collapsedEl.style.display = 'block';
1391
+ expandedEl.style.display = 'none';
1392
+ iconEl.textContent = 'โ–ผ';
1393
+ } else {
1394
+ // Currently collapsed, expand it
1395
+ collapsedEl.style.display = 'none';
1396
+ expandedEl.style.display = 'block';
1397
+ iconEl.textContent = 'โ–ฒ';
1398
+ }
1399
+ }
1400
+
1401
+ function escapeHtml(text) {
1402
+ if (!text) return '';
1403
+ const div = document.createElement('div');
1404
+ div.textContent = text;
1405
+ return div.innerHTML;
1406
+ }
1407
+
1408
+ function showToast(message, type = 'success') {
1409
+ // Simple console log for now - can be enhanced with actual toast UI
1410
+ if (type === 'error') {
1411
+ console.error(message);
1412
+ } else {
1413
+ console.log(message);
1414
+ }
1415
+ // TODO: Implement actual toast notification UI
1416
+ }
1417
+
1418
+ // Initialize on page load
1419
+ document.addEventListener('DOMContentLoaded', initQueryMonitoring);
1420
+ window.addEventListener('beforeunload', () => {
1421
+ stopQueryMonitorPolling();
1422
+ });
1004
1423
  </script>
@@ -50,59 +50,101 @@
50
50
  </div>
51
51
  <% end %>
52
52
 
53
- <!-- Collapsible Documentation Section -->
54
- <% if @documentation && @documentation[:what].present? %>
55
- <details class="documentation-section">
56
- <summary class="documentation-toggle">
57
- <span class="toggle-icon">โ–ถ</span>
58
- <span>๐Ÿ“– What does this report show?</span>
59
- </summary>
60
- <div class="documentation-content">
61
- <% if @documentation[:what].present? %>
62
- <div class="doc-block">
63
- <h4>๐Ÿ“‹ What</h4>
64
- <p><%= @documentation[:what] %></p>
65
- </div>
66
- <% end %>
53
+ <div class="report-details-grid">
54
+ <!-- Collapsible Documentation Section -->
55
+ <% if @documentation && @documentation[:what].present? %>
56
+ <details class="documentation-section">
57
+ <summary class="documentation-toggle">
58
+ <span class="toggle-icon">โ–ถ</span>
59
+ <span>๐Ÿ“– What does this report show?</span>
60
+ </summary>
61
+ <div class="documentation-content">
62
+ <% if @documentation[:what].present? %>
63
+ <div class="doc-block">
64
+ <h4>๐Ÿ“‹ What</h4>
65
+ <p><%= @documentation[:what] %></p>
66
+ </div>
67
+ <% end %>
67
68
 
68
- <% if @documentation[:why].present? %>
69
- <div class="doc-block">
70
- <h4>โ“ Why It Matters</h4>
71
- <p><%= @documentation[:why] %></p>
72
- </div>
73
- <% end %>
74
-
75
- <% if @documentation[:nuances].present? %>
76
- <div class="doc-block">
77
- <h4>โš ๏ธ Nuances</h4>
78
- <ul class="nuances-list">
79
- <% @documentation[:nuances].each do |nuance| %>
80
- <li><%= nuance %></li>
81
- <% end %>
82
- </ul>
83
- </div>
84
- <% end %>
85
-
86
- <% if @thresholds.present? %>
87
- <div class="thresholds-block">
88
- <h4>๐Ÿ“Š Thresholds</h4>
89
- <div class="thresholds-grid">
90
- <% @thresholds.each do |field, values| %>
91
- <div class="threshold-item">
92
- <span class="threshold-field"><%= field %></span>
93
- <span class="threshold-warning">โš ๏ธ Warning: <%= values[:warning] %></span>
94
- <span class="threshold-critical">๐Ÿ”ด Critical: <%= values[:critical] %></span>
95
- <% if values[:inverted] %>
96
- <span class="threshold-note">(lower is worse)</span>
97
- <% end %>
69
+ <% if @documentation[:why].present? %>
70
+ <div class="doc-block">
71
+ <h4>โ“ Why It Matters</h4>
72
+ <p><%= @documentation[:why] %></p>
73
+ </div>
74
+ <% end %>
75
+
76
+ <% if @documentation[:nuances].present? %>
77
+ <div class="doc-block">
78
+ <h4>โš ๏ธ Nuances</h4>
79
+ <ul class="nuances-list">
80
+ <% @documentation[:nuances].each do |nuance| %>
81
+ <li><%= nuance %></li>
82
+ <% end %>
83
+ </ul>
84
+ </div>
85
+ <% end %>
86
+
87
+ <% if @thresholds.present? %>
88
+ <div class="thresholds-block">
89
+ <h4>๐Ÿ“Š Thresholds</h4>
90
+ <div class="thresholds-grid">
91
+ <% @thresholds.each do |field, values| %>
92
+ <div class="threshold-item">
93
+ <span class="threshold-field"><%= field %></span>
94
+ <span class="threshold-warning">โš ๏ธ Warning: <%= values[:warning] %></span>
95
+ <span class="threshold-critical">๐Ÿ”ด Critical: <%= values[:critical] %></span>
96
+ <% if values[:inverted] %>
97
+ <span class="threshold-note">(lower is worse)</span>
98
+ <% end %>
99
+ </div>
100
+ <% end %>
101
+ </div>
102
+ </div>
103
+ <% end %>
104
+ </div>
105
+ </details>
106
+ <% end %>
107
+
108
+ <!-- Filter Parameters Section -->
109
+ <% if @report_filters.present? %>
110
+ <div class="filter-section">
111
+ <details class="filter-details">
112
+ <summary class="filter-toggle">
113
+ <span class="toggle-icon">โ–ถ</span>
114
+ <span>๐Ÿ” ะŸะฐั€ะฐะผะตั‚ั€ั‹ ั„ะธะปัŒั‚ั€ะฐั†ะธะธ</span>
115
+ </summary>
116
+ <div class="filter-content">
117
+ <div class="filter-grid">
118
+ <% @report_filters.each do |name, config| %>
119
+ <div class="filter-item">
120
+ <label class="filter-label">
121
+ <%= config[:label] %>
122
+ <% if config[:description] %>
123
+ <span class="filter-description"><%= config[:description] %></span>
124
+ <% end %>
125
+ <% if config[:is_threshold] && config[:current_config] %>
126
+ <span class="filter-current-value">(current: <%= config[:current_config] %>)</span>
127
+ <% end %>
128
+ </label>
129
+ <input
130
+ type="<%= config[:type] == 'integer' ? 'number' : 'text' %>"
131
+ class="filter-input"
132
+ data-param="<%= name %>"
133
+ value="<%= config[:default] %>"
134
+ <% if config[:type] == 'integer' %>
135
+ min="0"
136
+ step="1"
137
+ <% end %>
138
+ placeholder="<%= config[:default] %>"
139
+ >
98
140
  </div>
99
141
  <% end %>
100
142
  </div>
101
143
  </div>
102
- <% end %>
144
+ </details>
103
145
  </div>
104
- </details>
105
- <% end %>
146
+ <% end %>
147
+ </div>
106
148
 
107
149
  <!-- Saved Records Section -->
108
150
  <div class="saved-records-section" id="saved-records-section" style="display: none;">
@@ -249,6 +249,46 @@ en:
249
249
  - "Too many idle = application not closing connections or pool is too large."
250
250
  - "idle_session_timeout (PostgreSQL 14+) can automatically close idle connections."
251
251
 
252
+ pool_usage:
253
+ title: "Connection Pool Usage"
254
+ what: "Current connection pool utilization showing active, idle, and available connections across databases."
255
+ how: "Analyzes pg_stat_activity connection states and compares against max_connections limit."
256
+ nuances:
257
+ - "Utilization above 70% indicates approaching pool limits โ€” consider scaling."
258
+ - "Idle in transaction connections waste resources and block VACUUM."
259
+ - "max_connections is database-wide, not per-database."
260
+ - "Connection poolers (PgBouncer/pgpool) allow many more application connections than database connections."
261
+
262
+ pool_wait_times:
263
+ title: "Pool Wait Time Analysis"
264
+ what: "Queries currently waiting for resources like locks, I/O, or network operations."
265
+ how: "Analyzes wait_event and wait_event_type from pg_stat_activity for non-idle connections."
266
+ nuances:
267
+ - "ClientRead waits = slow client not consuming data fast enough."
268
+ - "Lock waits = contention between concurrent queries."
269
+ - "IO waits = disk performance issues or insufficient cache."
270
+ - "Waits over 60 seconds are critical and need immediate investigation."
271
+
272
+ pool_saturation:
273
+ title: "Pool Saturation Warnings"
274
+ what: "Overall connection pool health metrics with saturation warnings and recommendations."
275
+ how: "Calculates utilization percentages for total, active, idle, and problematic connections."
276
+ nuances:
277
+ - "Consistent utilization above 70% = need for pool tuning or scaling."
278
+ - "High idle in transaction count = application transaction handling issues."
279
+ - "superuser_reserved_connections reduce available pool capacity."
280
+ - "Monitor trends โ€” sudden spikes may indicate connection leaks."
281
+
282
+ connection_churn:
283
+ title: "Connection Churn Analysis"
284
+ what: "Analyzes connection lifecycle patterns to identify excessive connection churn (frequent connect/disconnect)."
285
+ how: "Examines connection ages to identify short-lived connections and calculate churn rates per application."
286
+ nuances:
287
+ - "Connections under 10 seconds = short-lived."
288
+ - "Churn rate over 50% = missing/misconfigured connection pooling."
289
+ - "Many short connections = increased CPU and authentication overhead."
290
+ - "Web apps should maintain connection pools, not create per-request connections."
291
+
252
292
  # === SYSTEM ===
253
293
  database_sizes:
254
294
  title: "Database Sizes"
@@ -295,6 +335,19 @@ en:
295
335
  - "Target value: >99% for OLTP, >95% for mixed workload."
296
336
  - "Low cache hit: increase shared_buffers (up to 25% RAM), or the problem is in queries."
297
337
 
338
+ # === SCHEMA ANALYSIS ===
339
+ missing_validations:
340
+ title: "Missing Validations"
341
+ what: "Unique indexes in the database without corresponding uniqueness validations in Rails models."
342
+ how: "Analyzes all unique indexes (including composite ones) and checks if the corresponding Rails model has validates :column, uniqueness: true validation."
343
+ nuances:
344
+ - "Database constraints (indexes) and model validations serve different purposes โ€” both should be present."
345
+ - "Unique index prevents duplicates at database level, validation provides user-friendly error messages."
346
+ - "For composite indexes (a, b), need validation with scope: validates :a, uniqueness: { scope: :b }"
347
+ - "Some tables may not have models โ€” this is normal for join tables or legacy tables."
348
+ - "Validations in concerns and parent models are also detected."
349
+ - "Primary keys don't need validations โ€” they are automatically unique."
350
+
298
351
  # Problem explanations for highlighting
299
352
  problems:
300
353
  high_mean_time: "High mean execution time. Consider adding indexes or optimizing the query."
@@ -308,3 +361,8 @@ en:
308
361
  long_running: "Long-running query. May block other operations."
309
362
  blocking: "Blocking other queries. Requires attention."
310
363
  idle_in_transaction: "Open transaction without activity. Blocks VACUUM and holds locks."
364
+ high_pool_usage: "Connection pool utilization is high. Scale up max_connections or implement connection pooling."
365
+ long_wait_time: "Query waiting for resources for too long. Check for lock contention or I/O issues."
366
+ pool_saturation: "Connection pool is saturated. Risk of connection exhaustion and application errors."
367
+ high_connection_churn: "High connection churn rate. Implement connection pooling to reduce overhead."
368
+ too_many_short_connections: "Too many short-lived connections. Application should reuse connections via pooling."