pg_insights 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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +183 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/javascripts/pg_insights/application.js +436 -0
  6. data/app/assets/javascripts/pg_insights/health.js +104 -0
  7. data/app/assets/javascripts/pg_insights/results/chart_renderer.js +126 -0
  8. data/app/assets/javascripts/pg_insights/results/table_manager.js +378 -0
  9. data/app/assets/javascripts/pg_insights/results/view_toggles.js +25 -0
  10. data/app/assets/javascripts/pg_insights/results.js +13 -0
  11. data/app/assets/stylesheets/pg_insights/application.css +750 -0
  12. data/app/assets/stylesheets/pg_insights/health.css +501 -0
  13. data/app/assets/stylesheets/pg_insights/results.css +682 -0
  14. data/app/controllers/pg_insights/application_controller.rb +4 -0
  15. data/app/controllers/pg_insights/health_controller.rb +110 -0
  16. data/app/controllers/pg_insights/insights_controller.rb +77 -0
  17. data/app/controllers/pg_insights/queries_controller.rb +44 -0
  18. data/app/helpers/pg_insights/application_helper.rb +4 -0
  19. data/app/helpers/pg_insights/insights_helper.rb +190 -0
  20. data/app/jobs/pg_insights/application_job.rb +4 -0
  21. data/app/jobs/pg_insights/health_check_job.rb +45 -0
  22. data/app/jobs/pg_insights/health_check_scheduler_job.rb +52 -0
  23. data/app/jobs/pg_insights/recurring_health_checks_job.rb +49 -0
  24. data/app/models/pg_insights/application_record.rb +5 -0
  25. data/app/models/pg_insights/health_check_result.rb +46 -0
  26. data/app/models/pg_insights/query.rb +10 -0
  27. data/app/services/pg_insights/health_check_service.rb +298 -0
  28. data/app/services/pg_insights/insight_query_service.rb +21 -0
  29. data/app/views/layouts/pg_insights/application.html.erb +58 -0
  30. data/app/views/pg_insights/health/index.html.erb +324 -0
  31. data/app/views/pg_insights/insights/_chart_view.html.erb +25 -0
  32. data/app/views/pg_insights/insights/_column_panel.html.erb +18 -0
  33. data/app/views/pg_insights/insights/_query_examples.html.erb +32 -0
  34. data/app/views/pg_insights/insights/_query_panel.html.erb +36 -0
  35. data/app/views/pg_insights/insights/_result.html.erb +15 -0
  36. data/app/views/pg_insights/insights/_results_info.html.erb +19 -0
  37. data/app/views/pg_insights/insights/_results_panel.html.erb +13 -0
  38. data/app/views/pg_insights/insights/_results_table.html.erb +45 -0
  39. data/app/views/pg_insights/insights/_stats_view.html.erb +3 -0
  40. data/app/views/pg_insights/insights/_table_controls.html.erb +21 -0
  41. data/app/views/pg_insights/insights/_table_view.html.erb +5 -0
  42. data/app/views/pg_insights/insights/index.html.erb +5 -0
  43. data/config/default_queries.yml +85 -0
  44. data/config/routes.rb +22 -0
  45. data/lib/generators/pg_insights/clean_generator.rb +74 -0
  46. data/lib/generators/pg_insights/install_generator.rb +176 -0
  47. data/lib/pg_insights/engine.rb +40 -0
  48. data/lib/pg_insights/version.rb +3 -0
  49. data/lib/pg_insights.rb +83 -0
  50. data/lib/tasks/pg_insights.rake +172 -0
  51. metadata +124 -0
