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,104 @@
1
+ document.addEventListener('DOMContentLoaded', function() {
2
+ initializeHealthDashboard();
3
+ });
4
+
5
+ function initializeHealthDashboard() {
6
+ initializeSmoothScrolling();
7
+ initializeSectionHighlighting();
8
+ initializeResponsive();
9
+ }
10
+ function initializeSmoothScrolling() {
11
+ const anchorLinks = document.querySelectorAll('a[href^="#"]');
12
+
13
+ anchorLinks.forEach(link => {
14
+ link.addEventListener('click', function(e) {
15
+ const targetId = this.getAttribute('href').substring(1);
16
+ const targetElement = document.getElementById(targetId);
17
+
18
+ if (targetElement) {
19
+ e.preventDefault();
20
+
21
+ if (this.classList.contains('stat-card-link')) {
22
+ this.classList.add('clicked');
23
+ setTimeout(() => {
24
+ this.classList.remove('clicked');
25
+ }, 200);
26
+ }
27
+
28
+ const headerOffset = 20;
29
+ const elementPosition = targetElement.getBoundingClientRect().top;
30
+ const offsetPosition = elementPosition + window.pageYOffset - headerOffset;
31
+
32
+ window.scrollTo({
33
+ top: offsetPosition,
34
+ behavior: 'smooth'
35
+ });
36
+
37
+ highlightSection(targetElement);
38
+ }
39
+ });
40
+ });
41
+ }
42
+
43
+ function initializeSectionHighlighting() {
44
+ if (window.location.hash) {
45
+ const targetElement = document.querySelector(window.location.hash);
46
+ if (targetElement) {
47
+ setTimeout(() => {
48
+ highlightSection(targetElement);
49
+ }, 500);
50
+ }
51
+ }
52
+ }
53
+
54
+ function highlightSection(element) {
55
+ const previousHighlighted = document.querySelector('.health-section.highlighted');
56
+ if (previousHighlighted) {
57
+ previousHighlighted.classList.remove('highlighted');
58
+ }
59
+
60
+ element.classList.add('highlighted');
61
+
62
+ setTimeout(() => {
63
+ element.classList.remove('highlighted');
64
+ }, 2000);
65
+ }
66
+ function initializeResponsive() {
67
+ let resizeTimeout;
68
+
69
+ window.addEventListener('resize', function() {
70
+ clearTimeout(resizeTimeout);
71
+ resizeTimeout = setTimeout(function() {
72
+ adjustForScreenSize();
73
+ }, 250);
74
+ });
75
+
76
+ adjustForScreenSize();
77
+ }
78
+
79
+ function adjustForScreenSize() {
80
+ const screenWidth = window.innerWidth;
81
+ const queryTexts = document.querySelectorAll('.query-text');
82
+
83
+ queryTexts.forEach(queryText => {
84
+ if (screenWidth <= 768) {
85
+ queryText.style.whiteSpace = 'normal';
86
+ queryText.style.wordBreak = 'break-word';
87
+ } else {
88
+ queryText.style.whiteSpace = 'nowrap';
89
+ queryText.style.wordBreak = 'normal';
90
+ }
91
+ });
92
+ }
93
+ function formatNumber(num) {
94
+ return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
95
+ }
96
+
97
+ function truncateText(text, maxLength) {
98
+ if (text.length <= maxLength) {
99
+ return text;
100
+ }
101
+ return text.substring(0, maxLength - 3) + '...';
102
+ }
103
+
104
+
@@ -0,0 +1,126 @@
1
+ function initChartRendering() {
2
+ const chartTypeSelect = document.getElementById('chartType');
3
+ const chartDataElement = document.querySelector('[data-chart-data]');
4
+
5
+ if (!chartTypeSelect || !chartDataElement) return;
6
+
7
+ try {
8
+ const chartData = JSON.parse(chartDataElement.dataset.chartData);
9
+
10
+ chartTypeSelect.addEventListener('change', function() {
11
+ renderChart(this.value, chartData);
12
+ });
13
+
14
+ // Initial render
15
+ renderChart('bar', chartData);
16
+ } catch (e) {
17
+ console.error('Failed to parse chart data:', e);
18
+ }
19
+ }
20
+
21
+ function renderChart(type, data) {
22
+ const container = document.getElementById('dynamicChart');
23
+ if (!container || !data || !data.chartData) return;
24
+
25
+ const containerRect = container.getBoundingClientRect();
26
+ const containerHeight = Math.max(250, containerRect.height - 20);
27
+ const containerWidth = containerRect.width - 20;
28
+
29
+ const options = {
30
+ height: containerHeight + 'px',
31
+ width: containerWidth + 'px',
32
+ colors: ["#00979D", "#00838a", "#00767a", "#006064", "#004d4f"],
33
+ responsive: true,
34
+ maintainAspectRatio: false,
35
+ library: {
36
+ responsive: true,
37
+ maintainAspectRatio: false,
38
+ interaction: {
39
+ intersect: false
40
+ },
41
+ plugins: {
42
+ legend: {
43
+ display: true,
44
+ position: 'bottom',
45
+ labels: {
46
+ fontSize: 11,
47
+ fontColor: '#6b7280',
48
+ padding: 10
49
+ }
50
+ }
51
+ },
52
+ scales: {
53
+ x: {
54
+ ticks: {
55
+ maxRotation: 45,
56
+ minRotation: 0,
57
+ font: {
58
+ size: 10
59
+ }
60
+ }
61
+ },
62
+ y: {
63
+ ticks: {
64
+ font: {
65
+ size: 10
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
71
+ };
72
+
73
+ container.innerHTML = '';
74
+
75
+ try {
76
+ setTimeout(function() {
77
+ var chartInstance;
78
+
79
+ switch(type) {
80
+ case 'line':
81
+ chartInstance = new Chartkick.LineChart(container, data.chartData, options);
82
+ break;
83
+ case 'bar':
84
+ chartInstance = new Chartkick.BarChart(container, data.chartData, options);
85
+ break;
86
+ case 'pie':
87
+ chartInstance = new Chartkick.PieChart(container, data.chartData, {
88
+ ...options,
89
+ library: {
90
+ ...options.library,
91
+ plugins: {
92
+ legend: {
93
+ display: true,
94
+ position: 'right',
95
+ labels: {
96
+ fontSize: 10,
97
+ fontColor: '#6b7280',
98
+ padding: 8
99
+ }
100
+ }
101
+ }
102
+ }
103
+ });
104
+ break;
105
+ case 'area':
106
+ chartInstance = new Chartkick.AreaChart(container, data.chartData, options);
107
+ break;
108
+ default:
109
+ chartInstance = new Chartkick.BarChart(container, data.chartData, options);
110
+ }
111
+
112
+ if (chartInstance && chartInstance.getChart) {
113
+ setTimeout(function() {
114
+ const chart = chartInstance.getChart();
115
+ if (chart && chart.resize) {
116
+ chart.resize();
117
+ }
118
+ }, 100);
119
+ }
120
+ }, 50);
121
+
122
+ } catch (error) {
123
+ console.error('Chart rendering error:', error);
124
+ container.innerHTML = '<div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; color: #64748b; text-align: center; padding: 20px;"><div style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;">⚠️</div><h3 style="margin: 0 0 8px 0; color: #374151;">Chart Error</h3><p style="margin: 0 0 8px 0; font-size: 14px;">Unable to render chart</p><small style="opacity: 0.7;">' + error.message + '</small></div>';
125
+ }
126
+ }
@@ -0,0 +1,378 @@
1
+ function TableManager() {
2
+ this.table = document.getElementById('resultsTable');
3
+ this.tableScroll = document.getElementById('tableScroll');
4
+ this.columnPanel = document.getElementById('columnPanel');
5
+ this.originalColumnWidths = new Map();
6
+ this.isResizing = false;
7
+
8
+ if (this.table) {
9
+ this.init();
10
+ }
11
+ }
12
+
13
+ TableManager.prototype.init = function() {
14
+ this.setupColumnToggles();
15
+ this.setupTableControls();
16
+ this.setupScrollIndicators();
17
+ this.setupColumnResizing();
18
+ this.setupKeyboardNavigation();
19
+ this.detectColumnTypes();
20
+ };
21
+
22
+ TableManager.prototype.setupColumnToggles = function() {
23
+ var self = this;
24
+ var toggleBtn = document.getElementById('toggleColumns');
25
+ var showAllBtn = document.getElementById('showAllColumns');
26
+ var hideAllBtn = document.getElementById('hideAllColumns');
27
+ var columnToggles = document.querySelectorAll('.column-toggle');
28
+
29
+ if (toggleBtn && this.columnPanel) {
30
+ toggleBtn.addEventListener('click', function() {
31
+ var isVisible = self.columnPanel.style.display !== 'none';
32
+ self.columnPanel.style.display = isVisible ? 'none' : 'block';
33
+ toggleBtn.textContent = isVisible ? '👁️ Show/Hide Columns' : '👁️ Hide Panel';
34
+ });
35
+ }
36
+
37
+ if (showAllBtn) {
38
+ showAllBtn.addEventListener('click', function() {
39
+ columnToggles.forEach(function(toggle) {
40
+ toggle.checked = true;
41
+ self.toggleColumn(toggle.dataset.column, true);
42
+ });
43
+ });
44
+ }
45
+
46
+ if (hideAllBtn) {
47
+ hideAllBtn.addEventListener('click', function() {
48
+ columnToggles.forEach(function(toggle) {
49
+ toggle.checked = false;
50
+ self.toggleColumn(toggle.dataset.column, false);
51
+ });
52
+ });
53
+ }
54
+
55
+ columnToggles.forEach(function(toggle) {
56
+ toggle.addEventListener('change', function() {
57
+ self.toggleColumn(toggle.dataset.column, toggle.checked);
58
+ });
59
+ });
60
+ };
61
+
62
+ TableManager.prototype.toggleColumn = function(columnIndex, show) {
63
+ var columns = document.querySelectorAll('[data-column="' + columnIndex + '"]');
64
+ columns.forEach(function(col) {
65
+ if (show) {
66
+ col.classList.remove('column-hidden');
67
+ col.style.display = '';
68
+ } else {
69
+ col.classList.add('column-hidden');
70
+ col.style.display = 'none';
71
+ }
72
+ });
73
+ };
74
+
75
+ TableManager.prototype.setupTableControls = function() {
76
+ var self = this;
77
+ var fitBtn = document.getElementById('fitColumns');
78
+ var resetBtn = document.getElementById('resetTable');
79
+
80
+ if (fitBtn) {
81
+ fitBtn.addEventListener('click', function(e) {
82
+ e.preventDefault();
83
+ self.fitColumns();
84
+ });
85
+ }
86
+
87
+ if (resetBtn) {
88
+ resetBtn.addEventListener('click', function(e) {
89
+ e.preventDefault();
90
+ self.resetTable();
91
+ });
92
+ }
93
+ };
94
+
95
+ TableManager.prototype.fitColumns = function() {
96
+ // Check if required elements exist
97
+ if (!this.table || !this.tableScroll) {
98
+ return;
99
+ }
100
+
101
+ // Get all non-row-number headers that are visible
102
+ var headers = this.table.querySelectorAll('th:not(.row-num):not(.column-hidden)');
103
+
104
+ if (headers.length === 0) {
105
+ return;
106
+ }
107
+
108
+ // Calculate available width (subtract row number column and padding)
109
+ var rowNumWidth = 60; // Width for row number column
110
+ var scrollbarWidth = 20; // Account for scrollbar
111
+ var padding = 20; // Additional padding
112
+ var containerWidth = this.tableScroll.clientWidth - rowNumWidth - scrollbarWidth - padding;
113
+
114
+ // Ensure minimum total width and calculate per-column width
115
+ var minColumnWidth = 100;
116
+ var maxColumnWidth = 300;
117
+ var columnWidth = Math.max(minColumnWidth, Math.min(maxColumnWidth, containerWidth / headers.length));
118
+
119
+ // Set table layout to fixed for consistent column sizing
120
+ this.table.style.tableLayout = 'fixed';
121
+
122
+ // Apply width to headers and corresponding cells
123
+ headers.forEach(function(header, index) {
124
+ var columnIndex = header.getAttribute('data-column');
125
+
126
+ // Set header width
127
+ header.style.width = columnWidth + 'px';
128
+ header.style.minWidth = columnWidth + 'px';
129
+ header.style.maxWidth = columnWidth + 'px';
130
+
131
+ // Set corresponding cell widths
132
+ var cells = document.querySelectorAll('td[data-column="' + columnIndex + '"]:not(.column-hidden)');
133
+ cells.forEach(function(cell) {
134
+ cell.style.width = columnWidth + 'px';
135
+ cell.style.minWidth = columnWidth + 'px';
136
+ cell.style.maxWidth = columnWidth + 'px';
137
+ });
138
+ });
139
+ };
140
+
141
+ TableManager.prototype.resetTable = function() {
142
+ // Reset table layout
143
+ if (this.table) {
144
+ this.table.style.tableLayout = '';
145
+ }
146
+
147
+ // Reset all column and cell styles
148
+ var allColumns = this.table.querySelectorAll('th, td');
149
+ allColumns.forEach(function(col) {
150
+ col.style.width = '';
151
+ col.style.minWidth = '';
152
+ col.style.maxWidth = '';
153
+ col.style.display = '';
154
+ col.classList.remove('column-hidden');
155
+ });
156
+
157
+ // Show all hidden columns
158
+ var hiddenColumns = this.table.querySelectorAll('.column-hidden');
159
+ hiddenColumns.forEach(function(col) {
160
+ col.classList.remove('column-hidden');
161
+ col.style.display = '';
162
+ });
163
+
164
+ // Reset all column toggles to checked state
165
+ var toggles = document.querySelectorAll('.column-toggle');
166
+ toggles.forEach(function(toggle) {
167
+ toggle.checked = true;
168
+ });
169
+
170
+ // Hide column panel
171
+ if (this.columnPanel) {
172
+ this.columnPanel.style.display = 'none';
173
+
174
+ // Also reset the toggle button text
175
+ var toggleBtn = document.getElementById('toggleColumns');
176
+ if (toggleBtn) {
177
+ toggleBtn.innerHTML = '<span class="btn-icon">👁️</span> Show/Hide Columns';
178
+ }
179
+ }
180
+ };
181
+
182
+ TableManager.prototype.setupScrollIndicators = function() {
183
+ if (!this.tableScroll) return;
184
+
185
+ var self = this;
186
+ var scrollIndicatorH = document.getElementById('scrollIndicatorH');
187
+ var scrollIndicatorV = document.getElementById('scrollIndicatorV');
188
+ var scrollTimeout;
189
+
190
+ this.tableScroll.addEventListener('scroll', function() {
191
+ self.updateScrollIndicators();
192
+
193
+ if (scrollIndicatorH) scrollIndicatorH.classList.add('visible');
194
+ if (scrollIndicatorV) scrollIndicatorV.classList.add('visible');
195
+
196
+ clearTimeout(scrollTimeout);
197
+ scrollTimeout = setTimeout(function() {
198
+ if (scrollIndicatorH) scrollIndicatorH.classList.remove('visible');
199
+ if (scrollIndicatorV) scrollIndicatorV.classList.remove('visible');
200
+ }, 1000);
201
+ });
202
+
203
+ this.updateScrollIndicators();
204
+ };
205
+
206
+ TableManager.prototype.updateScrollIndicators = function() {
207
+ var scrollIndicatorH = document.getElementById('scrollIndicatorH');
208
+ var scrollIndicatorV = document.getElementById('scrollIndicatorV');
209
+ var scrollThumbH = document.getElementById('scrollThumbH');
210
+ var scrollThumbV = document.getElementById('scrollThumbV');
211
+
212
+ if (!this.tableScroll) return;
213
+
214
+ var scrollLeft = this.tableScroll.scrollLeft;
215
+ var scrollTop = this.tableScroll.scrollTop;
216
+ var scrollWidth = this.tableScroll.scrollWidth;
217
+ var scrollHeight = this.tableScroll.scrollHeight;
218
+ var clientWidth = this.tableScroll.clientWidth;
219
+ var clientHeight = this.tableScroll.clientHeight;
220
+
221
+ if (scrollIndicatorH && scrollThumbH && scrollWidth > clientWidth) {
222
+ var thumbWidth = (clientWidth / scrollWidth) * 100;
223
+ var thumbLeft = (scrollLeft / (scrollWidth - clientWidth)) * (100 - thumbWidth);
224
+
225
+ scrollThumbH.style.width = thumbWidth + '%';
226
+ scrollThumbH.style.left = thumbLeft + '%';
227
+ }
228
+
229
+ if (scrollIndicatorV && scrollThumbV && scrollHeight > clientHeight) {
230
+ var thumbHeight = (clientHeight / scrollHeight) * 100;
231
+ var thumbTop = (scrollTop / (scrollHeight - clientHeight)) * (100 - thumbHeight);
232
+
233
+ scrollThumbV.style.height = thumbHeight + '%';
234
+ scrollThumbV.style.top = thumbTop + '%';
235
+ }
236
+ };
237
+
238
+ TableManager.prototype.setupColumnResizing = function() {
239
+ var self = this;
240
+ var headers = this.table.querySelectorAll('th:not(.row-num)');
241
+
242
+ headers.forEach(function(header, index) {
243
+ var resizeHandle = document.createElement('div');
244
+ resizeHandle.className = 'resize-handle';
245
+ header.appendChild(resizeHandle);
246
+
247
+ var startX, startWidth;
248
+
249
+ resizeHandle.addEventListener('mousedown', function(e) {
250
+ self.isResizing = true;
251
+ startX = e.clientX;
252
+ startWidth = parseInt(document.defaultView.getComputedStyle(header).width, 10);
253
+
254
+ document.addEventListener('mousemove', handleMouseMove);
255
+ document.addEventListener('mouseup', handleMouseUp);
256
+
257
+ e.preventDefault();
258
+ });
259
+
260
+ var handleMouseMove = function(e) {
261
+ if (!self.isResizing) return;
262
+
263
+ var width = startWidth + e.clientX - startX;
264
+ var minWidth = 80;
265
+ var maxWidth = 500;
266
+ var newWidth = Math.max(minWidth, Math.min(maxWidth, width));
267
+
268
+ header.style.width = newWidth + 'px';
269
+ header.style.minWidth = newWidth + 'px';
270
+ header.style.maxWidth = newWidth + 'px';
271
+
272
+ var cells = self.table.querySelectorAll('td[data-column="' + (index + 1) + '"]');
273
+ cells.forEach(function(cell) {
274
+ cell.style.width = newWidth + 'px';
275
+ cell.style.minWidth = newWidth + 'px';
276
+ cell.style.maxWidth = newWidth + 'px';
277
+ });
278
+ };
279
+
280
+ var handleMouseUp = function() {
281
+ self.isResizing = false;
282
+ document.removeEventListener('mousemove', handleMouseMove);
283
+ document.removeEventListener('mouseup', handleMouseUp);
284
+ };
285
+ });
286
+ };
287
+
288
+ TableManager.prototype.setupKeyboardNavigation = function() {
289
+ var self = this;
290
+
291
+ this.tableScroll.addEventListener('keydown', function(e) {
292
+ if (!e.target.closest('.results-table')) return;
293
+
294
+ var scrollAmount = 50;
295
+
296
+ switch(e.key) {
297
+ case 'ArrowLeft':
298
+ self.tableScroll.scrollLeft -= scrollAmount;
299
+ e.preventDefault();
300
+ break;
301
+ case 'ArrowRight':
302
+ self.tableScroll.scrollLeft += scrollAmount;
303
+ e.preventDefault();
304
+ break;
305
+ case 'ArrowUp':
306
+ self.tableScroll.scrollTop -= scrollAmount;
307
+ e.preventDefault();
308
+ break;
309
+ case 'ArrowDown':
310
+ self.tableScroll.scrollTop += scrollAmount;
311
+ e.preventDefault();
312
+ break;
313
+ case 'Home':
314
+ if (e.ctrlKey) {
315
+ self.tableScroll.scrollTop = 0;
316
+ self.tableScroll.scrollLeft = 0;
317
+ } else {
318
+ self.tableScroll.scrollLeft = 0;
319
+ }
320
+ e.preventDefault();
321
+ break;
322
+ case 'End':
323
+ if (e.ctrlKey) {
324
+ self.tableScroll.scrollTop = self.tableScroll.scrollHeight;
325
+ self.tableScroll.scrollLeft = self.tableScroll.scrollWidth;
326
+ } else {
327
+ self.tableScroll.scrollLeft = self.tableScroll.scrollWidth;
328
+ }
329
+ e.preventDefault();
330
+ break;
331
+ }
332
+ });
333
+ };
334
+
335
+ TableManager.prototype.detectColumnTypes = function() {
336
+ var headers = this.table.querySelectorAll('th:not(.row-num)');
337
+
338
+ headers.forEach(function(header, index) {
339
+ var cells = document.querySelectorAll('td[data-column="' + (index + 1) + '"] .cell-content');
340
+ var typeSpan = header.querySelector('.header-type');
341
+
342
+ if (!typeSpan || cells.length === 0) return;
343
+
344
+ var numericCount = 0;
345
+ var dateCount = 0;
346
+ var sampleSize = Math.min(10, cells.length);
347
+
348
+ for (var i = 0; i < sampleSize; i++) {
349
+ var cell = cells[i];
350
+ var value = cell.textContent.trim();
351
+
352
+ if (value && value !== 'NULL' && value !== 'empty') {
353
+ if (/^-?\d+(\.\d+)?$/.test(value)) {
354
+ numericCount++;
355
+ } else if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
356
+ dateCount++;
357
+ }
358
+ }
359
+ }
360
+
361
+ var numericRatio = numericCount / sampleSize;
362
+ var dateRatio = dateCount / sampleSize;
363
+
364
+ if (numericRatio > 0.7) {
365
+ typeSpan.textContent = 'number';
366
+ typeSpan.style.background = '#dcfce7';
367
+ typeSpan.style.color = '#166534';
368
+ } else if (dateRatio > 0.7) {
369
+ typeSpan.textContent = 'date';
370
+ typeSpan.style.background = '#e0f2fe';
371
+ typeSpan.style.color = '#0c4a6e';
372
+ } else {
373
+ typeSpan.textContent = 'text';
374
+ typeSpan.style.background = '#f1f5f9';
375
+ typeSpan.style.color = '#64748b';
376
+ }
377
+ });
378
+ };
@@ -0,0 +1,25 @@
1
+ function initViewToggles() {
2
+ const toggleBtns = document.querySelectorAll('.toggle-btn');
3
+ const views = {
4
+ table: document.getElementById('table-view'),
5
+ chart: document.getElementById('chart-view'),
6
+ stats: document.getElementById('stats-view')
7
+ };
8
+
9
+ toggleBtns.forEach(function(btn) {
10
+ btn.addEventListener('click', function() {
11
+ const targetView = this.dataset.view;
12
+
13
+ // Update active button
14
+ toggleBtns.forEach(function(b) { b.classList.remove('active'); });
15
+ this.classList.add('active');
16
+
17
+ // Show/hide views
18
+ Object.keys(views).forEach(function(viewName) {
19
+ if (views[viewName]) {
20
+ views[viewName].style.display = viewName === targetView ? 'block' : 'none';
21
+ }
22
+ });
23
+ });
24
+ });
25
+ }
@@ -0,0 +1,13 @@
1
+ //= require_tree ./results
2
+
3
+ document.addEventListener('DOMContentLoaded', function() {
4
+ if (!document.querySelector('.results-section')) return;
5
+
6
+ initViewToggles();
7
+ initChartRendering();
8
+ initTableManager();
9
+ });
10
+
11
+ function initTableManager() {
12
+ var tableManager = new TableManager();
13
+ }