pg_reports 0.3.1 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +36 -0
- data/app/controllers/pg_reports/dashboard_controller.rb +59 -4
- data/app/views/layouts/pg_reports/application.html.erb +1 -1
- data/app/views/pg_reports/dashboard/_show_modals.html.erb +8 -1
- data/app/views/pg_reports/dashboard/_show_scripts.html.erb +56 -18
- data/app/views/pg_reports/dashboard/_show_styles.html.erb +122 -1
- data/app/views/pg_reports/dashboard/show.html.erb +89 -47
- data/config/locales/en.yml +13 -0
- data/config/locales/ru.yml +13 -0
- data/config/locales/uk.yml +13 -0
- data/lib/pg_reports/dashboard/reports_registry.rb +14 -0
- data/lib/pg_reports/definitions/connections/active_connections.yml +23 -0
- data/lib/pg_reports/definitions/connections/blocking_queries.yml +20 -0
- data/lib/pg_reports/definitions/connections/connection_stats.yml +18 -0
- data/lib/pg_reports/definitions/connections/idle_connections.yml +21 -0
- data/lib/pg_reports/definitions/connections/locks.yml +22 -0
- data/lib/pg_reports/definitions/connections/long_running_queries.yml +43 -0
- data/lib/pg_reports/definitions/indexes/bloated_indexes.yml +43 -0
- data/lib/pg_reports/definitions/indexes/duplicate_indexes.yml +19 -0
- data/lib/pg_reports/definitions/indexes/index_sizes.yml +29 -0
- data/lib/pg_reports/definitions/indexes/index_usage.yml +27 -0
- data/lib/pg_reports/definitions/indexes/invalid_indexes.yml +19 -0
- data/lib/pg_reports/definitions/indexes/missing_indexes.yml +27 -0
- data/lib/pg_reports/definitions/indexes/unused_indexes.yml +41 -0
- data/lib/pg_reports/definitions/queries/all_queries.yml +35 -0
- data/lib/pg_reports/definitions/queries/expensive_queries.yml +43 -0
- data/lib/pg_reports/definitions/queries/heavy_queries.yml +49 -0
- data/lib/pg_reports/definitions/queries/low_cache_hit_queries.yml +47 -0
- data/lib/pg_reports/definitions/queries/missing_index_queries.yml +31 -0
- data/lib/pg_reports/definitions/queries/slow_queries.yml +48 -0
- data/lib/pg_reports/definitions/system/activity_overview.yml +17 -0
- data/lib/pg_reports/definitions/system/cache_stats.yml +18 -0
- data/lib/pg_reports/definitions/system/database_sizes.yml +18 -0
- data/lib/pg_reports/definitions/system/extensions.yml +19 -0
- data/lib/pg_reports/definitions/system/settings.yml +20 -0
- data/lib/pg_reports/definitions/tables/bloated_tables.yml +43 -0
- data/lib/pg_reports/definitions/tables/cache_hit_ratios.yml +26 -0
- data/lib/pg_reports/definitions/tables/recently_modified.yml +27 -0
- data/lib/pg_reports/definitions/tables/row_counts.yml +29 -0
- data/lib/pg_reports/definitions/tables/seq_scans.yml +31 -0
- data/lib/pg_reports/definitions/tables/table_sizes.yml +31 -0
- data/lib/pg_reports/definitions/tables/vacuum_needed.yml +39 -0
- data/lib/pg_reports/filter.rb +58 -0
- data/lib/pg_reports/module_generator.rb +44 -0
- data/lib/pg_reports/modules/connections.rb +8 -73
- data/lib/pg_reports/modules/indexes.rb +9 -94
- data/lib/pg_reports/modules/queries.rb +9 -100
- data/lib/pg_reports/modules/schema_analysis.rb +156 -0
- data/lib/pg_reports/modules/system.rb +7 -59
- data/lib/pg_reports/modules/tables.rb +9 -96
- data/lib/pg_reports/report_definition.rb +161 -0
- data/lib/pg_reports/report_loader.rb +38 -0
- data/lib/pg_reports/sql/schema_analysis/unique_indexes.sql +35 -0
- data/lib/pg_reports/version.rb +1 -1
- data/lib/pg_reports.rb +24 -0
- metadata +38 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7a57deebae9eda0df4aceb3cfeb1912e2559a7cd70c3c7f197fb86f06bb689b1
|
|
4
|
+
data.tar.gz: 9369b5d6f54d8b0f2939f57d429f477aa0a8c2a862add70e2116232f364fe1e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0e6248b26056ffe059105c2399f7a9c8e607e3f34030d4af6f14b324deb76435163b8b2116ff5d7502350410f0a6a20368ac3757b9247d30328da3330a941a86
|
|
7
|
+
data.tar.gz: 5d67f8d3c5663421b839301c3c4463502b7cbaeb57d3351d808ff00ebb355141b6c7cf676df091f7a2bb52238809cb02b8e9123d16b7a597bbc636e0cf888540
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.4.0] - 2026-01-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **YAML-based Report Configuration System** - declarative report definitions:
|
|
13
|
+
- Each report now defined in a single YAML file (1 file = 1 report approach)
|
|
14
|
+
- Migrated all 31 reports from Ruby methods to YAML format
|
|
15
|
+
- New classes: `ReportDefinition`, `Filter`, `ReportLoader`, `ModuleGenerator`
|
|
16
|
+
- Support for post-SQL filtering with operators (eq, ne, lt, lte, gt, gte)
|
|
17
|
+
- Title interpolation with variable substitution using `${variable}` syntax
|
|
18
|
+
- Enrichment hooks pattern for data transformation
|
|
19
|
+
- Problem explanations configuration in YAML (field → explanation key mapping)
|
|
20
|
+
- **Filter UI** on report pages for manual parameter input:
|
|
21
|
+
- Collapsible filter section with form inputs for each parameter
|
|
22
|
+
- Support for threshold overrides (limit, min_duration_seconds, etc.)
|
|
23
|
+
- Real-time report refresh with custom filter values
|
|
24
|
+
- Horizontal layout with descriptions and inputs side-by-side
|
|
25
|
+
- Dark theme styling for filter inputs and labels
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Report definitions now use declarative YAML files instead of repetitive Ruby methods (~800 lines of code eliminated)
|
|
30
|
+
- Modules now use metaprogramming to dynamically generate methods from YAML at load time
|
|
31
|
+
- Dashboard category cards width increased from 280px to 350px for better readability
|
|
32
|
+
- Documentation and filter sections positioned side-by-side in responsive grid layout
|
|
33
|
+
- Filter parameters section positioned below documentation and collapsed by default
|
|
34
|
+
- Reduced vertical spacing between sections for more compact layout
|
|
35
|
+
- Problem field to explanation key mapping moved from hardcoded JavaScript to server-driven YAML configuration
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- CSS Grid `align-items: stretch` causing synchronized expansion of documentation and filter blocks
|
|
40
|
+
- Visual "chin" appearing on closed filter details block due to default margins
|
|
41
|
+
- TypeError when report has no limit parameter default value
|
|
42
|
+
- Excessive margins between report sections
|
|
43
|
+
|
|
8
44
|
## [0.3.0] - 2026-01-28
|
|
9
45
|
|
|
10
46
|
### Added
|
|
@@ -51,6 +51,9 @@ module PgReports
|
|
|
51
51
|
@thresholds = Dashboard::ReportsRegistry.thresholds(@report_key)
|
|
52
52
|
@problem_fields = Dashboard::ReportsRegistry.problem_fields(@report_key)
|
|
53
53
|
|
|
54
|
+
# Load filter parameters from YAML
|
|
55
|
+
@report_filters = load_report_filters(@category, @report_key)
|
|
56
|
+
|
|
54
57
|
@report = execute_report(@category, @report_key)
|
|
55
58
|
rescue => e
|
|
56
59
|
@error = e.message
|
|
@@ -61,9 +64,13 @@ module PgReports
|
|
|
61
64
|
category = params[:category].to_sym
|
|
62
65
|
report_key = params[:report].to_sym
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
# Extract filter parameters from request
|
|
68
|
+
filter_params = extract_filter_params
|
|
69
|
+
|
|
70
|
+
report = execute_report(category, report_key, **filter_params)
|
|
65
71
|
thresholds = Dashboard::ReportsRegistry.thresholds(report_key)
|
|
66
72
|
problem_fields = Dashboard::ReportsRegistry.problem_fields(report_key)
|
|
73
|
+
problem_explanations = load_problem_explanations(category, report_key)
|
|
67
74
|
|
|
68
75
|
render json: {
|
|
69
76
|
success: true,
|
|
@@ -73,7 +80,8 @@ module PgReports
|
|
|
73
80
|
total: report.size,
|
|
74
81
|
generated_at: report.generated_at.strftime("%Y-%m-%d %H:%M:%S"),
|
|
75
82
|
thresholds: thresholds,
|
|
76
|
-
problem_fields: problem_fields
|
|
83
|
+
problem_fields: problem_fields,
|
|
84
|
+
problem_explanations: problem_explanations
|
|
77
85
|
}
|
|
78
86
|
rescue => e
|
|
79
87
|
render json: {success: false, error: e.message}, status: :unprocessable_entity
|
|
@@ -240,6 +248,15 @@ module PgReports
|
|
|
240
248
|
end
|
|
241
249
|
|
|
242
250
|
def create_migration
|
|
251
|
+
# Only allow migration creation in development environment
|
|
252
|
+
unless Rails.env.development?
|
|
253
|
+
render json: {
|
|
254
|
+
success: false,
|
|
255
|
+
error: "Migration creation is only allowed in development environment"
|
|
256
|
+
}, status: :forbidden
|
|
257
|
+
return
|
|
258
|
+
end
|
|
259
|
+
|
|
243
260
|
file_name = params[:file_name]
|
|
244
261
|
code = params[:code]
|
|
245
262
|
|
|
@@ -280,13 +297,51 @@ module PgReports
|
|
|
280
297
|
@categories = Dashboard::ReportsRegistry.all
|
|
281
298
|
end
|
|
282
299
|
|
|
283
|
-
def
|
|
300
|
+
def load_report_filters(category, report_key)
|
|
301
|
+
definition = ReportLoader.get(category.to_s, report_key.to_s)
|
|
302
|
+
return {} unless definition
|
|
303
|
+
|
|
304
|
+
definition.filter_parameters
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def load_problem_explanations(category, report_key)
|
|
308
|
+
definition = ReportLoader.get(category.to_s, report_key.to_s)
|
|
309
|
+
return {} unless definition
|
|
310
|
+
|
|
311
|
+
definition.problem_explanations
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def extract_filter_params
|
|
315
|
+
# Allow common filter parameters
|
|
316
|
+
allowed = [:limit, :min_duration_seconds, :min_calls]
|
|
317
|
+
result = {}
|
|
318
|
+
|
|
319
|
+
allowed.each do |key|
|
|
320
|
+
if params[key].present?
|
|
321
|
+
value = params[key].to_s
|
|
322
|
+
# Convert to appropriate type
|
|
323
|
+
result[key] = value.match?(/^\d+$/) ? value.to_i : value
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Also allow threshold overrides (calls_threshold, etc.)
|
|
328
|
+
params.each do |key, value|
|
|
329
|
+
if key.to_s.end_with?('_threshold') && value.present?
|
|
330
|
+
result[key.to_sym] = value.to_i
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
result
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def execute_report(category, report_key, **filter_params)
|
|
284
338
|
mod = case category
|
|
285
339
|
when :queries then Modules::Queries
|
|
286
340
|
when :indexes then Modules::Indexes
|
|
287
341
|
when :tables then Modules::Tables
|
|
288
342
|
when :connections then Modules::Connections
|
|
289
343
|
when :system then Modules::System
|
|
344
|
+
when :schema_analysis then Modules::SchemaAnalysis
|
|
290
345
|
else raise ArgumentError, "Unknown category: #{category}"
|
|
291
346
|
end
|
|
292
347
|
|
|
@@ -294,7 +349,7 @@ module PgReports
|
|
|
294
349
|
raise ArgumentError, "Unknown report: #{report_key}"
|
|
295
350
|
end
|
|
296
351
|
|
|
297
|
-
mod.public_send(report_key)
|
|
352
|
+
mod.public_send(report_key, **filter_params)
|
|
298
353
|
end
|
|
299
354
|
|
|
300
355
|
def substitute_params(query, params_hash)
|
|
@@ -94,11 +94,18 @@
|
|
|
94
94
|
<button class="modal-close" onclick="closeMigrationModal()">×</button>
|
|
95
95
|
</div>
|
|
96
96
|
<div class="modal-body" id="migration-modal-body">
|
|
97
|
+
<div class="migration-warning" style="background: rgba(255, 152, 0, 0.1); border: 1px solid rgba(255, 152, 0, 0.3); padding: 1rem; border-radius: 8px; margin-bottom: 1rem;">
|
|
98
|
+
<p style="margin: 0 0 0.5rem 0; font-weight: 600; color: #ffb74d;">⚠️ Warning</p>
|
|
99
|
+
<p style="margin: 0; color: #ffcc80; font-size: 0.875rem; line-height: 1.5;">
|
|
100
|
+
Creating a migration will generate a migration file in your project. Running this migration will drop the index from the database, which may significantly impact application performance.
|
|
101
|
+
<strong>This operation should only be performed in a local development environment.</strong>
|
|
102
|
+
</p>
|
|
103
|
+
</div>
|
|
97
104
|
<p class="settings-label">Generated migration to remove the index:</p>
|
|
98
105
|
<div id="migration-code" class="migration-code"></div>
|
|
99
106
|
<div class="migration-actions">
|
|
100
107
|
<button class="btn btn-secondary" onclick="copyMigrationCode()">📋 Copy Code</button>
|
|
101
|
-
<button class="btn btn-primary" onclick="createMigrationFile()">📁 Create File & Open in IDE</button>
|
|
108
|
+
<button class="btn btn-primary" id="create-migration-btn" onclick="createMigrationFile()">📁 Create File & Open in IDE</button>
|
|
102
109
|
</div>
|
|
103
110
|
</div>
|
|
104
111
|
</div>
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
let currentReportData = null;
|
|
3
|
+
let currentProblemExplanations = {};
|
|
3
4
|
const category = '<%= @category %>';
|
|
4
5
|
const reportKey = '<%= @report_key %>';
|
|
6
|
+
const railsEnv = '<%= Rails.env %>';
|
|
7
|
+
const isDevelopment = railsEnv === 'development';
|
|
5
8
|
let syncingScroll = false;
|
|
6
9
|
let currentSort = { column: null, direction: 'asc' };
|
|
7
10
|
|
|
@@ -331,21 +334,16 @@
|
|
|
331
334
|
document.getElementById('problem-modal').style.display = 'none';
|
|
332
335
|
}
|
|
333
336
|
|
|
334
|
-
// Map problem fields to explanation keys
|
|
337
|
+
// Map problem fields to explanation keys using server-provided mapping
|
|
335
338
|
function getExplanationKey(problems) {
|
|
336
339
|
const fields = problems.map(p => p.field);
|
|
337
340
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (fields.includes('bloat_ratio') || fields.includes('bloat_size')) return 'high_bloat';
|
|
345
|
-
if (fields.includes('dead_tuple_ratio') || fields.includes('n_dead_tup')) return 'many_dead_tuples';
|
|
346
|
-
if (fields.includes('duration_seconds') || fields.includes('duration')) return 'long_running';
|
|
347
|
-
if (fields.includes('blocked_count')) return 'blocking';
|
|
348
|
-
if (fields.includes('idle_in_transaction')) return 'idle_in_transaction';
|
|
341
|
+
// Use server-provided problem explanations mapping
|
|
342
|
+
for (const field of fields) {
|
|
343
|
+
if (currentProblemExplanations[field]) {
|
|
344
|
+
return currentProblemExplanations[field];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
349
347
|
|
|
350
348
|
return '';
|
|
351
349
|
}
|
|
@@ -541,11 +539,15 @@
|
|
|
541
539
|
|
|
542
540
|
function buildDetailRow(row, columns, rowIndex, thresholds, problemFields) {
|
|
543
541
|
let html = '<div class="row-detail">';
|
|
544
|
-
const hasQuery = columns.includes('query')
|
|
545
|
-
const hasIndexName = columns.includes('index_name')
|
|
542
|
+
const hasQuery = columns.includes('query') || row.query;
|
|
543
|
+
const hasIndexName = columns.includes('index_name') || row.index_name;
|
|
546
544
|
const rowId = generateRowId(row);
|
|
547
545
|
|
|
548
|
-
|
|
546
|
+
// Show all fields from row, not just columns
|
|
547
|
+
// First show fields from columns array in order, then remaining fields
|
|
548
|
+
const allKeys = [...new Set([...columns, ...Object.keys(row)])];
|
|
549
|
+
|
|
550
|
+
allKeys.forEach(col => {
|
|
549
551
|
const value = row[col] ?? '';
|
|
550
552
|
const strValue = String(value);
|
|
551
553
|
const isQuery = col === 'query';
|
|
@@ -599,12 +601,16 @@
|
|
|
599
601
|
}
|
|
600
602
|
}
|
|
601
603
|
|
|
602
|
-
// Show migration button for index reports
|
|
604
|
+
// Show migration button for index reports (only in development)
|
|
603
605
|
if (hasIndexName && (category === 'indexes')) {
|
|
604
606
|
const indexName = row.index_name;
|
|
605
607
|
const tableName = row.table_name || row.tablename || '';
|
|
606
608
|
const schemaName = row.schema_name || row.schemaname || 'public';
|
|
607
|
-
|
|
609
|
+
if (isDevelopment) {
|
|
610
|
+
html += `<button class="btn-migration" onclick="event.stopPropagation(); showMigrationModal('${escapeHtml(indexName)}', '${escapeHtml(tableName)}', '${escapeHtml(schemaName)}')">🗑️ Generate Migration</button>`;
|
|
611
|
+
} else {
|
|
612
|
+
html += `<button class="btn-migration" disabled title="Migration generation is only available in development environment" style="opacity: 0.5; cursor: not-allowed;">🗑️ Generate Migration (Development Only)</button>`;
|
|
613
|
+
}
|
|
608
614
|
}
|
|
609
615
|
|
|
610
616
|
html += `</div>`;
|
|
@@ -634,12 +640,27 @@
|
|
|
634
640
|
if (telegramBtn) telegramBtn.style.display = 'none';
|
|
635
641
|
|
|
636
642
|
try {
|
|
643
|
+
// Collect filter parameters from form
|
|
644
|
+
const filterParams = {};
|
|
645
|
+
document.querySelectorAll('[data-param]').forEach(input => {
|
|
646
|
+
const paramName = input.dataset.param;
|
|
647
|
+
const value = input.value;
|
|
648
|
+
if (value && value.trim() !== '') {
|
|
649
|
+
filterParams[paramName] = value;
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
|
|
637
653
|
const response = await fetch(`${pgReportsRoot}/${cat}/${report}/run`, {
|
|
638
654
|
method: 'POST',
|
|
639
655
|
headers: {
|
|
640
656
|
'Content-Type': 'application/json',
|
|
641
657
|
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]')?.content || ''
|
|
642
|
-
}
|
|
658
|
+
},
|
|
659
|
+
body: JSON.stringify({
|
|
660
|
+
category: cat,
|
|
661
|
+
report: report,
|
|
662
|
+
...filterParams
|
|
663
|
+
})
|
|
643
664
|
});
|
|
644
665
|
|
|
645
666
|
const data = await response.json();
|
|
@@ -653,6 +674,7 @@
|
|
|
653
674
|
}
|
|
654
675
|
|
|
655
676
|
currentReportData = data;
|
|
677
|
+
currentProblemExplanations = data.problem_explanations || {};
|
|
656
678
|
const thresholds = data.thresholds || {};
|
|
657
679
|
const problemFields = data.problem_fields || [];
|
|
658
680
|
|
|
@@ -1239,6 +1261,22 @@ end
|
|
|
1239
1261
|
|
|
1240
1262
|
document.getElementById('migration-code').textContent = migrationCode;
|
|
1241
1263
|
document.getElementById('migration-modal').style.display = 'flex';
|
|
1264
|
+
|
|
1265
|
+
// Disable "Create File" button in non-development environments
|
|
1266
|
+
const createBtn = document.getElementById('create-migration-btn');
|
|
1267
|
+
if (createBtn) {
|
|
1268
|
+
if (!isDevelopment) {
|
|
1269
|
+
createBtn.disabled = true;
|
|
1270
|
+
createBtn.style.opacity = '0.5';
|
|
1271
|
+
createBtn.style.cursor = 'not-allowed';
|
|
1272
|
+
createBtn.title = 'Migration file creation is only available in development environment';
|
|
1273
|
+
} else {
|
|
1274
|
+
createBtn.disabled = false;
|
|
1275
|
+
createBtn.style.opacity = '1';
|
|
1276
|
+
createBtn.style.cursor = 'pointer';
|
|
1277
|
+
createBtn.title = '';
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1242
1280
|
}
|
|
1243
1281
|
|
|
1244
1282
|
function closeMigrationModal() {
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
background: var(--bg-card);
|
|
5
5
|
border: 1px solid var(--border-color);
|
|
6
6
|
border-radius: 12px;
|
|
7
|
-
margin-bottom: 0.75rem;
|
|
8
7
|
overflow: hidden;
|
|
9
8
|
}
|
|
10
9
|
|
|
@@ -1143,4 +1142,126 @@
|
|
|
1143
1142
|
gap: 0.75rem;
|
|
1144
1143
|
justify-content: flex-end;
|
|
1145
1144
|
}
|
|
1145
|
+
|
|
1146
|
+
/* Report details grid (documentation + filters side by side) */
|
|
1147
|
+
.report-details-grid {
|
|
1148
|
+
display: grid;
|
|
1149
|
+
grid-template-columns: 1fr 1fr;
|
|
1150
|
+
gap: 1rem;
|
|
1151
|
+
align-items: start;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
@media (max-width: 1200px) {
|
|
1155
|
+
.report-details-grid {
|
|
1156
|
+
grid-template-columns: 1fr;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/* Filter Section */
|
|
1161
|
+
.filter-section {
|
|
1162
|
+
background: var(--bg-card);
|
|
1163
|
+
border: 1px solid var(--border-color);
|
|
1164
|
+
border-radius: 12px;
|
|
1165
|
+
overflow: hidden;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
.filter-details {
|
|
1169
|
+
margin: 0;
|
|
1170
|
+
display: block;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
.filter-toggle {
|
|
1174
|
+
display: flex;
|
|
1175
|
+
align-items: center;
|
|
1176
|
+
gap: 0.75rem;
|
|
1177
|
+
padding: 1rem 1.25rem;
|
|
1178
|
+
margin: 0;
|
|
1179
|
+
cursor: pointer;
|
|
1180
|
+
user-select: none;
|
|
1181
|
+
color: var(--text-secondary);
|
|
1182
|
+
font-weight: 500;
|
|
1183
|
+
transition: all 0.15s;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
.filter-toggle:hover {
|
|
1187
|
+
background: var(--bg-tertiary);
|
|
1188
|
+
color: var(--text-primary);
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
.filter-details[open] .filter-toggle {
|
|
1192
|
+
border-bottom: 1px solid var(--border-color);
|
|
1193
|
+
background: var(--bg-tertiary);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
.filter-details[open] .toggle-icon {
|
|
1197
|
+
transform: rotate(90deg);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
.filter-details:not([open]) .filter-toggle {
|
|
1201
|
+
background: transparent;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
.filter-details:not([open]) .filter-toggle:hover {
|
|
1205
|
+
background: var(--bg-tertiary);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.filter-content {
|
|
1209
|
+
padding: 1rem 1.25rem;
|
|
1210
|
+
background: var(--bg-card);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
.filter-grid {
|
|
1214
|
+
display: flex;
|
|
1215
|
+
flex-direction: column;
|
|
1216
|
+
gap: 0.75rem;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
.filter-item {
|
|
1220
|
+
display: grid;
|
|
1221
|
+
grid-template-columns: 1fr auto;
|
|
1222
|
+
gap: 1.5rem;
|
|
1223
|
+
align-items: start;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
.filter-label {
|
|
1227
|
+
display: flex;
|
|
1228
|
+
flex-direction: column;
|
|
1229
|
+
gap: 0.25rem;
|
|
1230
|
+
font-weight: 500;
|
|
1231
|
+
color: var(--text-primary);
|
|
1232
|
+
font-size: 0.9rem;
|
|
1233
|
+
line-height: 1.4;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
.filter-description {
|
|
1237
|
+
font-size: 0.8rem;
|
|
1238
|
+
font-weight: 400;
|
|
1239
|
+
color: var(--text-secondary);
|
|
1240
|
+
opacity: 0.8;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
.filter-current-value {
|
|
1244
|
+
font-size: 0.8rem;
|
|
1245
|
+
font-weight: 400;
|
|
1246
|
+
color: var(--accent-blue);
|
|
1247
|
+
opacity: 0.9;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
.filter-input {
|
|
1251
|
+
padding: 0.625rem 0.875rem;
|
|
1252
|
+
border: 1px solid var(--border-color);
|
|
1253
|
+
border-radius: 8px;
|
|
1254
|
+
font-size: 1rem;
|
|
1255
|
+
background: var(--bg-primary);
|
|
1256
|
+
color: var(--text-primary);
|
|
1257
|
+
transition: all 0.15s;
|
|
1258
|
+
min-width: 120px;
|
|
1259
|
+
max-width: 200px;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
.filter-input:focus {
|
|
1263
|
+
outline: none;
|
|
1264
|
+
border-color: var(--accent-blue);
|
|
1265
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
1266
|
+
}
|
|
1146
1267
|
</style>
|
|
@@ -50,59 +50,101 @@
|
|
|
50
50
|
</div>
|
|
51
51
|
<% end %>
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
144
|
+
</details>
|
|
103
145
|
</div>
|
|
104
|
-
|
|
105
|
-
|
|
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;">
|
data/config/locales/en.yml
CHANGED
|
@@ -295,6 +295,19 @@ en:
|
|
|
295
295
|
- "Target value: >99% for OLTP, >95% for mixed workload."
|
|
296
296
|
- "Low cache hit: increase shared_buffers (up to 25% RAM), or the problem is in queries."
|
|
297
297
|
|
|
298
|
+
# === SCHEMA ANALYSIS ===
|
|
299
|
+
missing_validations:
|
|
300
|
+
title: "Missing Validations"
|
|
301
|
+
what: "Unique indexes in the database without corresponding uniqueness validations in Rails models."
|
|
302
|
+
how: "Analyzes all unique indexes (including composite ones) and checks if the corresponding Rails model has validates :column, uniqueness: true validation."
|
|
303
|
+
nuances:
|
|
304
|
+
- "Database constraints (indexes) and model validations serve different purposes — both should be present."
|
|
305
|
+
- "Unique index prevents duplicates at database level, validation provides user-friendly error messages."
|
|
306
|
+
- "For composite indexes (a, b), need validation with scope: validates :a, uniqueness: { scope: :b }"
|
|
307
|
+
- "Some tables may not have models — this is normal for join tables or legacy tables."
|
|
308
|
+
- "Validations in concerns and parent models are also detected."
|
|
309
|
+
- "Primary keys don't need validations — they are automatically unique."
|
|
310
|
+
|
|
298
311
|
# Problem explanations for highlighting
|
|
299
312
|
problems:
|
|
300
313
|
high_mean_time: "High mean execution time. Consider adding indexes or optimizing the query."
|
data/config/locales/ru.yml
CHANGED
|
@@ -295,6 +295,19 @@ ru:
|
|
|
295
295
|
- "Целевое значение: >99% для OLTP, >95% для mixed workload."
|
|
296
296
|
- "Низкий cache hit: увеличьте shared_buffers (до 25% RAM), или проблема в запросах."
|
|
297
297
|
|
|
298
|
+
# === SCHEMA ANALYSIS ===
|
|
299
|
+
missing_validations:
|
|
300
|
+
title: "Отсутствующие валидации"
|
|
301
|
+
what: "Уникальные индексы в базе данных без соответствующих валидаций uniqueness в Rails-моделях."
|
|
302
|
+
how: "Анализирует все уникальные индексы (включая композитные) и проверяет наличие validates :column, uniqueness: true в соответствующей Rails-модели."
|
|
303
|
+
nuances:
|
|
304
|
+
- "Ограничения БД (индексы) и валидации модели служат разным целям — оба должны присутствовать."
|
|
305
|
+
- "Уникальный индекс предотвращает дубликаты на уровне БД, валидация даёт понятные сообщения об ошибках."
|
|
306
|
+
- "Для композитных индексов (a, b) нужна валидация со scope: validates :a, uniqueness: { scope: :b }"
|
|
307
|
+
- "Некоторые таблицы могут не иметь моделей — это нормально для join-таблиц или legacy-таблиц."
|
|
308
|
+
- "Валидации в concerns и родительских моделях также детектируются."
|
|
309
|
+
- "Первичные ключи не требуют валидаций — они автоматически уникальны."
|
|
310
|
+
|
|
298
311
|
# Problem explanations for highlighting
|
|
299
312
|
problems:
|
|
300
313
|
high_mean_time: "Высокое среднее время выполнения. Рассмотрите добавление индексов или оптимизацию запроса."
|