@@ -0,0 +1,324 @@
1
+ <div class="health-overview-section">
2
+ <div class="page-header">
3
+ <h1>Database Health Overview</h1>
4
+ <p>Monitor your PostgreSQL database performance and identify potential issues</p>
5
+ </div>
6
+
7
+ <div class="health-stats-grid">
8
+ <a href="#parameter-settings" class="stat-card-link">
9
+ <div class="stat-card">
10
+ <div class="stat-icon param-icon">⚙️</div>
11
+ <div class="stat-content">
12
+ <div class="stat-number"><%= (@parameter_settings.is_a?(Array) ? @parameter_settings.count : 0) %></div>
13
+ <div class="stat-label">Config Parameters</div>
14
+ </div>
15
+ </div>
16
+ </a>
17
+
18
+ <a href="#unused-indexes" class="stat-card-link">
19
+ <div class="stat-card">
20
+ <div class="stat-icon unused-icon">🗑️</div>
21
+ <div class="stat-content">
22
+ <div class="stat-number"><%= (@unused_indexes.is_a?(Array) ? @unused_indexes.count : 0) %></div>
23
+ <div class="stat-label">Unused Indexes</div>
24
+ </div>
25
+ </div>
26
+ </a>
27
+
28
+ <a href="#slow-queries" class="stat-card-link">
29
+ <div class="stat-card">
30
+ <div class="stat-icon slow-icon">🐌</div>
31
+ <div class="stat-content">
32
+ <div class="stat-number"><%= (@slow_queries.is_a?(Array) ? @slow_queries.count : 0) %></div>
33
+ <div class="stat-label">Slow Queries</div>
34
+ </div>
35
+ </div>
36
+ </a>
37
+
38
+ <a href="#table-bloat" class="stat-card-link">
39
+ <div class="stat-card">
40
+ <div class="stat-icon bloat-icon">💾</div>
41
+ <div class="stat-content">
42
+ <div class="stat-number"><%= (@table_bloat.is_a?(Array) ? @table_bloat.count : 0) %></div>
43
+ <div class="stat-label">Bloated Tables</div>
44
+ </div>
45
+ </div>
46
+ </a>
47
+ </div>
48
+ </div>
49
+
50
+ <div class="health-details">
51
+
52
+ <div class="health-section" id="parameter-settings">
53
+ <div class="section-header">
54
+ <h2>⚙️ Parameter Settings</h2>
55
+ <span class="section-description">PostgreSQL configuration parameters and recommendations</span>
56
+ </div>
57
+
58
+ <div class="section-content">
59
+ <% if @parameter_settings.is_a?(Hash) && @parameter_settings[:error] %>
60
+ <div class="error-message">
61
+ <i class="fas fa-exclamation-triangle"></i>
62
+ <%= @parameter_settings[:error] %>
63
+ </div>
64
+ <% elsif @parameter_settings.nil? || @parameter_settings.empty? %>
65
+ <div class="info-message">
66
+ <i class="fas fa-info-circle"></i>
67
+ No parameter settings to review at this time.
68
+ </div>
69
+ <% else %>
70
+ <div class="items-list">
71
+ <% @parameter_settings.each do |param| %>
72
+ <div class="item-row">
73
+ <div class="item-main">
74
+ <div class="item-title"><%= param['name'] %></div>
75
+ <div class="item-subtitle"><%= param['short_desc'] %></div>
76
+ </div>
77
+ <div class="item-details">
78
+ <div class="detail-item">
79
+ <span class="detail-label">Current Value:</span>
80
+ <span class="detail-value">
81
+ <code><%= param['setting'] %><%= param['unit'] %></code>
82
+ </span>
83
+ </div>
84
+ <% if param['recommendation'] %>
85
+ <div class="detail-item recommendation">
86
+ <span class="detail-label">Recommendation:</span>
87
+ <span class="detail-value"><%= param['recommendation'] %></span>
88
+ </div>
89
+ <% end %>
90
+ </div>
91
+ </div>
92
+ <% end %>
93
+ </div>
94
+ <% end %>
95
+ </div>
96
+ </div>
97
+
98
+ <div class="health-section" id="unused-indexes">
99
+ <div class="section-header">
100
+ <h2>🗑️ Unused Indexes</h2>
101
+ <span class="section-description">Indexes that are rarely or never used and can be removed to save space</span>
102
+ </div>
103
+
104
+ <div class="section-content">
105
+ <% if @unused_indexes.is_a?(Hash) && @unused_indexes[:error] %>
106
+ <div class="error-message">
107
+ <i class="fas fa-exclamation-triangle"></i>
108
+ <%= @unused_indexes[:error] %>
109
+ </div>
110
+ <% elsif @unused_indexes.nil? || @unused_indexes.empty? %>
111
+ <div class="success-message">
112
+ <i class="fas fa-check-circle"></i>
113
+ No unused indexes found - excellent database maintenance!
114
+ </div>
115
+ <% else %>
116
+ <div class="items-list">
117
+ <% @unused_indexes.each do |index| %>
118
+ <div class="item-row">
119
+ <div class="item-main">
120
+ <div class="item-title"><%= index['index'] %></div>
121
+ <div class="item-subtitle">Table: <%= index['table'] %></div>
122
+ </div>
123
+ <div class="item-details">
124
+ <div class="detail-item">
125
+ <span class="detail-label">Size:</span>
126
+ <span class="detail-value"><%= index['index_size'] %></span>
127
+ </div>
128
+ <div class="detail-item">
129
+ <span class="detail-label">Scans:</span>
130
+ <span class="detail-value"><%= index['index_scans'] %></span>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ <% end %>
135
+ </div>
136
+ <% end %>
137
+ </div>
138
+ </div>
139
+
140
+ <div class="health-section" id="missing-indexes">
141
+ <div class="section-header">
142
+ <h2>⚠️ Potentially Missing Indexes</h2>
143
+ <span class="section-description">Tables with high sequential scan ratios that might benefit from indexes</span>
144
+ </div>
145
+
146
+ <div class="section-content">
147
+ <% if @missing_indexes.is_a?(Hash) && @missing_indexes[:error] %>
148
+ <div class="error-message">
149
+ <i class="fas fa-exclamation-triangle"></i>
150
+ <%= @missing_indexes[:error] %>
151
+ </div>
152
+ <% elsif @missing_indexes.nil? || @missing_indexes.empty? %>
153
+ <div class="success-message">
154
+ <i class="fas fa-check-circle"></i>
155
+ No tables with excessive sequential scans found - good indexing strategy!
156
+ </div>
157
+ <% else %>
158
+ <div class="items-list">
159
+ <% @missing_indexes.each do |table| %>
160
+ <div class="item-row">
161
+ <div class="item-main">
162
+ <div class="item-title"><%= table['table'] %></div>
163
+ <div class="item-subtitle">Size: <%= table['table_size'] %></div>
164
+ </div>
165
+ <div class="item-details">
166
+ <div class="detail-item">
167
+ <span class="detail-label">Sequential Scans:</span>
168
+ <span class="detail-value warning"><%= table['seq_scan'] %></span>
169
+ </div>
170
+ <div class="detail-item">
171
+ <span class="detail-label">Index Scans:</span>
172
+ <span class="detail-value"><%= table['idx_scan'] %></span>
173
+ </div>
174
+ </div>
175
+ </div>
176
+ <% end %>
177
+ </div>
178
+ <% end %>
179
+ </div>
180
+ </div>
181
+
182
+ <div class="health-section" id="slow-queries">
183
+ <div class="section-header">
184
+ <h2>🐌 Slow Queries</h2>
185
+ <span class="section-description">Queries with the highest execution times that need optimization</span>
186
+ </div>
187
+
188
+ <div class="section-content">
189
+ <% if @slow_queries.is_a?(Hash) && @slow_queries[:error] %>
190
+ <div class="error-message">
191
+ <i class="fas fa-exclamation-triangle"></i>
192
+ <strong>Note:</strong> This feature requires the <code>pg_stat_statements</code> extension.
193
+ <br><%= @slow_queries[:error] %>
194
+ </div>
195
+ <% elsif @slow_queries.nil? || @slow_queries.empty? %>
196
+ <div class="success-message">
197
+ <i class="fas fa-check-circle"></i>
198
+ No slow queries detected - excellent query performance!
199
+ </div>
200
+ <% else %>
201
+ <div class="items-list">
202
+ <% @slow_queries.each do |query| %>
203
+ <div class="item-row query-item">
204
+ <div class="item-main">
205
+ <div class="item-title">
206
+ <code class="query-text"><%= truncate(query['query'], length: 80) %></code>
207
+ </div>
208
+ </div>
209
+ <div class="item-details">
210
+ <div class="detail-item">
211
+ <span class="detail-label">Avg Time:</span>
212
+ <span class="detail-value warning"><%= number_with_precision(query['mean_exec_time'], precision: 2) %>ms</span>
213
+ </div>
214
+ <div class="detail-item">
215
+ <span class="detail-label">Total Time:</span>
216
+ <span class="detail-value"><%= number_with_precision(query['total_exec_time'], precision: 2) %>ms</span>
217
+ </div>
218
+ <div class="detail-item">
219
+ <span class="detail-label">Calls:</span>
220
+ <span class="detail-value"><%= number_with_delimiter(query['calls']) %></span>
221
+ </div>
222
+ <div class="detail-item">
223
+ <span class="detail-label">Rows:</span>
224
+ <span class="detail-value"><%= number_with_delimiter(query['rows']) %></span>
225
+ </div>
226
+ </div>
227
+ </div>
228
+ <% end %>
229
+ </div>
230
+ <% end %>
231
+ </div>
232
+ </div>
233
+
234
+ <div class="health-section" id="sequential-scans">
235
+ <div class="section-header">
236
+ <h2>🔍 High Sequential Scans</h2>
237
+ <span class="section-description">Tables with high sequential scan activity</span>
238
+ </div>
239
+
240
+ <div class="section-content">
241
+ <% if @sequential_scans.is_a?(Hash) && @sequential_scans[:error] %>
242
+ <div class="error-message">
243
+ <i class="fas fa-exclamation-triangle"></i>
244
+ <%= @sequential_scans[:error] %>
245
+ </div>
246
+ <% elsif @sequential_scans.nil? || @sequential_scans.empty? %>
247
+ <div class="success-message">
248
+ <i class="fas fa-check-circle"></i>
249
+ No tables with excessive sequential scans found.
250
+ </div>
251
+ <% else %>
252
+ <div class="items-list">
253
+ <% @sequential_scans.each do |scan| %>
254
+ <div class="item-row">
255
+ <div class="item-main">
256
+ <div class="item-title"><%= scan['table'] %></div>
257
+ <div class="item-subtitle">Size: <%= scan['table_size'] %></div>
258
+ </div>
259
+ <div class="item-details">
260
+ <div class="detail-item">
261
+ <span class="detail-label">Sequential Scans:</span>
262
+ <span class="detail-value warning"><%= scan['seq_scan'] %></span>
263
+ </div>
264
+ <div class="detail-item">
265
+ <span class="detail-label">Rows Read:</span>
266
+ <span class="detail-value"><%= number_with_delimiter(scan['seq_tup_read']) %></span>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ <% end %>
271
+ </div>
272
+ <% end %>
273
+ </div>
274
+ </div>
275
+
276
+ <div class="health-section" id="table-bloat">
277
+ <div class="section-header">
278
+ <h2>💾 Table Bloat</h2>
279
+ <span class="section-description">Tables with significant bloat that may need maintenance</span>
280
+ </div>
281
+
282
+ <div class="section-content">
283
+ <% if @table_bloat.is_a?(Hash) && @table_bloat[:error] %>
284
+ <div class="error-message">
285
+ <i class="fas fa-exclamation-triangle"></i>
286
+ <%= @table_bloat[:error] %>
287
+ </div>
288
+ <% elsif @table_bloat.nil? || @table_bloat.empty? %>
289
+ <div class="success-message">
290
+ <i class="fas fa-check-circle"></i>
291
+ No significant table bloat detected.
292
+ </div>
293
+ <% else %>
294
+ <div class="items-list">
295
+ <% @table_bloat.each do |bloat| %>
296
+ <div class="item-row">
297
+ <div class="item-main">
298
+ <div class="item-title"><%= bloat['table_name'] %></div>
299
+ <div class="item-subtitle">Size: <%= bloat['table_mb'] %> MB</div>
300
+ </div>
301
+ <div class="item-details">
302
+ <div class="detail-item">
303
+ <span class="detail-label">Bloat Percentage:</span>
304
+ <span class="detail-value <%= bloat['bloat_pct'].to_f > 50 ? 'danger' : 'warning' %>">
305
+ <%= bloat['bloat_pct'] %>%
306
+ </span>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ <% end %>
311
+ </div>
312
+ <% end %>
313
+ </div>
314
+ </div>
315
+
316
+ </div>
317
+
318
+ <div class="health-footer">
319
+ <div class="footer-text">
320
+ <i class="fas fa-clock"></i>
321
+ Last updated: <%= Time.current.strftime("%B %d, %Y at %H:%M:%S %Z") %>
322
+ </div>
323
+ </div>
324
+
@@ -0,0 +1,25 @@
1
+ <% if should_show_chart?(@result) %>
2
+ <div class="chart-container" id="chart-view" style="display: none;" data-chart-data="<%= prepare_chart_data(@result).to_json %>">
3
+ <div class="chart-controls">
4
+ <label class="chart-label">Chart Type:</label>
5
+ <select id="chartType" class="chart-select">
6
+ <option value="bar">Bar Chart</option>
7
+ <option value="line">Line Chart</option>
8
+ <option value="pie">Pie Chart</option>
9
+ <option value="area">Area Chart</option>
10
+ </select>
11
+ </div>
12
+
13
+ <div id="dynamicChart" class="chart-display">
14
+ <%= render_chart(@result) %>
15
+ </div>
16
+ </div>
17
+ <% else %>
18
+ <div class="chart-container" id="chart-view" style="display: none;">
19
+ <div class="no-chart-message">
20
+ <span class="chart-icon">📊</span>
21
+ <h3>Charts not available</h3>
22
+ <p>Charts work best with 2-3 columns containing numeric data</p>
23
+ </div>
24
+ </div>
25
+ <% end %>
@@ -0,0 +1,18 @@
1
+ <div id="columnPanel" class="column-panel" style="display: none;">
2
+ <div class="column-panel-header">
3
+ <h4>Show/Hide Columns</h4>
4
+ <div class="column-panel-actions">
5
+ <button type="button" id="showAllColumns" class="panel-btn">Show All</button>
6
+ <button type="button" id="hideAllColumns" class="panel-btn">Hide All</button>
7
+ </div>
8
+ </div>
9
+
10
+ <div class="column-checkboxes">
11
+ <% @result.columns.each_with_index do |column, index| %>
12
+ <div class="column-checkbox">
13
+ <input type="checkbox" id="col-<%= index + 1 %>" class="column-toggle" data-column="<%= index + 1 %>" checked>
14
+ <label for="col-<%= index + 1 %>" class="checkbox-label"><%= column %></label>
15
+ </div>
16
+ <% end %>
17
+ </div>
18
+ </div>
@@ -0,0 +1,32 @@
1
+ <div class="query-examples-section">
2
+ <div class="examples-header">
3
+ <span class="examples-label">Query Examples</span>
4
+ <div class="category-filters">
5
+ <% filter_buttons_data.each do |filter| %>
6
+ <button type="button"
7
+ class="filter-btn <%= 'active' if filter[:active] %>"
8
+ data-category="<%= filter[:category] %>">
9
+ <%= filter[:label] %>
10
+ </button>
11
+ <% end %>
12
+ </div>
13
+ </div>
14
+
15
+ <div class="query-examples-grid">
16
+ <% @insight_queries.each do |query| %>
17
+ <button type="button"
18
+ class="query-example-btn"
19
+ data-query-id="<%= query[:id] %>"
20
+ data-category="<%= query[:category] %>"
21
+ title="<%= query[:description] %>">
22
+ <div class="query-btn-content">
23
+ <span class="query-name"><%= query[:name] %></span>
24
+ <span class="query-desc"><%= query[:description] %></span>
25
+ <span class="<%= query_category_badge_class(query[:category]) %>">
26
+ <%= query[:category].to_s.titleize %>
27
+ </span>
28
+ </div>
29
+ </button>
30
+ <% end %>
31
+ </div>
32
+ </div>
@@ -0,0 +1,36 @@
1
+ <div class="query-panel">
2
+ <div class="query-section">
3
+ <%= form_with url: pg_insights.root_path, method: :post, local: true, class: "query-form" do |f| %>
4
+ <div class="form-header">
5
+ <div class="preview-tables-dropdown">
6
+ <select id="table-preview-select" class="table-select">
7
+ <option value="">📊 Preview Tables</option>
8
+ </select>
9
+ </div>
10
+ <div class="header-actions">
11
+ <button type="button" class="btn-icon btn-copy" onclick="copyCurrentQuery()" title="Copy query to clipboard">📋</button>
12
+ <button type="button" class="btn-icon btn-save" onclick="saveCurrentQuery()" title="Save query">💾</button>
13
+ </div>
14
+ </div>
15
+
16
+ <div class="form-content">
17
+ <%= f.text_area :sql,
18
+ rows: 12,
19
+ class: "sql-editor",
20
+ placeholder: sql_placeholder_text,
21
+ value: params[:sql] %>
22
+ </div>
23
+
24
+ <div class="form-actions">
25
+ <%= f.submit "Execute", class: "btn btn-primary", id: "execute-btn" %>
26
+ <button type="button" class="btn btn-secondary" onclick="clearQuery()">Clear</button>
27
+ </div>
28
+
29
+ <%= render "query_examples" %>
30
+
31
+ <div class="query-info">
32
+ <small>SELECT only • 5s timeout • 1k row limit</small>
33
+ </div>
34
+ <% end %>
35
+ </div>
36
+ </div>
@@ -0,0 +1,15 @@
1
+ <% if @result.present? %>
2
+ <div class="results-section">
3
+ <%= render "results_info" %>
4
+ <%= render "chart_view" %>
5
+ <%= render "stats_view" %>
6
+ <%= render "table_view" %>
7
+ </div>
8
+ <% elsif flash.now[:alert] %>
9
+ <div class="results-section">
10
+ <div class="error-message">
11
+ <span class="error-icon">⚠️</span>
12
+ <strong>Error:</strong> <%= flash.now[:alert] %>
13
+ </div>
14
+ </div>
15
+ <% end %>
@@ -0,0 +1,19 @@
1
+ <div class="results-info">
2
+ <div class="results-meta">
3
+ <%= "#{@result.rows.size} records • #{@result.columns.size} columns" if @result %>
4
+ </div>
5
+
6
+ <div class="view-toggle">
7
+ <button type="button" class="toggle-btn active" data-view="table">
8
+ <span class="toggle-icon">📊</span> Table
9
+ </button>
10
+ <% if should_show_chart?(@result) %>
11
+ <button type="button" class="toggle-btn" data-view="chart">
12
+ <span class="toggle-icon">📈</span> Chart
13
+ </button>
14
+ <% end %>
15
+ <button type="button" class="toggle-btn" data-view="stats">
16
+ <span class="toggle-icon">📋</span> Stats
17
+ </button>
18
+ </div>
19
+ </div>
@@ -0,0 +1,13 @@
1
+ <div class="results-panel">
2
+ <% if @result.nil? %>
3
+ <div class="empty-results">
4
+ <div class="empty-content">
5
+ <div class="empty-icon">📊</div>
6
+ <h3>Ready to Query</h3>
7
+ <p>Enter a SELECT query on the left and click Execute to see results here.</p>
8
+ </div>
9
+ </div>
10
+ <% else %>
11
+ <%= render "result" %>
12
+ <% end %>
13
+ </div>
@@ -0,0 +1,45 @@
1
+ <div class="table-wrapper">
2
+ <div id="tableScroll" class="table-scroll" tabindex="0">
3
+ <table id="resultsTable" class="results-table">
4
+ <!-- Sticky Header -->
5
+ <thead class="sticky-header">
6
+ <tr>
7
+ <th class="row-num">#</th>
8
+ <% @result.columns.each_with_index do |column, index| %>
9
+ <th data-column="<%= index + 1 %>">
10
+ <div class="table-header-content">
11
+ <span class="header-text"><%= column %></span>
12
+ <span class="header-type">text</span>
13
+ </div>
14
+ </th>
15
+ <% end %>
16
+ </tr>
17
+ </thead>
18
+
19
+ <!-- Table Body -->
20
+ <tbody>
21
+ <% @result.rows.each_with_index do |row, row_index| %>
22
+ <tr class="<%= 'even-row' if row_index.even? %>">
23
+ <td class="row-num"><%= row_index + 1 %></td>
24
+ <% row.each_with_index do |cell, col_index| %>
25
+ <td data-column="<%= col_index + 1 %>">
26
+ <span class="cell-content <%= cell_value_class(cell) %>">
27
+ <%= format_cell_value(cell) %>
28
+ </span>
29
+ </td>
30
+ <% end %>
31
+ </tr>
32
+ <% end %>
33
+ </tbody>
34
+ </table>
35
+ </div>
36
+
37
+ <!-- Scroll Indicators -->
38
+ <div id="scrollIndicatorH" class="scroll-indicator scroll-indicator-horizontal">
39
+ <div id="scrollThumbH" class="scroll-thumb"></div>
40
+ </div>
41
+
42
+ <div id="scrollIndicatorV" class="scroll-indicator scroll-indicator-vertical">
43
+ <div id="scrollThumbV" class="scroll-thumb"></div>
44
+ </div>
45
+ </div>
@@ -0,0 +1,3 @@
1
+ <div class="stats-container" id="stats-view" style="display: none;">
2
+ <%= render_stats(@result) %>
3
+ </div>
@@ -0,0 +1,21 @@
1
+ <div class="table-controls">
2
+ <div class="table-info">
3
+ <%= "#{@result.rows.size} rows × #{@result.columns.size} columns" if @result %>
4
+ </div>
5
+
6
+ <div class="table-actions">
7
+ <% if @result && @result.columns.size > 10 %>
8
+ <button type="button" id="toggleColumns" class="table-btn">
9
+ <span class="btn-icon">👁️</span> Show/Hide Columns
10
+ </button>
11
+ <% end %>
12
+
13
+ <button type="button" id="fitColumns" class="table-btn">
14
+ <span class="btn-icon">📏</span> Fit Columns
15
+ </button>
16
+
17
+ <button type="button" id="resetTable" class="table-btn">
18
+ <span class="btn-icon">🔄</span> Reset
19
+ </button>
20
+ </div>
21
+ </div>
@@ -0,0 +1,5 @@
1
+ <div class="table-container" id="table-view">
2
+ <%= render "table_controls" %>
3
+ <%= render "column_panel" if @result && @result.columns.size > 10 %>
4
+ <%= render "results_table" %>
5
+ </div>
@@ -0,0 +1,5 @@
1
+
2
+ <div class="insights-container" data-queries="<%= @insight_queries.to_json %>">
3
+ <%= render "query_panel" %>
4
+ <%= render "results_panel" %>
5
+ </div>
@@ -0,0 +1,85 @@
1
+ - id: 'table_sizes'
2
+ name: 'Table Sizes'
3
+ description: 'Database table sizes (great for charts!)'
4
+ category: 'database'
5
+ sql: >
6
+ SELECT
7
+ tablename as "Table Name",
8
+ pg_total_relation_size(schemaname||'.'||tablename) as "Size (Bytes)"
9
+ FROM pg_tables
10
+ WHERE schemaname = 'public'
11
+ ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
12
+ LIMIT 10
13
+
14
+ - id: 'table_activity'
15
+ name: 'Table Activity'
16
+ description: 'Table insert/update/delete statistics'
17
+ category: 'database'
18
+ sql: >
19
+ SELECT
20
+ schemaname || '.' || relname as "Table",
21
+ n_tup_ins as "Inserts",
22
+ n_tup_upd as "Updates",
23
+ n_tup_del as "Deletes"
24
+ FROM pg_stat_user_tables
25
+ WHERE schemaname = 'public'
26
+ ORDER BY (n_tup_ins + n_tup_upd + n_tup_del) DESC
27
+ LIMIT 8
28
+
29
+ - id: 'index_usage'
30
+ name: 'Index Usage'
31
+ description: 'Index usage statistics'
32
+ category: 'database'
33
+ sql: >
34
+ SELECT
35
+ schemaname || '.' || indexrelname as "Index",
36
+ idx_scan as "Scans",
37
+ idx_tup_read as "Tuples Read"
38
+ FROM pg_stat_user_indexes
39
+ WHERE schemaname = 'public'
40
+ AND idx_scan > 0
41
+ ORDER BY idx_scan DESC
42
+ LIMIT 10
43
+
44
+ - id: 'unused_indexes'
45
+ name: 'Unused Indexes'
46
+ description: 'Indexes that have never been scanned (possible cleanup candidates).'
47
+ category: 'database'
48
+ sql: >
49
+ SELECT
50
+ schemaname || '.' || indexrelname as "Index",
51
+ idx_scan as "Scans",
52
+ pg_size_pretty(pg_relation_size(indexrelid)) as "Index Size"
53
+ FROM pg_stat_user_indexes
54
+ WHERE schemaname = 'public' AND idx_scan = 0
55
+ ORDER BY pg_relation_size(indexrelid) DESC
56
+ LIMIT 12
57
+
58
+ - id: 'slowest_queries'
59
+ name: 'Slowest Queries'
60
+ description: 'Top slowest queries by mean execution time.'
61
+ category: 'database'
62
+ sql: >
63
+ SELECT
64
+ query,
65
+ calls,
66
+ ROUND(mean_exec_time::numeric,2) as "Mean Time (ms)",
67
+ ROUND(total_exec_time::numeric,2) as "Total Time (ms)"
68
+ FROM pg_stat_statements
69
+ ORDER BY mean_exec_time DESC
70
+ LIMIT 10
71
+
72
+ - id: 'table_bloat'
73
+ name: 'Table Bloat Estimate'
74
+ description: 'Estimate of wasted space in tables due to bloat.'
75
+ category: 'database'
76
+ sql: >
77
+ SELECT
78
+ relname as "Table",
79
+ n_live_tup as "Live Rows",
80
+ n_dead_tup as "Dead Rows",
81
+ pg_size_pretty(pg_table_size(relid)) as "Table Size"
82
+ FROM pg_stat_user_tables
83
+ WHERE n_dead_tup > 0 AND schemaname = 'public'
84
+ ORDER BY n_dead_tup DESC
85
+ LIMIT 10